大家在求职应聘java开发岗时,想必会经常被面试官问到HashMap是怎么实现的问题,本文
通过jdk源码来简析HashMap的实现机制。
首先,HashMap继承了AbstractMap,并实现了Map、Cloneable和Serializable接口,这里不
作阐述。在Eclipse里查看源码,可得如下类结构截图。
首先来看成员变量,注意到有一个Entry类型的数组table,这个其实就是真正用来存储我们
的数据的地方,其中Entry是HashMap的一个静态内部类,它包含一个next引用(源代码如
下),其实它就是一个简单的链表结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
到这里,我们其实可以知道HashMap是怎么回事了,它底层是利用数组(还记得那个table么?)实现,数组每一个元素本身相当于一
个链表,我们的数据就存在这些个链表的域中!我们知道数组存取速度快,链表增删方便,HashMap是在存取和增删这两方面取了一
个折中!结构图如下
是不是很简单?
我们再看前三个final变量,根据名字就知道,分别是HashMap的默认初始容量、最大容量
、默认负载因子。不错,HashMap也是有最大容量的(2的30次方),当然平时我们都没
机会让它爆掉。为了节省空间,HashMap一开始只是申请一个相对较小的空间(一般就是
默认的容量数值这里是2的16次方)。对于负载因子,这里有必要提一下,它和HashMap
的容量有关,比如负载因子为0.75,表示12单位的空间只存储12*0.75=8个单位,这个数
字8其实也就是threshold的意义!而size变量表示真个map存储的键值对的个数,在后文
对put函数讲解得时候会说到HashMap的扩容方式,它的判断标准也是根据这个将size和
threshold变量进行大小比较。
再看它的构造函数,其中有四个构造函数,我们平时用的最多是第三个无参构造函数,
其源码如下:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
这里做一些基本的初始化工作,注意threshold等于什么!这在之前已经说过。另外,
init()是个空方法,这里不太清楚原因,有知道的网友,多多指教啊!其它三个构
造函数其实最终都是调用调用第一个构造函数,只是使用传入的参数进行初始化。
接下来,看看两个最具代表性的方法:put、get
对于添加键值对对象的put方法:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> 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;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
该方法,首先判断key是否为null,如果null则调用putForNullKey方法,查看源码发现putForNullKey方法默认
将Null对应的value放在table[0]位置所在的entry链表上(因为有的key会映射到该
位置),所以HashMap是支持null为key的!然后,通过就是计算key对应的hash
值,hash和indexFor函数如下:
static int hash(int h) {
// 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);
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
没啥特别要说的,大底知道是通过无符号位移操作再与table的长度进行与操作,且不提。在put函数的最后有一行
addEntry(hash, key, value, i);
也就是添加元素到对应的链表中,其源码为:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
看到其中的if 语句了么? 当元素越来越多时,hashmap的扩容方式为扩大一倍!
再接着,通过k的hashcode和k值判断是否冲突!如果冲突则将
value覆盖,并返回覆盖之前的value,当然,没有冲突和发生冲突的key值为
null时都返回null (0.0, 别晕)
接下来,看看get方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
同put一样,先判断key值是否为null。 然后通过计算hashcode形成对table的一个映射,找到所在的entry链表,
然后就是对该链表进行简单的遍历,蛮简单,不多提了!
最后提一下,table数组是声明为transient的,这可以防止HashMap被序列化。
另外,HashMap还有很多内部iterator接口的实现类,其主要是对HashMap
提供不需要了解内部结构就可以进行访问的目的,这个可以参考iterator设
计模式 。