Java里各种基础容器的实现都是链表外加引用的形式。So,hashmap也不例外。
那小考虑下:hashmap是怎么产生的呢?
常用的两种数据结构数组和链表各有优劣,数组寻址容易,插入和删除困难;而链表寻址困难,插入删除容易,那我们能不能综合两者的特性呢?那就是哈希表啦。hashmap是java中对于哈希表的无锁实现。
首先,说句题外话:Hashtable是HashMap的难弟儿,hashtable是hashmap的线程安全版本,它的实现和HashMap实现基本一致,除了它不能包含null值的key和value,并且它在计算hash值和数组索引值的方式要稍微简单一些。对于线程安全的实现,Hashtable简单的将所有操作都标记成synchronized,即对当前实例的锁,这样容易引起一些性能问题,所以目前一般使用性能更好的ConcurrentHashMap。另外,对于HashMap是可以解决同步问题的,通过调用Map Collections.synchronizedMap(Map m),当然与可以自己在使用地方加锁。
下面进入正题:
哈希表有多种实现方法,java中实现采用的是拉链法,我们可以当成“链表的数组”,如下图所示:
先创建一个数组,在数组中存放是是链表的头节点。Java的HashMap里面实现了一个静态内部类Entry,其重要的属性有Key Value Entry(next)。而数组中保存的就是Entry。
我们先来看hashmap构造函数的代码。如下:
this.loadFactor = DEFAULT_LOAD_FACTOR; //装填因子,扩容的时候使用 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); //默认的容量阈值 table = new Entry[DEFAULT_INITIAL_CAPACITY];//构造一个以Entry为对象的数组 init(); //空方法
由以上代码可以看出,初始化hashmap的过程只是初始化了一个Entry类型数组,Entry就是刚才说的Java实现的内部类,我们来看下Entry的代码?
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; ....... public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); }/** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } }
可以看出Entry是一个链表一样结构,其中有next域用于指向下一个Entry。
既然是Entry数组,那为什么能线性插入删除呢?那么我们看一下往hashmap里put之后都发生了什么呢?来看代码。
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key); // 对key的哈希在做一个一些移位异或的操作,目的是防止一些鸡肋的key的哈希函数 int i = indexFor(hash, table.length);//返回hash对应的数组的下标。最简单的实现肯定是用hash%length,但这里不是这样,这里的处理很巧妙,下面还会再介绍。 //遍历链表 for (Entry<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++; //修改次数,用于fail-fast策略 addEntry(hash, key, value, i); //添加entry到对应的数组中,函数见下 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//如果大小超过阈值的话,resize阈值。resize的过程原存在的Entry会重新计算索引值,并且Entry链的顺序也会发生颠倒(如果还在同一个链中的话);而该新添加的Entry的索引值也会重新计算。,消耗还是蛮大的,所以如果知道hashmap大小的话,最好还是给个初始值 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e);//插入到头部,e表示Entry.next size++; } } return null; }
上文说的利用key的二次hash值求对应的下标的时候,最常规的想法就是用hash%length,但是在jdk里,没这么实现。而是
static int indexFor(int h, int length) { return h & (length-1); }
这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n 次方(可以看初始化的代码,扩容的时候也是扩容到2的那次方)。这看上去很简单,其实比较有玄机的,而当数组长度为16时,即为2的n次方时,2的n次方得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了复杂的异或和位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。这样效率提高了,而且冲突的可能性也减少了。
知道put操作作了什么,get的话就不难了。
就先写到这。