因为Java 1.7
和Java 1.8
的HashMap
的HashMap
中的put()
和get()
方法在实现上差别很大,因此本文将于分别分析这两个版本的put()
和get()
f方法源码分析
下面将会分析这部分的源码,若是以为源码分析内容太啰嗦,能够跳过源码部分,直接看源码下面的总结。学习
HashMap
的put()
方法是咱们最经常使用的方法,可是put()
方法是怎么工做的呢?this
public V put(K key, V value) { if (key == null)// 处理key为null的状况 return putForNullKey(value); // 计算key的hash值 int hash = hash(key); // 计算命中table的索引 int i = indexFor(hash, table.length); // 遍历命中的链表 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 存在key和hash值相同则替换value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 记录结构性变化 modCount++; // 增长新链表 addEntry(hash, key, value, i); // 上一次节点不存在,返回null return null; }
put()
方法其实是spa
key
为null
时,直接调用putForNullKey()
方法。不然进入下一步hash()
方法获取key
的hash
值,进入下一步indexFor()
计算命中的散列表table
的索引key
和hash
值相同的节点,则建立新的链表或尾部添加节点,不然替换对应节点的value
private V putForNullKey(V value) { // 遍历链表,可是命中的散列表的索引和key的hash值为0 // 后续逻辑与`put()`相似 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
putForNullKey
只是将命中散列表table
的索引和key
的hash
值都设置为0
,其余逻辑与put()
方法后续的逻辑一致。指针
/** * 计算命中散列表的索引 */ static int indexFor(int h, int length) { // 等价于length%h return h & (length-1); }
/** * hash值计算方法 */ final int hash(Object k) { int h = 0; // 使用替代的hash方法 if (useAltHashing) { if (k instanceof String) { // 为字符串则使用特定的hash方法 return sun.misc.Hashing.stringHash32((String) k); } // 使用特定的hash种子计算hash值 h = hashSeed; } h ^= k.hashCode(); // 这部分代码是为了减小哈希碰撞 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
void addEntry(int hash, K key, V value, int bucketIndex) { // 判断散列表是否须要扩容或者未初始化 if ((size >= threshold) && (null != table[bucketIndex])) { // 散列表扩容为原来的2倍 resize(2 * table.length); // 计算key的hash值,key为null则返回0 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 建立新的链表 // 若是链表已存在,则是将新节点插入头部(头插法) createEntry(hash, key, value, 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++; }
/** * HashMap的put()方法支持key/value为null */ public V put(K key, V value) { //其实是先调用HashMap的hash()方法获取到key的hash值 //而后调用HashMap的putVal()方法 return putVal(hash(key), key, value, false, true); }
put()
方法其实是code
hash()
方法获取到key
的hash
值putVal()
方法存储key-value
核心方法是putVal()
方法,下面我会先分析一下hash()
方法,由于这个方法涉及到hash
值这个关键属性的计算。对象
static final int hash(Object key) { int h; // key为null时,hash值为0 // key不为null时,调用key对象的hashCode()方法并经过位运算异或和无符号右移将高位分散到低位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hash()
方法指定了null
的hash
值为0
。这样就能够支持key
为null
。(h = key.hashCode()) ^ (h >>> 16)
这段代码经过位运算异或和无符号右移将高位分散到低位,这样作能够减小哈希碰撞的几率(这块不是很清楚原理,是从方法注释上了解到的)/** * Map.put()方法的实际实现 * * @param hash key的hash值 * @param key 键值对中的key * @param value 键值对中的value * @param onlyIfAbsent 若是为true,则键值对中的值已经存在则不修改这个值 * @param evict 若是为false,则是处于建立模式 * @return 上一次的value,若是上一次的value不存在,则为null */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab用于暂存散列表table。p为散列表中对应索引的链表的头节点的指针。n存储tab的长度。i则为命中的散列表的索引 Node<K,V>[] tab; Node<K,V> p; int n, i; //给tab和n赋值 //当tab为null或者tab的长度n为0时,触发resize()来初始化tab if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //使用(n - 1) & hash(等价于hash%n)计算命中的散列表索引,同时判断散列表对应索引的链表是否存在 if ((p = tab[i = (n - 1) & hash]) == null) //散列表对应索引的链表不存在则建立一个新的链表 tab[i] = newNode(hash, key, value, null); else {//散列表对应索引的链表已存在 Node<K,V> e; K k; // 判断头节点的hash值和key是否与入参的hash值和key一致。须要注意,null的hash值为0 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 对应的键值对已经存在,记录下来 e = p; else if (p instanceof TreeNode)//判断对应的链表是否转化为红黑树 //如果,则直接调用红黑树的putTreeVal()方法 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//链表的头节点与新的键值对不重复,即没有发生哈希碰撞 for (int binCount = 0; ; ++binCount) {//遍历链表 if ((e = p.next) == null) {//遍历到尾节点 //尾插法添加一个新的节点 p.next = newNode(hash, key, value, null); //链表长度大于阈值 if (binCount >= TREEIFY_THRESHOLD - 1) // 从-1开始,因此为阈值-1 // 将链表转化为红黑树 treeifyBin(tab, hash); // 中断循环 break; } // 判断当前遍历的节点的hash值和key是否与入参的hash值和key一致,即key是否已经存在 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // key已经存在,中断循环 break; // 记录当前遍历的节点 p = e; } } if (e != null) { // Map中存在重复的key V oldValue = e.value;//记录下旧值 if (!onlyIfAbsent || oldValue == null)//判断值存在是否能够进行修改以及旧值是否为null e.value = value;//修改该节点的值 afterNodeAccess(e);// 链表节点的回调方法,此处为空方法 return oldValue;//返回旧值 } } // HashMap发生结构变化,变化次数累加 ++modCount; // 键值对个数自增,同时判断是否达到扩容的阈值 if (++size > threshold) resize(); // 链表节点的回调方法,此处为空方法 afterNodeInsertion(evict); // 此处返回null是由于链表新增了节点,因此上一次的值必然为null return null; }
putVal()
方法的关键点:索引
table
没有初始化则调用reszie()
方法初始化。(n - 1) & hash
(等价于hash%n
)。其中n
为散列表长度,hash
为插入的键值对的key
的哈希值。null
,若为null
,则建立链表,不然进入下一步。key
和hash
一致,若一致则替换该节点的值为value
,不然进入下一步putTreeVal()
方法遍历红黑树,不然遍历链表。key
和hash
相同的节点就替换对应节点的值value
,若不存在则插入新的树节点。key
和hash
相同的节点就替换对应节点的值为value
。若找不到key
和hash
相同的节点,则链表尾部插入节点,同时进入下一步。TREEIFY_THRESHOLD(8)
时,则将链表转化为红黑树。除了HashMap
的put()
方法外,get()
方法也是一个咱们经常使用的方法,下面开始分析其关键的源码。rem
public V get(Object key) { if (key == null)// key为null时特殊处理 return getForNullKey(); // 关键获取key对应value的代码 Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
get()方法
的关键点以下:字符串
key
为null
,则调用getForNullKey()
方法获取value
,不然进入下一步getEntry()
方法获取对应的Entry
对象Entry
对象为null
时返回null,不然调用getValue()
返回其value
private V getForNullKey() { // 命中散列表索引为0,无需计算key的hash值 // 遍历命中的链表 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
final Entry<K,V> getEntry(Object key) { // 计算key的hash值,key为null时返回0 int hash = (key == null) ? 0 : hash(key); // 遍历命中的链表 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } // 链表不存在或链表中不存在key和hash一致的节点 return null; }
/** * 返回key对应的value,若是不存在则返回null */ public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
get()
方法其实是
hash()
方法获取到key
的hash
值getNode()
方法经过key
和hash
获取对应的value
。不存在则返回null
核心方法是getNode()
方法,下面我会先分析一下getNode()
方法。
/** * Map.get()方法的实际实现 * @param hash key的哈希值 * @param key 查询用的key * @return 节点或者是节点不存在是返回null */ final Node<K,V> getNode(int hash, Object key) { //tab用于暂存散列表table。first为散列表中对应索引的链表的头节点的指针。n存储tab的长度。i则为命中的散列表的索引 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //初始化方法内的变量,同时尝试命中散列表 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))// 老是先检查链表的头节点 return first;//头节点符合直接返回头节点 if ((e = first.next) != null) {//是否只有一个节点 if (first instanceof TreeNode)//判断头节点是否为红黑树节点 return ((TreeNode<K,V>)first).getTreeNode(hash, key);//改成遍历红黑树 do {//遍历链表是否有符合的节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //不存在对应的key,返回null return null; }
getNode()
方法的关键点:
table
不为null
且长度
大于0
且其索引为(n - 1) & hash
(等价于hash%n
)的节点不为null
。其中n
为散列表长度,hash
为插入的键值对的key
的哈希值。则进入下一步,不然直接返回null
key
和hash
是否与入参一致,若相同则返回首节点,不然进入下一步。1
个,如果则返回null
,不然进入下一步key
和hash
与入参相同的节点,若找到则返回该节点,不然返回null
put()
和get()
方法是HashMap
的经常使用方法,经过学习其源码了解到HashMap
是如何使用拉链法解决哈希冲突。而下面将会经过两幅图展现put()
和get()
的执行过程:
put()
方法图解get()
方法图解put()
方法图解get()
方法图解既然分析了Java 1.7
和Java 1.8
中HashMap
的put()
和get()
方法,固然少不了对两者的比较:
Java 1.7
的HashMap
中存在不少重复的代码。例如putForNullKey()
和put()
方法中重复的链表遍历,大量重复的hash
值计算逻辑等等。而在Java 1.8
中则对这部分的代码进行了重构。例如将putForNullKey()
和put()
方法重复的代码整合成putVal()
方法,hash()
方法处理key
为null
时的状况。Java 1.8
中的put()
方法会在链表超过树化阈值的时候,将链表转化为红黑树。而Java 1.7
中则只有链表Java 1.7
的链表节点插入为头插法(不须要判断链表是否存在),而Java 1.8
的链表节点插入则为尾插法。Java 1.8
增长了对putIfAbsent()
方法(存在才进行更新)的支持,详情能够看putVal()
中关于onlyIfAbsent
参数的处理逻辑。