1、hashmap简介
hashmap是一个key-value键值对形式的集合,用于存储键值对数据。默认容量为16,每次扩容是在原有容量基础上扩大一倍。jdk1.7时底层采用数组+链表形式存储,且因为头插法,在并发扩容时容易产生循环链表。jdk1.8时底层采用数组+链表+红黑树形式存储,且数据插入时采用尾插法,另外转变红黑树需要满足两个条件,一是链表长度大于等于8,二是数组容量达到64。
2、hashmap整体查看
先来看下HashMap在集合框架中的位置,可以看到HashMap继承自AbstractMap并实现了Map接口,且能序列化和克隆。所以HashMap肯定具备Map所定义的键值对存储能力,中间的AbstractMap抽象类定义了可公用的Map方法实现,使得继承自它的子类在实现Map功能时只需关注自定义的那部分即可。
表面来看hashmap功能可能比较简单,仅用于存储键值对数据集合,但是其内部实现比较复杂,这里先整体看一下内部的组成,便于我们查看源码前对其有个整体的认识:
通过上图可以看到,HashMap内部类大概由HashMapSpliterator、Node、KeySet、HashIterator、EntrySet、Values六部分组成,各部分功能如下:
HashMapSpliterator:是java1.8新增的接口,主要用于并行处理数据。这一块使用较少,所以不多做介绍,感兴趣的可以单独搜索查看。
Node:继承Entry接口,封装成链表节点,内部含有key、value、hash,且重写了hashcode与equals方法,TreeNode则是转变红黑树后的节点类型
KeySet、Values、EntrySet:key、value、entry的集合,表面是集合,其实并没有单独存储一份数据,底层还是hashmap那一套数据,只不过重写了set集合的相关方法。
HashIterator:其并没有实现Iterator接口,但是却编辑了Iterator中除next方法外的所有的同名方法。然后KeyIterator、ValueIterator、EntryIterator继承HashIterator和实现Iterator接口,由于HashIterator封装有Iterator接口除next()方法外的所有同名方法,所以key、value、entry迭代器只需要实现各自的next方法即可。这是模板方法体现之一,即父类封装共用的方法,子类仅实现需要自定义的方法。只不过模板方法的父类一般是一个抽象类,hashmap中则由HashIterator和Iterator共同组成。
3、内部类介绍
由于hashmap键值对存储和遍历的功能离不开内部类的支持,所以在正式介绍源码前讲一下主要的内部类,便于后面理解源码:
1)Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //每个键值对节点对应的唯一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; }
//Node节点的hashcode为key与value的hashcode异或值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//重写equals方法,判断两个Node是否相同,只有在key与value都相同的情况下才相同
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实现了Map的Entry接口,并实现了Entry接口中key、value值获取的方法。注意这里有一个小知识点,就是jdk1.8之后,支持接口定义静态方法。所有Node并没有全部重写Entry的方法。Node封装了基本的节点信息,无论是链表还是后续的红黑树,基础存储单元都是Node节点。
2)HashIterator(KeyIterator、ValueIterator、EntryIterator)
HashIterator实现了迭代器的除next外的所有方法,KeyIterator、ValueIterator、EntryIterator都继承自HashIterator,且实现了迭代器Iterator接口,由于HashIterator实现了Iterator接口除next方法外的所有方法,所以KeyIterator、ValueIterator、EntryIterator只需要关注具体的next方法实现即可。其源码结构也比较简单,大致如下:
abstract class HashIterator {
Node<K,V> next; // 下一个节点引用
Node<K,V> current; // 当前节点引用
int expectedModCount; // 快速失败机制使用的属性,用于监测并发修改
int index; // 遍历数组中的第几个链表或者红黑树
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // 给next引用赋值第一个找到的非空链表或红黑树
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
//1、当前修改书与预期修改数不同,说明遍历期间hashmap被并发修改,故抛异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//2、如果next引用为空,抛异常
if (e == null)
throw new NoSuchElementException();
//3、这一步主要是记录当前节点以及在当前链表或红黑树遍历结束后,将next引用指向下一个链表或红黑树,从而遍历完所有数据。
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
//1、记录当前节点信息
Node<K,V> p = current;
//2、如果当前节点为空,或者这个过程中hashmap被并发修改,则抛异常
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//3、释放当前引用
current = null;
//4、获取当前节点的key信息,并调用外部类中的移除方法(具体的后面讲)
K key = p.key;
removeNode(hash(key), key, null, false, false);
//5、因为修改了hashmap集合,此时修改次数变了,所以重新赋值
expectedModCount = modCount;
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
迭代器源码中比较重要的是index索引的切换,它是一个递增值,用于顺序遍历数组中所有链表或红黑树。
3)KeySet(Values、EntrySet)
KeySet是一个key值的集合对象,但是并不是说真有一个存储key的set集合,其底层还是hashmap,在hashmap中通过方法获得该对象。通过源码查看可以知道set方法实现基础还是调用外部类或者迭代器实现。Values、EntrySet情况类似,这里就不贴代码浪费空间了。
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
至此,hashmap在map框架中的位置以及其内部类大致都了解了,下面来看一下hashmap常用的方法源码。