本文的目的是阅读理解HashMap的源码,做为集合中重要的一个角色,平时用到十分多的一个类,深刻理解它,知其因此然很重要。本文基于Jdk1.7,由于Jdk1.8改变了HashMap的数据结构,进行了优化,咱们先从基础阅读,以后再阅读理解Jdk1.8的内容数组
1.经过key-value的形式快速的存取元素
2.容许键为Null,但只容许有一个键的值为Null
3.线程不安全
4.底层结构是Hash表,元素是无序的
5.再不考虑Hash冲突的时候,插入和查询的复杂度是能够达到O(1)的安全
底层数据结构是一个Hash表,基于数组和链表,数组里面保存着一个单向链表的头节点,单项链表保存着具备相同Hash值的不一样元素,再不发生Hash冲突的状况下,链表应该只有一个元素,这是最理想的状态。数据结构
链表的数据结构代码
`函数
static class Entry<K, V> implements Map.Entry<K, V> { final K key; V value; Entry<K, V> next; // 下一个Entry对象的引用 int hash; // 其实就是key的hash值 }
// 默认初始化容量 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // HashMap容许的最大容量 2^30 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的负载率 75% static final float DEFAULT_LOAD_FACTOR = 0.75f; // 空的哈希表 static final Entry<?, ?>[] EMPTY_TABLE = {}; // 实际使用的哈希表 transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE; // HashMap的大小,即存储的key-value的数量 transient int size; // 扩容的阀值,当HashMap的size达到阀值时,就开始扩容 threshold=length*threshold int threshold; // 负载率 final float loadFactor; // 修改次数, 用于fail-fast机制 transient int modCount; // 替代哈希使用的默认扩容阀值 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; // 随机的哈希种子, 有助于减小发生哈希碰撞的概率 transient int hashSeed = 0;
HashMap的初始化涉及到上面的多个常量,在了解完常量的做用以后,咱们就能够理解HashMap的初始化思想,首先,HashMap并非经过构造函数来初始化的,构造函数只是初始化HashMap的初始化参数,包括DEFAULT_INITIAL_CAPACITY ,loadFactor等,再初始化参数以后,真正的调用Put方法时,会判断table 是否已经初始化,没有的话再根据参数进行初始化。性能
put方法的流程咱们这边也要先理解:
(1)检查哈希表是不是个空表,若是是空表就调用inflateTable方法进行初始化
(2)判断key是否为null,若是为null,就调用putForNullKey方法, 将key为null的key-value存储在哈希表的第一个位置中
若是key不为null,则调用hash方法计算key的hash值
(3)根据hash值和Entry数组的长度定位到Entry数组的指定槽位
(4)判断Entry数组指定槽位的值e是否为null, 若是e不为null, 则遍历e指向的单链表, 若是传入的key在单链表中已经存在了, 就进行替换操做, 不然就新建一个Entry并添加到单链表的表头位置
(5)若是e为null, 就新建一个Entry并添加到指定槽位优化
下面是代码:
构造方法this
public HashMap(int initialCapacity, float loadFactor) { // 若是初始容量小于0,则抛出异常 if (initialCapacity < 0) { throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); } // 若是初始容量大于容量最大值,则使用最大值做为初始容量 if (initialCapacity > MAXIMUM_CAPACITY) { initialCapacity = MAXIMUM_CAPACITY; } // 若是负载率小于等于0或负载率不是浮点数,则抛出异常 if (loadFactor <= 0 || Float.isNaN(loadFactor)) { throw new IllegalArgumentException("Illegal load factor: " + loadFactor); } // 设置负载率 this.loadFactor = loadFactor; // 设置阀值为初始容量 threshold = initialCapacity; // 空实现, 交由子类实现 init(); } //
初始化数组方法spa
private void inflateTable(int toSize) { // 寻找大于toSize的,最小的,2的n次方做为新的容量 int capacity = roundUpToPowerOf2(toSize); // 阀值=容量*负载因子, 若是容量*负载因子>最大容量时, 阀值=最大容量 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 按新的容量建立一个新的数组 table = new Entry[capacity]; // 从新初始化hashSeed initHashSeedAsNeeded(capacity); }
put方法线程
public V put(K key, V value) { // 若是哈希表没有初始化就进行初始化 if (table == EMPTY_TABLE) { // 初始化哈希表 inflateTable(threshold); } // 当key为null时,调用putForNullKey方法,保存null于table的第一个位置中,这是HashMap容许为null的缘由 if (key == null) { return putForNullKey(value); } // 计算key的hash值 int hash = hash(key); // 根据key的hash值和数组的长度定位到entry数组的指定槽位 int i = indexFor(hash, table.length); // 获取存放位置上的entry,若是该entry不为空,则遍历该entry所在的链表 for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; // 经过key的hashCode和equals方法判断,key是否存在, 若是存在则用新的value取代旧的value,并返回旧的value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 修改次数增长1 modCount++; // 若是找不到链表 或者 遍历完链表后,发现key不存在,则建立一个新的Entry,并添加到HashMap中 addEntry(hash, key, value, i); return null; } void addEntry(int hash, K key, V value, int bucketIndex) { //添加key到table[bucketIndex]位置,新的元素老是在table[bucketIndex]的第一个元素,原来的元素后移 Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //判断元素个数是否达到了临界值,若已达到临界值则扩容,table长度翻倍 if (size++ >= threshold) resize(2 * table.length); }
当key值为Null的时候会进行特殊处理,在table[0]的链表上查找Key为null的元素,get的过程是:
(1)计算hash与table.length取模计算index值
(2)遍历table[index]上的链表,直到找到key设计
void addEntry(int hash, K key, V value, int bucketIndex) { //添加key到table[bucketIndex]位置,新的元素老是在table[bucketIndex]的第一个元素,原来的元素后移 Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //判断元素个数是否达到了临界值,若已达到临界值则扩容,table长度翻倍 if (size++ >= threshold) resize(2 * table.length); }
#HashMap的删
remove方法一样也是,先计算hash,在计算index,遍历查找,找到以后删除节点
/** * 根据key删除元素 */ public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e. value); } /** * 根据key删除链表节点 */ final Entry<K,V> removeEntryForKey(Object key) { // 计算key的hash值 int hash = (key == null) ? 0 : hash(key.hashCode()); // 根据hash值计算key在数组的索引位置 int i = indexFor(hash, table.length ); // 找到该索引出的第一个节点 Entry<K,V> prev = table[i]; Entry<K,V> e = prev; // 遍历链表(从链表第一个节点开始next),找出相同的key, while (e != null) { Entry<K,V> next = e. next; Object k; // 若是hash值和key都相等,则认为相等 if (e.hash == hash && ((k = e. key) == key || (key != null && key.equals(k)))) { // 修改版本+1 modCount++; // 计数器减1 size--; // 若是第一个就是要删除的节点(第一个节点没有上一个节点,因此要分开判断) if (prev == e) // 则将下一个节点放到table[i]位置(要删除的节点被覆盖) table[i] = next; else // 不然将上一个节点的next指向当要删除节点下一个(要删除节点被忽略,没有指向了) prev. next = next; e.recordRemoval( this); // 返回删除的节点内容 return e; } // 保存当前节点为下次循环的上一个节点 prev = e; // 下次循环 e = next; } return e; }
resize扩容是HashMap中很是重要的一个操做,在容器里的元素达到一个临界值时,HashMap会自动进行扩容,扩容的具体流程是;
1.在put的时候检查是否须要扩容,根据两个参数:初始容量和装载因子
2.建立一个容量为table.length*2的table,修改临界值
3.从新计算全部元素的hash值,并放入新的table,使用的是头插法
4.用新的table替换旧的table
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30 threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity];//新建一个新表 boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash transfer(newTable, rehash);//完成旧表到新表的转移 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } --------------------- void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) {//遍历同桶数组中的每个桶 while(null != e) {//顺序遍历某个桶的外挂链表 Entry<K,V> next = e.next;//引用next if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不一样的桶中去了,有效的缓解了哈希冲突。 e.next = newTable[i];//头插法插入新表中 newTable[i] = e; e = next; } } }
扩容的总体操做如上,可是有一些十分精妙的细节十分厉害
这么设计固然是为了性能,并且是十分显著的性能提高,涉及到了位操做,我以为很是有意思,会在下一篇专门讲这样计算进行提高性能的例子。