通过Debug 探寻Java-HashMap 实现原理:
一个简单的例子,代码如下,
测试方法 main:
1 public static void main(String[] args) { 2 3 KeyObj obj1 = new KeyObj("AAAA"); 4 KeyObj obj2 = new KeyObj("BBBB"); 5 KeyObj obj3 = new KeyObj("CCCCC"); 6 KeyObj obj4 = new KeyObj("DDDDD"); 7 8 HashMap<KeyObj, String> hashMap = new HashMap<KeyObj, String>(); 9 hashMap.put(obj1, "aaaa"); 10 hashMap.put(obj2, "bbbb"); 11 hashMap.put(obj3, "ccccc"); 12 hashMap.put(obj4, "ddddd"); 13 14 System.out.println(hashMap.values()); 15 16 }
KeyObj.java
1 public class KeyObj { 2 3 String keyStr; 4 5 public KeyObj(String keyStr) { 6 super(); 7 this.keyStr = keyStr; 8 } 9 10 public String getKeyStr() { 11 return keyStr; 12 } 13 14 public void setKeyStr(String keyStr) { 15 this.keyStr = keyStr; 16 } 17 18 @Override
// 覆盖hashCode方法,使得key的单双数分别获得一致的hash值,方便测试 19 public int hashCode() { 20 if (keyStr.length() % 2 == 0) { 21 return 31; 22 } 23 return 95; 24 } 25 26 @Override 27 public boolean equals(Object obj) { 28 KeyObj obj1 = (KeyObj) obj; 29 if (this.keyStr.equalsIgnoreCase(obj1.keyStr)) 30 return true; 31 return false; 32 } 33 34 }
运行测试方法main,Debug查看HashMap:
1.可以看到HashMap其实是有一个名称为table的Entry数组,我们使用HashMap的put方法,本质是把我们的Key-Value作为Entry对象放入到HashMap中。
2.HashMap的table数组初始大小为16.
3.为何我们put了4个对象却只使用了table[10] 与 table[14]?
查看put方法JDK代码:
1 public V put(K key, V value) { 2 if (table == EMPTY_TABLE) { 3 inflateTable(threshold); // 3.1 table若为空创建table,16大小 4 } 5 if (key == null) 6 return putForNullKey(value); // 3.2 key若为空放入table[0] 7 int hash = hash(key); // 3.3 计算放入key的hash,值为调用KeyObj的hashcode方法,再hash 8 int i = indexFor(hash, table.length);
// 3.4 计算当前put的Enrty在table数组中精确位置(int i),跟踪代码可以很容易看出:
// 3.4.1 精确位置i是由key的hash值与table.length取模 9 for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 3.5 遍历table[i]的Entry 10 Object k; 11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 3.6 若put相同key的KeyObj,则替换旧值 12 V oldValue = e.value; 13 e.value = value; 14 e.recordAccess(this); 15 return oldValue; 16 } 17 } 18 19 modCount++; 20 addEntry(hash, key, value, i);// 3.7 创建Entry,当前table[i]的首节点变为当前put节点Entry对象的next节点,注:JDK8之前每一个table[i]上的Entry使用单链表存储的 21 return null; 22 }
理解了put,查看HashMap的get方法就很简单了:
1 public V get(Object key) { 2 if (key == null) 3 return getForNullKey(); 4 Entry<K,V> entry = getEntry(key); 5 6 return null == entry ? null : entry.getValue(); 7 }
继续查看第四行getEntry方法:
1 final Entry<K,V> getEntry(Object key) { 2 if (size == 0) { 3 return null; 4 } 5 6 int hash = (key == null) ? 0 : hash(key); 7 for (Entry<K,V> e = table[indexFor(hash, table.length)]; // 找到table[i],并便利该位置节点 8 e != null; 9 e = e.next) { 10 Object k; 11 if (e.hash == hash && 12 ((k = e.key) == key || (key != null && key.equals(k)))) // 找到,返回 13 return e; 14 } 15 return null; 16 }
其他:
在put函数代码中有一个注释:
JDK8之前每一个table[i]上的Entry使用单链表存储的
一直到JDK7为止,HashMap的结构都是这么简单,基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储。
这样子的HashMap性能上就抱有一定疑问,如果说成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那就不可避免的花费O(N)的查找时间,这将是多么大的性能损失。这个问题终于在JDK8中得到了解决。再最坏的情况下,链表查找的时间复杂度为O(n),而红黑树一直是O(logn),这样会提高HashMap的效率。
所以在JDK8中,当同一个hash值的节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。