咱们知道HashMap容许插入元素的key值为null,咱们看下这部分的源代码:java
private V putForNullKey(V value) { 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; }
能够看出,key=null时,对应的数据是保存在内部数组第一个位置的链表中。知道了它是如何保存的,那么获取也就简单了:编译内部数组第一个位置的列表,找到key=null的数据项,返回该数据项中的value便可。算法
private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
在上一篇博文:《基础知识-HashMap》原理剖析中能够知道,对于key必须实现其hashCode和equals方法,缺一不可。可是你们知道,这两个方法默认都是从Object对象中继承来的,下面看下Object的原生的实现方式:数组
public native int hashCode(); public boolean equals(Object obj) { return (this == obj); }
能够看到,hashCode方法使用了native关键字,表示其实现调用C/C++底层的函数来实现的,而equals方法则认为只有两个对象的引用指向同一个对象时,才认为它们是相等的。并发
若是你自定义了一个类,且没有从新覆写equals方法和hashCode方法,而你又使用该类的对象做为key值保存到HashMap,那么在读取HashMap的时候,除非你使用一个与你保存时引用彻底相同的对象做为key值,不然你再也得不到该key所对应的value。ide
这里给出良好hashCode和equals的实现例子:函数
public final class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } @Override public boolean equals(Object o) { if (o == null) return false; if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; } @Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; } }
下面给出hashCode的实现建议:
性能
一、把某个非零的常数值,好比17,保存在一个名为result的int类型的变量中。
二、对于对象中每一个关键域f(指equals方法中涉及的每一个域),完成如下步骤:
a、为该域计算int类型的散列码c:
i、若是该域是boolean类型,则计算(f?1:0)。
ii、若是该域是byte,char,short或者int类型,则计算(int)f。
iii、若是该域是long类型,则计算(int)(f^(f>>>32))。
iv、若是该域是float类型,则计算Float.floatToIntBits(f)。
v、若是该域是double类型,则计算Double.doubleToLongBits(f),而后按照步骤2.a.iii,为获得的long类型值计算散列值。
vi、若是该域是一个对象引用,而且该类的equals方法经过递归地调用equals的方式来比较这个域,则一样为这个域递归地调用hashCode。若是须要更复杂的比较,则为这个域计算一个范式(canonical representation),而后针对这个范式调用hashCode。若是这个域的值为null,则返回0(其余常数也行)。
vii、若是该域是一个数组,则要把每个元素当作单独的域来处理。也就是说,递归地应用上述规则,对每一个重要的元素计算一个散列码,而后根据步骤2.b中的作法把这些散列值组合起来。若是数组域中的每一个元素都很重要,能够利用发行版本1.5中增长的其中一个Arrays.hashCode方法。
b、按照下面的公式,把步骤2.a中计算获得的散列码c合并到result中:result = 31 * result + c; //此处31是个奇素数,而且有个很好的特性,即用移位和减法来代替乘法,能够获得更好的性能:31*i == (i<<5) - i,现代JVM能自动完成此优化。
三、返回result
四、检验并测试该hashCode实现是否符合通用约定。
注意:在计算过程当中,冗余项要排除在外。必须排除能够经过其余域值计算出来或equals比较计算中没用的的任何域,不然有可能违反hashCode第二条约定。测试
HashMap内部维护了一个实例变量modCount,该变量被声明为volatile,被volatile声明的变量表示任何线程均可以看到该变量被其余线程修改的结果。当使用迭代器(Iterator)进行迭代时,会将modCount的值赋给expectedModCount,在迭代过程当中,经过每次比较二者是否相等来判断HashMap是否在内部或者被其余线程修改。而HashMap中不少方法都会改变ModCount,如:put,remove,clear。
优化
先看下HashMap内部迭代器的实现:this
private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } public final boolean hasNext() { return next != null; } final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry<K,V> e = next; if (e == null) throw new NoSuchElementException(); if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } current = e; return e; } //... }
从上面的实现能够看出,HashMap所采用的Fast-Fail机制本质上是一种乐观锁机制,经过检查modCount状态,没有问题则忽略,有问题则抛出异常的方式,来避免线程同步的开销。当咱们在迭代的过程当中,修改了HashMap内部的元素,致使modCount的值改变,代码就会抛出java.util.ConcurrentModificationException。有意思的是若是HashMap只有一个元素的时候, ConcurrentModificationException 异常并不会被抛出。须要注意的就是:注意,迭代器的快速失败行为不能获得保证,通常来讲,存在非同步的并发修改时,不可能做出任何坚定的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。所以,编写依赖于此异常的程序的作法是错误的,正确作法是:迭代器的快速失败行为应该仅用于检测程序错误。
当调用默认的构造函数时:
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; //数组的影子 init(); }
table.length=16。
当指定初始容量和加载因子时,源码以下:
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); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; //经过上面这个算法,找到最接近initialCapacity,且又知足2的整数次方 this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; //发现实际容量为capacity, 并不是参数initialCapacity init(); }
由此看出,构造函数中指定的initialCapacity并不必定是HashMap内部维护数组的初始大小,而永远都是2的N次方。