Map集合:
HashMap底层结构示意图:
HashMap是一个“链表散列”,其底层是一个数组,数组里面的每一项都是一条单链表。
数组和链表中每一项存的都是一“Entry对象”,该对象内部拥有key(键值名),value(键值),next(指向下一个结点)等属性。
一、HashMap属性:
内部属性源码解析:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容器初始容量,出于hash计算时的优化的考虑,必为2^n,此处为16 static final int MAXIMUM_CAPACITY = 1 << 30;// 默认最大容量 static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,关系到容器何时扩容 static final Entry<?,?>[] EMPTY_TABLE = {};//扩容之前的默认数组 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;// 用来存储数据(单向链表)的数组,长度就是容器长度即2^n transient int size;// 当前map的key-value映射数 int threshold;// 下一次扩容的大小(第一次创建空hashmap时,会上传一个该值,用于下一次创建table时设置大小) final float loadFactor;// 自定义的负载因子,关系到容器何时扩容 transient int modCount;// Hash表结构性修改次数,用于实现迭代器快速失败行为 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//容量阈值的默认大小 transient int hashSeed = 0;//该值在计算hash值时会用到,大小与table容量有关,由于初始table为空,所以此值初始也为空
二、HashMap构造:
构造函数poublic源码解析:
//生成空hashmap时指定容量大小和负载因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)//初始容量大小不能小于0 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//保证初始容量不大于默认的最大容量 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))//负载因子不能小于0,且只能为数字 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor;//设置负载因子 threshold = initialCapacity;//设置下次创建数组的大小。此时因为建的是个空map,并不会立刻通过该值初始化数组,所以这个初始化大小值会先存起来,当以后put数据后,再根据该值创建初始数组的大小 init();//进行一些必要的初始化,但在hashmap中没有实现,init()只在LinkedHashMap中有实现 } //生成空hashmap时只指定初始容量大小,负载因子使用默认值 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //生成空hashmap时,初始容量和负载因子都是用默认值 public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } //构造hashmap时传入一个map,负载因子使用默认值,初始容量与传入的map的大小有关 public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold);//对table扩容(新建一个table) putAllForCreate(m);//将指定map的键值对添加到当前map中 }
相关private、friendly源码解析:
//将指定map的键值对添加到当前map中 private void putAllForCreate(Map<? extends K, ? extends V> m) { for (Map.Entry<? extends K, ? extends V> e : m.entrySet())//利用foreach迭代出传入map里的每一项entry对象 putForCreate(e.getKey(), e.getValue());//调用私有方法存入该entry。(该私有put与public的put的主要区别是,该私有put不会扩容table(因为之前已经扩过了),也不会增加modCount) } //存入键值对 private void putForCreate(K key, V value) { int hash = null == key ? 0 : hash(key);//根据key值生成hash值 int i = indexFor(hash, table.length);//根据hash值生成数组下标索引 //根据新存入值的下标,查询现有数组中该下标里是否有元素,如果有元素,由于该元素一定是以链表的形式存储的,则将插入元素的key与查询链表里的每一项元素的key进行比对 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } } createEntry(hash, key, value, i);//在指定索引处的链表上创建该键值对 } //在table数组的某一项中,创建单向链表的表头 void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
Entry类源码解析:
//在数组的每一项里存储了一条单向链表,而每一条链表里存的都是这种Entry对象。key-value实际上就是被存成了这种Entry对象 static class Entry<K,V> implements Map.Entry<K,V> { final K key;//键值名 V value;//键值 Entry<K,V> next;//指向单向链表中的下一个entry对象 int hash;//hash值 //创建Entry对象时的构造函数 Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } //获取entry的key public final K getKey() { return key; } //获取entry的value public final V getValue() { return value; } //用新value覆盖旧value,并返回旧value public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } //如果传入的是个Entry对象,则判断其key和value是否相同,相等则返回true public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } //key与value的hashcode()做^运算 public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } //每当相同key的value被覆盖时被调用一次,HashMap的子类LinkedHashMap中实现了这个方法 void recordAccess(HashMap<K,V> m) { } //每移除一个entry就调用一次 void recordRemoval(HashMap<K,V> m) { } }
相关说明:
1、从源码可看出构造函数主要是负责在构造时指定“初始容量”和“负载因子”。
2、如果此时创建的是一个空map,则并不会直接创建其内部table数组,而是等到有数据添加进来后再创建。
但如果此时是根据传入的新map来构建此hashmap,则会先创建一个空hashmap,再根据初始化容量创建一个table数组,之后再将传入的map里的数组存入刚刚创建的table中。
3、关于负载因子(loadFactor),表示当前容量达到总容量的什么程度时再扩容。
如,loadFactor为0.75,且设置hashmap最大容量是16。当当前的数据量达到12时(16*0.75=12),就会进行扩容。同理,如果loadFactor为1,则当前的数据容量达到16时才会扩容。
4、“初始容量”在设置时不需要设置为2^n的形式,但在使用该值扩容(创建数组)时,就会查找“大于该值的最小2^n”。
三、HashMap存储:
相关public源码解析:
//向hashmap存入一个键值对,如果该key已经存在,则覆盖该key的内容 public V put(K key, V value) { if (table == EMPTY_TABLE) {//如果此时数组为空,数组为空,则新建数组(扩容数组) inflateTable(threshold); } if (key == null)//hashmap中允许key为null,如果key为null,则对value的存储位置特殊处理 return putForNullKey(value); int hash = hash(key);//根据key值,生成hash值 int i = indexFor(hash, table.length);//根据hash值找到该值在table数组中的下标索引 //根据新存入值的下标,查询现有数组中该下标里是否有元素,如果有元素,由于该元素一定是以链表的形式存储的,则将插入元素的key与查询链表里的每一项元素的key进行比对 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果新插入元素的key与链表中元素的key的“equals()与hashCode()”都相等,则覆盖value V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++;//该hashmap的操作次数+1,与迭代器迭代有关 addEntry(hash, key, value, i);//运行此方法表示该key值是新key,所以将存入的键值对变成一个“Entry对象”存入数组。 return null; } //将指定map中的元素存入此hashmap public void putAll(Map<? extends K, ? extends V> m) { int numKeysToBeAdded = m.size();//传入map的key-value映射数量 if (numKeysToBeAdded == 0)//传入map中的映射数量为0则不存 return; if (table == EMPTY_TABLE) {//判断当前hashmap中是否有初始table数组,没有则创建(扩容)初始table inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold)); } if (numKeysToBeAdded > threshold) {//传入map的key-value映射数量如果大于当前的“扩容大小”,则判断是否扩容 int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1); if (targetCapacity > MAXIMUM_CAPACITY)//与hashmap容量默认最大值比较,不能大于最大值 targetCapacity = MAXIMUM_CAPACITY; int newCapacity = table.length; while (newCapacity < targetCapacity)//如果当前数组长度小于targetCapacity,则扩大一个“2次方” newCapacity <<= 1;//按位运算,相当于扩大了一个平方 if (newCapacity > table.length)//如果newCapacity扩大完后大于了当前数组的长度,则重新扩容数组 resize(newCapacity); } //foreach传入的map,将里面的每个entry对象存入此map for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) put(e.getKey(), e.getValue()); }
相关private、friendly源码解析:
相关说明:
四、HashMap提取:
五、HashMap遍历:
六、HashMap线程相关:
七、HashMap其他注意事项:
1、只有JDK7之前是这一套底层结构,目前最新的JDK8中HashMap当同一个hash值的节点数大于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。