HashMap结构解析

一 简介

java.lang.Object  

↳     java.util.AbstractMap  

↳           java.util.HashMap  

public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { } 

HashMap是基于哈希表实现的,每个元素是一个key-value对,实现了Serializable、Cloneable接口,容许使用null值和null键。不保证映射的顺序,内部经过单链表解决冲突问题,容量超过(容量*加载因子)时,会自动增加。(除了不一样步和容许使用null以外,HashMap类与Hashtable大体相同)
HashMap不是线程安全的,若是想获取线程安全的HashMapjava

  • 1经过Collections类的静态方法synchronizedMap得到线程安全的HashMap。
    Map map = Collections.synchronizedMap(new HashMap()); 
  • 2使用concurrent并发包下的concurrentHashMap。

二 数据结构

HashMap由数组+链表组成的,主干是一个Entry数组,每个entry包含一个(key-value)键值对,链表则是主要为了解决哈希冲突而存在的,HashMap经过key的hashCode来计算hash值,当hashCode相同时,经过“拉链法”解决冲突,以下图所示。web

若是定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操做很快,仅需一次寻址便可;若是定位到的数组包含链表,对于添加操做,其时间复杂度依然为O(1),由于最新的Entry会插入链表头部,只须要简单改变引用链便可,而对于查找操做来说,此时就须要遍历链表,而后经过key对象的equals方法逐一比对查找。因此性能考虑,HashMap中的链表出现越少,性能才会越好。

 

三 源码分析

1 关键属性

//默认初始化化容量,即16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,即2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默认加载因子,当容器使用率达到75%的时候就扩容 static final float DEFAULT_LOAD_FACTOR = 0.75f; //HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化以前的状态 static final Entry<?,?>[] EMPTY_TABLE = {}; //空的存储实体 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //实际存储的key-value键值对的个数 transient int size; //扩容的临界点,若是当前容量达到该值,则须要扩容了. //若是当前数组容量为0时(空数组),则该值做为初始化内部数组的初始容量 int threshold; //由构造函数传入的指定负载因子 final float loadFactor; //修改次数,用于快速失败机制 transient int modCount; //默认的threshold值 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; 

2 构造方法

构造方法主要完成容量和加载因子的设置算法

/** * 经过初始容量和状态因子构造HashMap * @param initialCapacity 容量 * @param loadFactor 加载因子 */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)//参数有效性检查 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//参数有效性检查 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))//参数有效性检查 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; //init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现 init(); } 
/** * 经过扩容因子构造HashMap,容量去默认值,即16 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } 
/** * 加载因子取0.75,容量取16,构造HashMap */ public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } 
/** * 经过其余Map来初始化HashMap,容量经过其余Map的size来计算,加载因子取0.75 */ public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); //初始化HashMap底层的数组结构 inflateTable(threshold); //添加m中的元素 putAllForCreate(m); } 

3 存储数据(put)

/** * 存入一个键值对,若是key重复,则更新value * @param key 键值名 * @param value 键值 * @return 若是存的是新key则返回null,若是覆盖了旧键值对,则返回旧value */ public V put(K key, V value) { //若是数组为空,则新建数组 if (table == EMPTY_TABLE) { //初始化HashMap底层的数组结构 inflateTable(threshold); } //若是key为null,则把value放在table[0]中 if (key == null) return putForNullKey(value); //生成key所对应的hash值 int hash = hash(key); //根据hash值和数组的长度找到:该key所属entry在table中的位置i int i = indexFor(hash, table.length); /** * 数组中每一项存的都是一个链表, * 先找到i位置,而后循环该位置上的每个entry, * 若是发现存在key与传入key相等,则替换其value。而后结束方法。 * 若是没有找到相同的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))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //map操做次数加一 modCount++; //查看是否须要扩容,并将该键值对存入指定下标的链表头中 addEntry(hash, key, value, i); //若是是新存入的键值对,则返回null return null; } 
/** * 将该键值对存入指定下标的链表头中 * @param hash hash值 * @param key 键值名 * @param value 键值 * @param bucketIndex 索引 */ void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //当size超过临界阈值threshold,而且即将发生哈希冲突时进行扩容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } 
/** * 将键值对与他的hash值做为一个entry,插入table的指定下标中的链表头中 * @param hash hash值 * @param key 键值名 * @param value 键值 * @param bucketIndex 索引 */ 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++; } 

4 hash算法

static int hash(int h) { //此功能确保hash码不一样,有限数量的碰撞 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } 

5 调整容量(resize)

/** * 对数组扩容,即建立一个新数组,并将旧数组里的东西从新存入新数组 * @param newCapacity 新数组容量 */ void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length //若是当前数组容量已经达到最大值了,则将扩容的临界值设置为Integer.MAX_VALUE(Integer.MAX_VALUE是容量的临界点) if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //建立一个扩容后的新数组 Entry[] newTable = new Entry[newCapacity]; //将当前数组中的键值对存入新数组 transfer(newTable, initHashSeedAsNeeded(newCapacity)); //用新数组替换旧数组 table = newTable; //计算下一个扩容临界点 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } 
/** * 将现有数组中的内容从新经过hash计算存入新数组 * @param newTable 新数组 * @param rehash */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //遍历现有数组中的每个单链表的头entry for (Entry<K,V> e : table) { //查找链表里的每个entry while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } //根据新的数组长度,从新计算此entry所在下标i int i = indexFor(e.hash, newCapacity); //将entry放入下标i处链表的头部(将新数组此处的原有链表存入entry的next指针) e.next = newTable[i]; //将链表存回下标i newTable[i] = e; //查看下一个entry e = next; } } } 

6 数据读取(get)

/** * 返回此hashmap中存储的键值对个数 * @return 键值对个数 */ public int size() { return size; } 
/** * 根据key找到对应value * @param key 键值名 * @return 键值value */ public V get(Object key) { //若是key为null,则从table[0]中取value if (key == null) return getForNullKey(); //若是key不为null,则先根据key,找到其entry Entry<K,V> entry = getEntry(key); //返回entry节点里的value值 return null == entry ? null : entry.getValue(); } 
/** * 返回一个set集合,里面装的都是hashmap的value。 * 由于map中的key不能重复,set集合中的值也不能重复,因此能够装入set。 * 在hashmap的父类AbstractMap中,定义了Set<K> keySet = null; * 若是keySet为null,则返回内部类KeySet。 * @return 含有全部key的set集合 */ public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } 
/** * 返回一个Collection集合,里面装的都是hashmap的value。 * 由于map中的value能够重复,因此装入Collection。 * 在hashmap的父类AbstractMap中,定义了Collection<V> values = null; * 若是values为null,则返回内部类Values。 */ public Collection<V> values() { Collection<V> vs = values; return (vs != null ? vs : (values = new Values())); } 

7 移除数据(clear remove)

/** * 删除hashmap中的全部元素 */ public void clear() { modCount++; //将table中的每个元素都设置成null Arrays.fill(table, null); size = 0; } 
/** * 根据key删除entry节点 * @param key 被删除的entry的key值 * @return 被删除的节点的value,删除失败则返回null */ public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } 
/** * 根据key删除entry节点 * @param key 被删除的entry的key值 * @return 被删除的节点,删除失败则返回null */ final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } //计算key的hash值 int hash = (key == null) ? 0 : hash(key); //计算所属下标 int i = indexFor(hash, table.length); //找到下标所存储的单链表的头节点 Entry<K,V> prev = table[i]; Entry<K,V> e = prev; //迭代单链表找到要删除的节点 while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } 

8 Fail-Fast机制

“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操做时,有可能会产生fail-fast机制。 注意 :记住是有可能,而不是必定。 例如:假设存在两个线程(线程一、线程2),线程1经过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。数组

四 JDK1.7和1.8 HashMap的区别

1 数据结构

 

使用一个Node数组来存储数据,但这个Node多是链表结构,也多是红黑树结构。若是插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个桶位。若是同一个桶位里的key不超过8个,使用链表结构存储。若是超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。那么即便hashcode彻底相同,因为红黑树的特色,查找某个特定元素,也只须要O(logn)的开销。也就是说put/get的操做的时间复杂度最差只有O(log n)。
注意:有一个限制:key的对象,必须正确的实现了Compare接口。若是没有实现Compare接口,或者实现得不正确(比方说全部Compare方法都返回0)。那JDK1.8的HashMap其实仍是慢于JDK1.7的。

 

2 hash算法

/** * @param key * @return */ static final int hash(Object key) { int h; // h = key.hashCode() 为第一步 取hashCode值 // h ^ (h >>> 16) 为第二步 高位参与运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
做者:LiuJian-Android 连接:https://juejin.im/post/5a49931351882533f01ee996 来源:掘金 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
相关文章
相关标签/搜索