HashMap是基于哈希表的Map接口的实现,它对数组以及链表做了综合考虑。在看Handler源码的时候看到需要了解这方面的知识,于是乎就了解下顺便写个博客加深理解。本文只对JDK7的HashMap源码进行分析,后续版本的红黑树先不考虑。
数组:采用一段连续的存储单元来存储数据。他的主要特点是:查找速度快,插入和删除效率低,内存空间要求高,必须有足够的连续内存空间。
链表:插入删除速度快,内存利用率高。
Hash:翻译成中文是“散列”的意思。把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。
Hash冲突:Key键值经过行哈希运算得到一个存储地址,发现已经被其他的元素所占据。这就是所谓的Hash冲突。
纵向是一个数组,数组的每一项都是一个链表。数组相当于蓝牙电话列表的首字母,而链表相当于对应首字母的一组电话号码。当然这边的首字母是举得一个例子而已,HashMap中对应的是key键值经过一定运算得出来的结果。这个结果既要保证数组不能太长以免造成空间的浪费,又要保证链表不能太长造成时间的浪费。
OK!在介绍源码之前先看下几个重要的变量:
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
// Android-Note: We always use a load factor of 0.75 and ignore any explicitly
// selected values.
final float loadFactor = DEFAULT_LOAD_FACTOR;
size代表的是HashMap中size。
threshold为当前的阈值,初始化的时候如果没人设置那么就是4。
loadFactor为负载因子,代表了table的填充度有多少,默认是0.75。
2、构造器
接下来就是分析源码了,对于HashMap可以从get、put以及构造这三方面入手。那么先看下构造的代码:
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Android-Note: We always use the default load factor of 0.75f.
// This might appear wrong but it's just awkward design. We always call
// inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
// to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
// the load factor).
threshold = initialCapacity;
init();
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
大概的意思是如果没有参数那么默认table的数组大小为4,加载因子为0.75。如果有参数,那么赋值为传进来的参数。
3、put方法介绍
下面介绍下put方法,贴上代码:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);//标注1
}
if (key == null)
return putForNullKey(value);//标注2
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);//标注3
int i = indexFor(hash, table.length);//标注4
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {//标注5
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//标注8
addEntry(hash, key, value, i);//标注7
return null;
}
先看下标注1。当table为空的时候会调用inflateTable方法:
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
// Android-changed: Replace usage of Math.min() here because this method is
// called from the <clinit> of runtime, at which point the native libraries
// needed by Float.* might not be loaded.
float thresholdFloat = capacity * loadFactor;
if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
thresholdFloat = MAXIMUM_CAPACITY + 1;
}
threshold = (int) thresholdFloat;
table = new HashMapEntry[capacity];
}
首先他是计算获取容量,容量的大小必须是2的n次方且大于toSize的数值,然后根据capacity * loadFactor得出阈值,也就是说当元素个数超过容量loadFactor倍的时候才进行扩容。最后是分配容量大小的内存给table。OK,那么table的初始化完成了。
此处有两个要重点理解的:1、为什么容量一定要是2的n次方。2、HashMapEntry结构包含了那些元素以及作用。
第一个问题接下来分析到indexFor的时候会解答。那么看下HashMapEntry的变量:
final K key;
V value;
HashMapEntry<K,V> next;
int hash;
key和value就不用多说了,这个是键值对的基本参数。next用于建立链表,而hash用于存储获取到的hash值。
OK,看下put方法的标注2,如果键值为空的情况下会调用putForNullKey方法。贴上putForNullKey方法代码:
private V putForNullKey(V value) {
for (HashMapEntry<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;
}
逻辑是:遍历table[0]的链表,如果存在key为null的Entry那么替换成新值。如果没找到,那么在table[0]位置添加该值(该操作由addEntry方法完成,后续介绍)。
回头再看下put方法的标注3,他的功能是对hashcode进行二次哈希计算,目前只知道他的目的是为了使哈希值分布的更加均匀,具体怎么计算的Mark一下有时间看看。
put方法的标注4是获取hash值低位的索引号,先看看代码。
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
我的理解是:1、比如说table有length的长度,比如说是16也就是0~15。OK,那么有12个元素的hash值,那么如何均匀地将它分布在这些数组呢。HashMap应该就是先通过二次哈希计算使得这12个hash值从低位开始尽量地均匀分布,也就是通过与运算能够让这12个值尽量的分散在table上。2、该方法也正回答了前面的疑问,长度如果是2的n次方,那么对于indexFor的与运算更加的友好。
put方法的标注5,他是循环遍历table数组中获取到的索引处的链表,如果找出key相等的键值对那么替换成新的值返回旧的值。
下面看下如何判断key相等首先是e.hash == hash,因为hash值不相等的话key一定不相等所以首先判断下这个必要不充分条件,第二步才是判断((k = e.key) == key || key.equals(k))。由于equals判断比hash更耗时间,所以这样子更能提高效率。
put方法的标注6,modCount是记录修改次数,他与线程安全有关。在后续的fail-fast策略会提到这个。
put方法的标注7,addEntry(hash, key, value, i)。也就是当在对应的链表中找不到的相同key的时候用来增加一个新的Entry。先看看源码:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
首先是判断如果元素的数目大于阈值的时候,扩容成原来长度的两倍并重新计算哈希值以及Index。这边有个疑问:为什么要重新计算哈希值,哈希值难道和低位位数有关?Mark一下。接下来就是重新创建Entry了。总的来说就是如果满足条件就先扩容,然后再创建键值对。
这里有两个地方需要理解:第一是resize方法,第二个是createEntry方法。
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
resize也就是根据新的长度创建newTable。具体方法在transfer中实现,遍历所有的元素重新计算index放入对应新table的桶中。createEntry即增加一个新的Entry。这些操作都是从桶的头部开始插入!put方法介绍完毕。
4、get方法介绍
下面看下get方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
关键是getEntry方法,跟进去看看:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
for (HashMapEntry<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))))//标注1
return e;
}
return null;
}
这边先通过key的哈希值获取index,然后只需在对应的桶中遍历寻找相同的值即可。标注1可以看到必须同时满足hash值相同以及key相同才能返回e。第一个hash的条件即可屏蔽很多键值对,而相对于只用equal来判断这样子高效了很多。同时有个疑问:tabel数组的大小永远会小于元素大小,那么链表不是很难产生,那和链表的特性不是体现不出来?从get这个方法看出,或许HashMap可能重点是在判断hash上面这样子比直接equal效率高多了。
5、fail-fast(快速失败)机制
fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代的过程中该就有可能会发生fail-fast抛出ConcurrentModificationException异常。前面提到modCount变量,注释可知该变量用于迭代时触发fail-fast机制。
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
现在看下HashMap抛出异常的代码:
private abstract class HashIterator<E> implements Iterator<E> {
...
HashIterator() {
expectedModCount = modCount;
...
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
...
}
...
}
在HashIterator方法在构造的时候赋值一下modCount,然后调用nextEntry的时候判断是否在迭代的过程中被修改了。如果被修改了,那么就报异常。分为两种情况:
1、单线程环境下
while(iterator.hasNext()) {
if (i == 3) list.remove(3);
System.out.println(iterator.next());
i ++;
}
在遍历的过程中删除一个元素,那么就会报出这个异常。
2、多线程条件下,由于HashMap不是线程安全的。所以比如A线程正在迭代的过程中,B线程修改了modCount值。那么就会报异常。至于modCount修饰符从volatile 变为transient不是很清楚。Mark一下!
1、与其他集合的差异性可以总结一下
2、Hash表的底层算法原理可以去了解