key值为null时的存取
我们知道HashMap允许插入元素的key值为null,我们看下这部分的源代码:
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;
}
hashCode和equals方法
在上一篇博文:《基础知识-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。
这里给出良好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的实现建议:
1、把某个非零的常数值,比如17,保存在一个名为result的int类型的变量中。
2、对于对象中每个关键域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能自动完成此优化。
3、返回result
4、检验并测试该hashCode实现是否符合通用约定。
注意:在计算过程中,冗余项要排除在外。必须排除可以通过其他域值计算出来或equals比较计算中没用的的任何域,否则有可能违反hashCode第二条约定。
Fast-Fail机制
HashMap内部维护了一个实例变量modCount,该变量被声明为volatile,被volatile声明的变量表示任何线程都可以看到该变量被其他线程修改的结果。当使用迭代器(Iterator)进行迭代时,会将modCount的值赋给expectedModCount,在迭代过程中,通过每次比较两者是否相等来判断HashMap是否在内部或者被其他线程修改。而HashMap中很多方法都会改变ModCount,如:put,remove,clear。
先看下HashMap内部迭代器的实现:
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。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
HashMap内部数组的容量
当调用默认的构造函数时:
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次方。
来源:oschina
链接:https://my.oschina.net/u/991868/blog/374221