前言
Map 是非常常用的一种数据接口。在 Java 中,提供了成熟的 Map 实现。

图 1
最主要的实现类有 Hashtable、HashMap、LinkedHashMap和 TreeMap。在 HashTable 的子类中,还有 Properties的实现。Properties 是专门读取配置文件的类,我们会在稍后介绍。这里首先值得关注的是 HashMap 和 HashTable 两套不同的实现,两者都实现了 Map 接口。从表面上看,并没有多大差别,但是在内部实现上却有些微小的细节。
首先,HashTable 的大部分方法都做了同步,而 HashMap 没有,因此, HashMap 不是线程安全的。
其次,HashTable 不允许 key 或者 value 使用 null 值,而 HashMap 可以。
第三,在内部实现算法上,它们对 key 的 hash 算法和 hash 值到内存索引的映射算法不同。
虽然有诸多不同,但是他们的性能确实相差无几。由于 HashMap 使用广泛性,现以 HashMap 为例,阐述它的实现机理。
?
1、HashMap 的实现原理
HashMap 内部维护一个数组,并且将 key 做 hash 算法,然后将 hash 值映射到内存地址,即数组的下标索引,这样就可以通过key直接取到所对应的数据。而对于发生碰撞的位置,则会维护一个链表,所有在同一位置发生碰撞的元素都会存放在同一位置的链表中。

图 2
如图 2,数组中的每一个元素都是一个 Entry 实例:
?
static class Entry
implements Map.Entry
{ final K key; V value; Entry
next; int hash; //.....省略部分 }
每一个实例都包含 元素key, 元素value , 元素hash值,以及指向下一个在当前位置发生冲突的 Entry实例。
?
2、Put 方法详细解析
下面我们来看一下最基本的put 操作。
?
/*
* 将(key, value)放入 map
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
// 计算 key 对应的下标 。关于 hash 和 indexFor 方法,我们会在后面讲到。
int hash = hash(key);
int i = indexFor(hash, table.length);
// 如果发生了冲突,那么就遍历当前冲突位置的链表。如果在链表中发现该元素已经存在(即两元素的 key 和 hash
// 值一样),则用新值替换原来的值,并返回原来的值。
for (Entry
e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
// 将该元素的访问存入历史记录中(在LinkedHashMap才发挥作用)
e.recordAccess(this);
return oldValue;
}
}
// 标志容器被修改次数的计数器,在使用迭代器遍历时起作用
modCount++;
// 为新值创建一个新元素,并添加到数组中
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果数组需要扩容,则进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 创建新元素并添加到数组中
createEntry(hash, key, value, bucketIndex);
}
/*
* 创建新元素,并将该新元素加到下标位置的最前端,该新元素的next引用指向该位置原来的元素(如果有)
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry
e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
应经加上了详细的注释,相信大家都能读得懂。同样的,get 操作就比这个简单多了,笔者就不再?嗦了,下面直接将最关键的部分。
?
3、HashMap 的核心算法-hash 函数的实现
HashMap的高性能需要保证以下几点:
1、hash 算法必须是高效的
2、hash 值到内存地址(数组索引)的算法是快速的
3、根据内存地址(数组索引)可以直接取得对应的值
首先来看第一点,hash 算法的高效性,在 HashMap 中,put() 方法和 hash 算法有关代码如下:
?
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
//...........省略部分
}
?
?
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
} HashMap的功能是通过“键(key)”能够快速的找到“值”。下面我们分析下HashMap计算下标索引的思路:
1、 当调用put(key,value)时,首先获取key的hashcode,int hash = key.hashCode();
2、 再把hash通过一下运算得到一个int h。
hash ^= (hash >>> 20) ^ (hash >>> 12);
int h = hash ^ (hash >>> 7) ^ (hash >>> 4);
为什么要经过这样的运算呢?这就是HashMap的高明之处。先看个例子,一个十进制数32768(二进制