HashMap与Hashtable的源码浅析
学习一门技术就要把它学通,学的深入一点,Java中的集合类源码解析是面试中经常会问到的问题,所以今天就来带大家一起解析下JDK源码。如若发现任何不妥的地方,欢迎大家fadeback。
HashMap源码解析
首先看下JDK中是如何定义HashMap的:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
它是继承自AbstractMap<K, V>
这个抽象类,实现了Map<K,V>
、Cloneable
、Serializable
这三个接口。值得注意的是AbstractMap<K, V>
也实现了Map<K, V>
接口。
HashMap存储键值对的成员变量——table
HashMap内声明了一个成员变量用于存储部分键值对,它是一个数组,源码如下:
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
table被transient关键字修饰,也就是说table里面存储的数据不会被序列化,它保证了用户敏感信息在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中不会被传输。
注意:上面之所以说存储部分Node是因为它只会存储Key的Hash码首次在该HashMap中出现的Node,其他的将以链表的形式存储在next上。如下。。
HashMap中的静态内部类——Node
Node是HashMap用来存储键值对的最小粒度,是一个静态内部类,它类似于链表,实现了Map接口的内部接口Entry<K, V>
,关键代码如下:
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Node的next存储了与当前Key的hash码相同的下一个Node对象。也就是说当使用HashMap存储一个键值对时,它会首先检查这个Key的hash值在table数组对应的位置上是否为null,若为null则直接赋值,若不为null,则会依次检验next对象,直到next为null后,然后给next赋值。
那么HashMap是怎么存储一个新的键值对的呢?
我们从HashMap的put方法看起(注释略):
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
它调用了一个putVal方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //若table为空,调用resize()方法初始化table数组。
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //当前key的hash对应的位置为空,直接赋值
tab[i] = newNode(hash, key, value, null);
else { //否则获取当前Node对象,检验并赋值
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
源码中本来没有这些注释,但是为了方便理解,我直接在源码里添加了注释,只是大致的叙述了下,细节地方还需要读者细细品味。不过个人觉得理解了大致是怎么回事就够了,没有必要深究哈。
HashMap小总结
HashMap就研究到这里啦,总的来说,HashMap是采用数组加链表的方式存储数据的,通过key对应的hash值来快速定位到指定的数组位置上,每一个hash值对应的实体类都可以看做是一个链表的起始位置(不同于表头,因为它是有数据的),通过这个实体类可以找到其他以此hash值为key的实体类。
值得注意的地方是:当使用HashMap存储数据时,最好不要使用一些连续的数字去作为key,最好使用字符串,因为连续的数字的hash值都是相同的,这样会导致数据都存储在一个hash值上,影响效率。另外HashMap是非线程安全的,当发布HashMap时要特别小心。
Hashtable源码分析
首先,看一下JDK中是如何定义Hashtable的:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
它的继承方式与HashMap一致。
其次,在Hashtable中也找到了内部类Node<K, V>
/**
* Hashtable bucket collision list entry
*/
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// Map.Entry Ops
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
它的实现方式跟HashMap.Node基本上是一致的,只是方法放置的上下顺序不同,有的个别的方法代码略有不同,但是结果是相同的。如:
//HashMap.Node的toString方法
public final String toString() { return key + "=" + value; }
//Hashtable.Node的toString方法
public String toString() {
return key.toString()+"="+value.toString();
}
虽然功能是一样的,但是为什么不直接复制呢?哈哈,也许有我不理解的一面吧。
在Hashtable中也找到了table数组,如下:
/**
* The hash table data.
*/
private transient Entry<?,?>[] table;
看到这里我们就能明白,Hashtable的实现也是数组加链表的实现的,不同的是Hashtable是线程安全类,而HashMap则是非线程安全的。
而且Hashtable与HashMap中的其他方法上下顺序也是不同的(有些方法实现也是不同的),从这里可以看出从HashMap到Hashtable的实现并不是直接复制然后给方法加锁。
我们再来看几个具体需要加锁的方法实现:
首先是put方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
可以看到它是直接在定义方法时直接synchronized ,这样加锁性能会大大的降低,在方法内部没有任何的优化,也就是说只要是调用put方法就会加锁。
我们再来看看其它方法:
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
remove方法也是直接在定义的时候synchronized。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
get方法也是一样,Hashtable并没有想象中的对锁进行相应优化,比如缩小锁的范围,使用CAS等。
Hashtable小总结
Hashtable对加锁的优化很粗糙,只是单纯的在外部使用synchronized,所以我们在并发较大的情况下尽量不要使用Hashtable,这将会造成严重的性能问题。另外提醒大家注意下Java中的Hashtable中table的首字母是不大写的,粗心的同学要稍微注意下。稍后我会带来ConcurrentHashMap的源码浅析,希望大家多多关注。