Java集合之HashMap的实现原理
HashMap是一个散列表,用来存储键值对的,在日常开发中用的很多,下面来学习一下它的内部实现原理
Java 8之前的实现原理:HashMap里面有个静态内部类:Entry,它是一个链表,主要变量有三个,key、value和next,分别代表HashMap的key、value和该链表下一个的地址。而HashMap里面有一个数组,数组元素就是Entry,所以HashMap保存数据的数据结构是一个链表数组。
当保存一个键值对时调用了put方法,源码如下:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
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是不是空,如果为空,则直接调用key是空的那个方法保存value,如果不是,则根据key的hashCode计算得到一个hash值,然后根据这个hash值和目前数组的长度得到一个值i,这个值就是要把这个键值对保存到数组的下标。得到这个下标之后就取出这个链表,然后对这个链表按顺序进行for循环拿出每一个节点,得到节点之后首先比较这个节点的hash值刚才计算的hash值是否一样,然后判断两者的key是否相等,key的相等则通过==和equals两个比较,==或者equals有一个返回true则代表key是相等的。当这两者都相等,则代表已经有一个key存在了,则把value替换成新的value就行,同时把旧的key返回回去;如果其中之一不同,则保存这个键值对到别的地方,即调用addEntry方法,代码如下:
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
保存键值对比较简单,先把该数组中i位置的Entry对象拿出来,然后创建一个链表对象,并且把刚才拿出来的Entry对象的地址赋值到刚创建的链表对象的next属性上,那么之前在该位置的对象则保存在第二个位置上了,最后判断当前数组的大小是否大于初始设置的一个数组大小,如果大于或者等于,则重新设置数组大小。
而get方法则是一个逆过程,代码如下:
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
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;
}
如果key是空,则从空key那里取value,如果不是,则根据key的hashCode计算一个hash值,然后根据这个hash值和目前数组的长度得到一个值,根据这个从数组中取出这个链表,然后遍历这个链表,根据每个节点的hash和key来跟计算得到的hash和当前传入的key比较,如果一直则就是这个节点,把value取出即可。如果没有找到,则返回null。
从以上的分析可以看出,当key的hash值一样的时候,则会出现一个冲突问题,而这个冲突则用链表解决了,如果当链表很长的时候,则会出现一个性能问题(链表增删快,查询慢,线程不安全,HashMap主要是查询多一点)。
以上是Java 8之前的一个实现原理,Java 8之后把HashMap的原理改了,里面有一部分是用红黑树实现的,目的是为了提高性能。所以它的实现原理结构是数组+链表+红黑树。
put方法代码如下:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
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;
}
Node和Entry是一样的,都是一个链表。首先判断这个数组是否为空,如果为空,则初始化这个数组,初始化之后则根据数组长度和hash找到链表,然后判断这个链表是否为空,为空则添加。如果不为空,则判断hash、key是否一样,一样则替换旧值(这里跟Java 8之前是一样的)。如果不为空,同时也没找到hash和key是一致的,则判断这个链表是否是红黑树,如果是,则根据红黑树的规则保存这个键值对,如果还不是,进行一个循环遍历这个链表,当链表最后一个节点为空时,则把键值对插入到这里,如果当这个链表的长度大于8的时候,则把这个链表转换成红黑树;如果key存在,则替换掉原来的value。完成一系列保存操作后,判断当前容量,如果超过了初始值,则扩容。其中注释了existing mapping for key是为LinkedHashMap保留的(这个看的不太明白,不知道是不是这样)。
现在来看一下它是怎么移除的,先看源码
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
/**
* Removes and returns the entry associated with the specified key
* in the HashMap. Returns null if the HashMap contains no mapping
* for this key.
*/
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
从源码上可以知道根据key去移除会调用removeEntryForKey方法,先是计算出key的hash值,然后根据hash值与数组的长度计算出在数组中的位置,取出这个链表,然后遍历这个链表(为空则跳出循环),只要key符合要求,则说明找到了这个节点,同时把这个节点的下一个节点赋值到它上一个节点的next属性中,用来代替自己,从而删除掉了这个节点。如果一开始就为空,则把null返回回去。
HashMap还可以直接移除一个Entry,不需要根据key去移除,源代码如下:
/**
* Special version of remove for EntrySet.
*/
final Entry<K,V> removeMapping(Object o) {
if (!(o instanceof Map.Entry))
return null;
Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
Object key = entry.getKey();
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
if (e.hash == hash && e.equals(entry)) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
从代码中可以看出,跟remove的代码基本一样,就是前面增加了一个判断,检查是否属于Map.Entry,这个就不多说了。
再来看包含的实现原理,代码如下:
/**
* Returns <tt>true</tt> if this map maps one or more keys to the
* specified value.
*
* @param value value whose presence in this map is to be tested
* @return <tt>true</tt> if this map maps one or more keys to the
* specified value
*/
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
包含的原理其实就是遍历,然后查找对应的value,如果查找到了则返回true,没有查找到则返回false。
以上就是HashMap几个重要方法的实现原理,有哪里不对的欢迎指正