Map集合
Map集合的概念理解
在现实生活中,我们经常会碰见一对一标识的例子:如身份证号与个人、IP地址与主机名等。这些一一对应的关系就是映射。在Java之中也能为我们的对象添加一个“一”的对应关系—Map<K,V>集合
,位于java.util.Map<K,V>
包下。里面的K
代表该集合中唯一对应的那个身份,也叫做键。里面的V
代表该集合中的元素值,也就是我们要存储的数据。
集合分为单列集合
和双列集合
。
- 单列集合:主要指的就是
Collection<E>
系列的集合,里面存储的是一个一个
的元素。这些数据结合在一起就成了一组数据,也就是集合。里面的每一个元素都是孤立存在的。 - 双列集合:主要指的是
Map<K,V>
系列的集合,里面存储的是一对一对的键-值对
,其中键不能重复,值可以重复。
Map常用方法
Map<K,V>是map系列集合的根接口,Map<K,V>接口中提供了一些简单操作map集合的方法让子类去重写。
添加操作
V put(K key,V value):
添加一组映射关系,往集合中添加一组数据void putAll(Map<? extends K,? extends V> m):
将指定map集合m中的所有映射关系添加现有map集合中,为当前map集合添加另外一个m集合的所有元素。
删除操作
V remove(Object key):
判断map集合中是否包含键key,如果包含将该key-value映射关系移除,并返回移除的key的value值default boolean remove(Object key,Object value):
判断当前map集合中是否包含映射关系key-value,如果包含返回true,并将该映射关系移除,如果不包含返回false。void clear():
清除该map中的所有映射关系
修改操作(了解即可)使用put()同样可以完成修改操作
- defalut V replace(K key,V value):替换指定键key的值为value。返回值为key位置修改之前的值
- default boolean replace(K key,V oleValue,V newValue):替换指定键key的oldValue为newValue。返回值为true或false
查询操作
V get(Object key):
查询map集合中指定键key的值并返回。boolean containsKey(Object key):
判断map集合中是否含有指定键key。boolean containsValue(Object value):
判断map集合中是否含有指定值value。
元视图操作的方法(用于获取遍历集合的方法)
Set<K> keySet():
返回该map集合中所有的键所组成的Set集合。(键不可重复)Set<Map.Entry<K,V>> entrySet():
返回map集合中所有的键值对组成的Set集合。Entry指Map中的内部接口Collection<V> values():
返回map集合中所有的value值组成的Collection集合。(值可以重复)
其他操作
int size():
返回该map集合中的键值对数量。如果值为空或键为空仍会计算在内,如果值和键都为空不会计算在内。boolean isEmpty():
判断该map集合是否为空。如果值为空或键为空或都为true,结果都不为空。- …
代码测试:
public class MapTest {
public static void main(String[] args) {
Map<Integer,Object> map = new HashMap<>();
map.put(1,"map");
map.put(2,"hashMap");
map.put(3,"treeMap");
System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}
Map<Integer,Object> mapNew = new HashMap<>();
mapNew.put(4,"linkedHashMap");
mapNew.put(5,"hashTable");
System.out.println("mapNew = " + mapNew); //{4=linkedHashMap, 5=hashTable}
map.putAll(mapNew);
System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap, 4=linkedHashMap, 5=hashTable}
Object remove = map.remove(4);
System.out.println("remove = " + remove); //remove = linkedHashMap
System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap, 5=hashTable}
boolean b1 = map.remove(6, "123");
boolean b = map.remove(5, "hashTable");
System.out.println("b1 = " + b1); //b1 = false
System.out.println("b = " + b); //b = true
System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}
mapNew.clear();
System.out.println("mapNew = " + mapNew); //mapNew = {}
//map.put(1,"hashTable");
//System.out.println("map = " + map); //map = {1=hashTable, 2=hashMap, 3=treeMap}
Object table = map.replace(1, "hashTable");
System.out.println("table = " + table); //table = map
System.out.println("map = " + map); //map = {1=hashTable, 2=hashMap, 3=treeMap}
boolean replace = map.replace(1, "hashTable", "map");
System.out.println("replace = " + replace); //replace = true
System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}
Object o = map.get(1);
System.out.println("o = " + o); //o = map
boolean containsKey = map.containsKey(3);
System.out.println("containsKey = " + containsKey); //containsKey = true
boolean hashMap = map.containsValue("hashMap");
System.out.println("hashMap = " + hashMap); //hashMap = true
Set<Integer> keySet = map.keySet();
for (Integer integer : keySet) { // 1 2 3
System.out.print(" " + integer);
}
System.out.println();
Collection<Object> values = map.values();
for (Object value : values) { // map hashMap treeMap
System.out.print(" " + value);
}
System.out.println();
Set<Map.Entry<Integer, Object>> entrySet = map.entrySet();
for (Map.Entry<Integer, Object> entry : entrySet) { // 键1的值:map 键2的值:hashMap 键3的值:treeMap
Integer key = entry.getKey();
Object value = entry.getValue();
System.out.print("键"+key+"的值:"+value+" ");
}
System.out.println();
System.out.println("map = " + map); //map = {1=map, 2=hashMap, 3=treeMap}
map.put(null,"null"); //4
map.put(7,null); //5
map.put(null,null); //5
int size = map.size();
System.out.println("size = " + size); //size = 5
boolean empty = map.isEmpty(); //false
boolean newEmpty = mapNew.isEmpty(); //true
//mapNew.put(null,"null"); //false
//newEmpty = mapNew.isEmpty();
//mapNew.put(9,null); //false
//newEmpty = mapNew.isEmpty();
mapNew.put(null,null); //false
newEmpty = mapNew.isEmpty();
System.out.println("empty = " + empty); //empty = false
System.out.println("newEmpty = " + newEmpty); //newEmpty = false
}
}
结果演示:
注意:
如果使用put方法时,如果是第一次添加该元素,即原来map集合中没有键为添加元素的键,也就表示没有这个键对应的值,返回null,并把新的元素放到map集合中。
如果map集合中含有该键,则会先将这个键对应的value值返回,再修改该键的值为新的值。
Map集合遍历方式
在单列集合Collection中可以通过Iterator对象进行遍历或增强for循环进行遍历。但是Map集合中没有实现获取iterator()的方法,所以不能使用获取迭代器对象的方法进行遍历,Map集合也没有继承java.lang.Iterable<T>
接口,所以也不能使用增强for循环进行遍历。
Map集合可以借助Collection集合进行遍历。简单的遍历主要分为以下三种:
- 单独遍历所有的key:使用keySet()方法
- 单独遍历所有value:使用values()方法
- 成对的遍历Map系列的集合中的每一对Map.Entry子接口的实现类对象:使用entrySet()方法。
代码测试:
public class GetMapElementTest {
public static void main(String[] args) {
Map<Integer,Object> map = new HashMap<>();
map.put(1001,"map");
map.put(1002,"hashMap");
map.put(1003,"treeMap");
System.out.println("map = " + map); //map = {1001=map, 1002=hashMap, 1003=treeMap}
Set<Integer> keySet = map.keySet();
for (Integer integer : keySet) { //键:1001,值:map 键:1002,值:hashMap 键:1003,值:treeMap
System.out.println("键:"+integer+",值:"+map.get(integer));
}
System.out.println("=======================================");
Collection<Object> values = map.values();
for (Object value : values) { //map hashMap treeMap
System.out.print(value+" ");
}
System.out.println();
System.out.println("==================================================");
Set<Map.Entry<Integer, Object>> entrySet = map.entrySet();
for (Map.Entry<Integer, Object> entry : entrySet) { //键:1001,值:map 键:1002,值:hashMap 键:1003,值:treeMap
Integer key = entry.getKey();
Object value = entry.getValue();
System.out.println("键:"+key+",值:"+value);
}
}
}
结果演示:
Map集合的实现类
Map接口是双列集合的父接口,而实现了Map接口的实现类也常常被叫做map集合。Map接口的常用的实现类有:HashMap
、LinkedHashMap
、TreeMap
和Properties
等。其中使用最多的是HashMap
。
HashMap集合
HashMap的底层实现与扩容机制:
HashMap在JDK1.8之前:底层实现是数组+链表,扩容机制是当table中元素的个数已经达到阈值(table.length*0.75)时并且新添加[index]桶已经是非空,那么table.length需要扩容为2倍。
HashMap在JDK1.8之后:底层实现是数组+链表/红黑树,扩容机制(1)是当table中元素的个数已经达到阈值(table.length*0.75)时并且新添加[index]桶已经是非空,那么table需要扩容为2倍。(2)当添加到[index]下时,发现[index]下的链表结点个数已经达到8个,而table的长度未达到64,此时table.length也会扩容为2倍
HashMap构造方法:
- HashMap();
- HashMap(int initialCapacity);//指定初始化容量
HashMap和Hashtable的区别
(1)线程安全性不同
HashMap 是线程不安全的,HashTable 是线程安全的,其中的方法是 Synchronize 的,在多线程并发的情况下,可以直接使用 HashTable,但是使用 HashMap 时必须自己增加同步处理。
(2)是否提供 contains 方法
HashMap 只有 containsValue 和 containsKey 方法;HashTable 有 contains、containsKey 和 containsValue 三个方法,其
中 contains 和 containsValue 方法功能相同。
(3)key 和 value 是否允许 null 值
Hashtable 中,key 和 value 都不允许出现 null 值。HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null。
(4)数组初始化和扩容机制
HashTable 在不指定容量的情况下的默认容量为 11,而 HashMap 为 16,Hashtable 不要求底层数组的容量一定要为2 的整数次幂,而 HashMap 则要求一定为 2 的整数次幂。Hashtable 扩容时,将容量变为原来的 2 倍加 1,而 HashMap 扩容时,将容量变为原来的 2倍。
代码:
public static void main(String[] args) {
HashMap<String,Double> map = new HashMap<>();
map.put("张三", 10000.0);
//key相同,新的value会覆盖原来的value
//因为String重写了hashCode和equals方法
map.put("张三", 12000.0);
map.put("李四", 14000.0);
//HashMap支持key和value为null值
String name = null;
Double salary = null;
map.put(name, salary);
Set<Entry<String, Double>> entrySet = map.entrySet();
for (Entry<String, Double> entry : entrySet) {
System.out.println(entry);
}
}
LinkedHashMap
LinkedHashMap 是 HashMap 的子类。此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。
public static void main(String[] args) {
LinkedHashMap<String,Double> map = new LinkedHashMap<>();
map.put("张三", 10000.0);
//key相同,新的value会覆盖原来的value
//因为String重写了hashCode和equals方法
map.put("张三", 12000.0);
map.put("李四", 14000.0);
String name = null;
Double salary = null;
map.put(name, salary);
Set<Entry<String, Double>> entrySet = map.entrySet();
for (Entry<String, Double> entry : entrySet) {
System.out.println(entry);
}
}
TreeMap
基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
代码:
public class TestTreeMap {
@Test
public void defaultComparableTest() {
TreeMap<String,Integer> map = new TreeMap<>();
map.put("Jack", 11000);
map.put("Alice", 12000);
map.put("zhangsan", 13000);
map.put("baitao", 14000);
map.put("Lucy", 15000);
//String实现了Comparable接口,默认按照Unicode编码值排序
Set<Entry<String, Integer>> entrySet = map.entrySet();
for (Entry<String, Integer> entry : entrySet) {
System.out.println(entry);
}
}
@Test
public void selfComparatorTest() {
//指定定制比较器Comparator,按照Unicode编码值排序,但是忽略大小写
TreeMap<String,Integer> map = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
});
map.put("Jack", 11000);
map.put("Alice", 12000);
map.put("zhangsan", 13000);
map.put("baitao", 14000);
map.put("Lucy", 15000);
Set<Entry<String, Integer>> entrySet = map.entrySet();
for (Entry<String, Integer> entry : entrySet) {
System.out.println(entry);
}
}
}
Set集合与Map集合的关系
Set集合的底层实际上都维护了一个Map集合,Set集合中不能重复的特点就是来源于Map集合的Key不能重复。HashSet的底层是一个HashMap,LinkedHashSet的底层是一个LinkedHashMap,TreeSet的底层就是一个TreeMap。
源码分析:
- HashSet的源码分析:
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//这个构造器是给子类LinkedHashSet调用的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
- LinkedHashSet的源码分析:
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);//调用HashSet的某个构造器
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);//调用HashSet的某个构造器
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);//调用HashSet的某个构造器
addAll(c);
}
- TreeSet的源码分析
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
Set集合的存储分析:
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public Iterator<E> iterator() {
return map.keySet().iterator();
}
Map的表结构与HashMap底层分析
Hash表:
在1.8之前的Hash表的底层是一个动态数组+链表
,在1.8之后Hash改成了动态数组+链表/红黑树
存储数据
当存入一个数据时进行二次hash运算选择到数组中对应的槽位上,当第二次运算进入到相同的槽位,就会在该槽位上进行尾插法
(1.7之前是头插法
)形成链表的某一个节点,这样避免了hash冲突。而为什么在1.8之后加上了红黑树,主要是查询性能的提升,从原来的 O(n)到 O(logn)。使用红黑树来替代超过 8 个节点的链表, 通过 hash 碰撞,让 HashMap 不断产生碰撞,那么相同的 key 的位置的链表就会不断增长,当对这个 Hashmap 的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树
HashMap底层分析(重点)
一.创建对象后成员变量的变化
// 默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
// 底层数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的加载因子/负载因子 扩容使用
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化阈值
static final int TREEIFY_THRESHOLD = 8;
// 反树化阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 树化底层顺序表最小长度
static final int MIN_TREEIFY_CAPACITY = 64;
// 底层数组的类型
transient Node<K,V>[] table;
// 键值对数量
transient int size;
// 阈值 长度*负载因子
int threshold;
// 加载因子
final float loadFactor;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
第一次创建对象 将默认的负载因子的值 赋值给 成员变量
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
二.添加数据
2.1 首次添加 key 为null
给底层数组开辟空间 长度为16
threshold=12
直接将元素放到指定的位置
2.2 指定位置第一次添加 key 不为null
直接添加
2.3 指定位置非第一次添加 hash相同 key 不相同
七上八下:
jdk7:后续添加元素在原有数据的上面(尾部)
jdk8:后续添加元素在原有数据的下面(尾部)
2.4 指定位置非第一次添加 hash相同 key 相同
新的value 替换旧的value 并将旧的value返回
2.5 扩容和树化
扩容:size>threshold进行扩容
数组长度*2
阈值*2
树化:
1.节点数量>=8
2.底层数组长度>=64