本文基于JDK1.7分析
1.数据结构
数据结构中可以用数组和链表存储数据,它们各有利弊。
数组:数组存储区间是连续的,空间复杂度大。它的特点是寻址容易,插入删除困难。
链表:链表存储区间离散,空间复杂度小。它的特点是寻址困难,插入删除容易。
哈希表:根据关键码值(Key value)而直接进行访问的数据结构。它的主干是数组,既满足了数据查找,同时也不会占用很多空间。
哈希冲突:当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,这就是哈希冲突。哈希冲突的解决方案有很多种:开放地址法、链地址法。HashMap所采用的处理方法就是链地址法。
链地址法:采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址。可以看下图:
链地址法.jpg2.实现原理
HashMap的内部是一个HashMapEntry的 table数组,HashMapEntry是HashMap的基本组成单元,每一个HashMapEntry包含一个key-value键值对。
// 初始值为一个空数组
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
transient 关键字的作用:被它修饰的变量不再能被序列化。
再看下HashMapEntry这个类,是HashMap的静态内部类。
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
// 存储指向下一个HashMapEntry的引用,单链表结构
HashMapEntry<K,V> next;
// 对key进行hash运算后的结果
int hash;
/**
* Creates new entry.
*/
HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
...
}
所以,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是为了解决哈希冲突。
HashMap的构造方法有4个,看一下主要的一个
/**
* 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) {
// 校验初始容量,控制大小在4~MAXIMUM_CAPACITY = 1 << 30之间
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();
}
再来看下存数据的过程
public V put(K key, V value) {
// 如果是空数组,创建一个一定大小的数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 正对key为null单独处理,存储到数组的第0个位置
if (key == null)
return putForNullKey(value);
// 对key进行hash运算
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
// 获取在数组中的位置
int i = indexFor(hash, table.length);
// 遍历链表
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// key相同则覆盖,返回了之前的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 新增一个HashMapEntry
addEntry(hash, key, value, i);
return null;
}
先看下inflateTable方法
private void inflateTable(int toSize) {
// 得到一个大于阈值threshold的2的次方数的容量
// 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.
// 控制thresholdFloat 最大值,为容量*负载因子与最大容量+1的最小值
float thresholdFloat = capacity * loadFactor;
if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
thresholdFloat = MAXIMUM_CAPACITY + 1;
}
// 为threshold赋值
threshold = (int) thresholdFloat;
table = new HashMapEntry[capacity];
}
再看putForNullKey这个方法
private V putForNullKey(V value) {
// 遍历了数组0位置的HashEntry
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;
}
再看indexFor方法,这里因为length是2的倍数,所以h & (length-1) = h%length,那么用位运算计算效率会高一些
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);
}
再来看addEntry方法
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);
}
继续看resize方法,它将HashMap的容量扩大为两倍
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;
}
}
}
这里有一个疑问,为什么HashMap的初始容量以及后续的扩容都是2的n次方?
我们来一个计算:
假设数组长度是15,计算过的hash值分别是8和9
h & (length-1) h (length-1)
8&(15-1) 0100 & 1110 = 0100
9&(15-1) 0101 & 1110 = 0100
8和9计算出来的结果相同,所以会定位到数组的同一个位置上去,产生哈希碰撞,8和9会被放到数组的同一个位置上形成链表,查询的时候会遍历链表,降低效率。而且当长度是15时候,&运算后,最后一位永远是0,所以0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
再来计算下数组长度是16的
h & (length-1) h (length-1)
8&(15-1) 0100 & 1111 = 0100
9&(15-1) 0101 & 1111 = 0101
当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
再来看下取数据的过程
public V get(Object key) {
// 针对key为null值,到数组的第0个位置取值
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
再看下getEntry方法,得到key对应的Entry
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
// 计算hash值
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
// 得到数组下标,遍历链表,找到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))))
return e;
}
return null;
}
网友评论