基于jdk1.8
Map接口下常用的集合有HashMap、HashTable、LinkedHashMap等。相比HashMap,Hashtable是线程安全的,Hashtable中的方法都用了synchronized进行了同步,下面开始源码解读。
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable
实现了Map接口,以及Cloneable和Serializable接口,继承了AbstractMap类。
HashMap与Hashtable的第一个区别在于此,HashMap继承了AbstractMap,而Hashtable继承的是Dictionary抽象类,后面介绍到。
看过源码的人可能都有这样一个疑问:AbstractMap也实现了Map接口,为什么HashMap既继承AbstractMap抽象类还需要实现Map接口吗???
从功能上来说:HashMap实现Map是没有任何作用的。
从结构上来说:由于我们一般是面对接口编程,为了维护结构清晰和完整,是需要实现Map接口的。
而HashMap继承AbstractMap的作用为:AbstractMap 提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
可以看到有四个构造方法。
第一个构造函数:public HashMap(int initialCapacity, float loadFactor)方法用来生成HashMap对象,其它的构造函数都是调用此构造函数来实现的。
参数的说明:
initialCapacity:分配数组的大小,默认大小为16,且只能是2的幂次方
loadFactor:加载因子,作用为:当数组中存储的数据大于了分配空间的总长度*loadFactor之后就进行扩容
该方法涉及到了tableSizeFor方法,源码如下:
//tableSizeFor的作用就是,如果传入A,当A大于0,小于定义的最大容量时,
//如果A是2次幂则返回A,否则将A转化为一个比A大且差距最小的2次幂。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
将我们输入的任意值转化为大于等于此值的2的幂次方。而对比去看,Hashtable的构造函数中对数组table进行了空间的分配,即在构造函数中直接使用了table = new Entry,?>[initialCapacity]
。而在HashMap中却不是在构造函数中分配的。如果不在构造函数中进行数组table空间的配,则一定是在第一次使用put函数存储数据时分配,追踪了下源码,发现HashMap确实是这样实现的。不过下面先看一下HashMap的属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认阈值
static final int TREEIFY_THRESHOLD = 8;
//如果链表的长度小于6,退化为红黑树
static final int UNTREEIFY_THRESHOLD = 6;
//HashMap的长度大于64的时候,转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
可以看到都是定义的几个常量。
而且还定义了一个静态内部类,源码如下:
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node 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的结构,包含数值项,对应的key以及hash值,还有一个next指针。
//初始化并可以进行动态扩容的table
transient Node[] table;
//为了调用keys和values方法而设置的属性
transient Set> entrySet;
//hashMap中k,v的个数
transient int size;
//修改的次数
transient int modCount;
//阈值
int threshold;
//装载因子
final float loadFactor;
上面提到了put方法,那么我们首先来看put方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看到调用了putVal方法。
此方法的思想为:首先根据key得到hashcode,根据hashcode得到要存储的位置i=hash&(n-1),其中n为数组的长度(只有n为2的幂次方时,这句话才与hash%n等价,这就解释了为什么了HashMap的容量必须为2的幂次方)。
得到存储位置i之后,检查此位置是否已经有元素,如果没有,则直接存储在该位置即可,如果有,则在位置的所有节点中遍历是否含有该key,如果已经有了该key,则更新其value即可,如果没有该key,则在该链表的末尾加入该新节点即可。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//如果HashMap为空
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;////重新开辟一个Node的数组
//如果得到位置i处没有元素,那么直接创建新的Node
//根据key的hash值找到要存储的位置,
//如果该位置还没有存储元素,则直接在该位置保存值即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//检查在位置的链表中是否有了该key,
//在下面的代码中,是先检查头结点是否为该key,如果不等于,则在剩余的节点中寻找
//有元素的话,进行下面处理
Node e; K k;
//如果对应p的hash值相等并且key值相等
//如果这个位置的old节点与new节点的key完全相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//p是TreeNode类型
//如果p已经是树节点的一个实例,既这里已经是树了
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
//在剩余的节点中寻找key的位置
//将节点(key,value)加到链表的末尾
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//p的下一个节点是null
p.next = newNode(hash, key, value, null);//指向新的节点
//下面的if判断,如果链表长度大于等于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);///将链表转为红黑树
break;
}
//如果遍历过程中链表中的元素与新添加的元素完全相同,则跳出循环
//简单解释来看,也就是说,hashmap中已经存在了这个key对应这个value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//跳出循环
break;
//继续遍历链表下面的元素
//等同于p = p.next;
p = e;
}
}
//如果e为空,则说明是添加的新节点,
//如果e不为空,则说明该key已经存在,只需要更新value
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;
}
上面使用了resize方法进行扩容。下面是resize方法的源码。在HashMap所有的构造函数中,都没有对数组table分配存储空间。而是将这一步放入到了在put方法中进行table检测,如果为空,则调用resize方法进行扩容(或者说是为了给其开辟空间)。
此方法实现的思想为:
处理了一下两种情况
1)原table为null的情况,如果为空,则开辟默认大小的空间
2)原table不为空的情况,则开辟原来空间的2倍。由于可能oldCap*2会大于最大容量,因此也对其这种溢出情况进行了处理。
分配空间之后,然后将原数组中的元素拷贝到新数组中即可。
final Node[] resize() {
//保存现有的数组内容
Node[] oldTab = table;
//获取原来数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//保存原来的扩充后的数组的长度
int oldThr = threshold;
//用来保存新长度,新扩充的大小
int newCap, newThr = 0;
//HashMap有值的情况
//扩容肯定执行这个分支
if (oldCap > 0) {
//是否大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩大为原来的两倍,并且小于最大值
//DEFAULT_INITIAL_CAPACITY 默认16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//不执行
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//HashMap没有初始化的时候
//不执行
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75
}
//不执行
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新的临界值赋值赋值给threshold
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//下面就是拷贝的过程
//扩容后,重新计算元素新的位置
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {//元素不为空才进行处理
oldTab[j] = null;//释放原来的引用
if (e.next == null)//直接确定元素位置
//扩容前的元素地址为 (oldCap - 1) & e.hash ,
//所以这里的新的地址只有两种可能,一是地址不变,
//二是变为 老位置+oldCap
newTab[e.hash & (newCap - 1)] = e;//该链表上只有一个元素的情况
else if (e instanceof TreeNode)//红黑树情况
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
//遍历链表上的元素,重新定位
//这里如果判断成立,那么该元素的地址在新的数组中就不会改变。因为oldCap
//的最高位的1,在e.hash对应的位上为0,
//所以扩容后得到的地址是一样的,位置不会改变 ,
//在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
//如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,
//也就是oldCap最高位的1,在e.hash对应的位置上也为1,
//所以扩容后的地址改变了,在后面的代码中会放到hiHead中,
//最后赋值给newTab[j + oldCap]
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
剩下部分明天继续补充。
下面是具体的一个例子:
举个栗子来说一下上面的两种情况:
设:oldCap=16 二进制为:0001 0000
oldCap-1=15 二进制为:0000 1111
e1.hash=10 二进制为:0000 1010
e2.hash=26 二进制为:0101 1010
e1在扩容前的位置为:e1.hash & oldCap-1 结果为:0000 1010
e2在扩容前的位置为:e2.hash & oldCap-1 结果为:0000 1010
结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。
现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1
那么在扩容后应该也这么计算呀,扩容后的容量为oldCap*2=32 0010 0000 newCap=32,新的计算
方式应该为
e1.hash & newCap-1
即:0000 1010 & 0001 1111
结果为0000 1010与扩容前的位置完全一样。
e2.hash & newCap-1
即:0101 1010 & 0001 1111
结果为0001 1010,为扩容前位置+oldCap。
而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位
是0,还是1。如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。
再来分析下loTail loHead这两个的执行过程(假设(e.hash & oldCap) == 0成立):
第一次执行:
e指向oldTab[j]所指向的node对象,即e指向该位置上链表的第一个元素
loTail为空,所以loHead指向与e相同的node对象,然后loTail也指向了同一个node对象。
最后,在判断条件e指向next,就是指向oldTab链表中的第二个元素
第二次执行:
lotail不为null,所以lotail.next指向e,这里其实是lotail指向的node对象的next指向e,
也可以说是,loHead的next指向了e,就是指向了oldTab链表中第二个元素。此时loHead指向
的node变成了一个长度为2的链表。然后lotail=e也就是指向了链表中第二个元素的地址。
第三次执行:
与第二次执行类似,loHead上的链表长度变为3,又增加了一个node,loTail指向新增的node
......
hiTail与hiHead的执行过程与以上相同,这里就不再做解释了。
由此可以看出,loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍
历完链表。 这是(e.hash & oldCap) == 0成立的时候。
(e.hash & oldCap) == 0不成立的情况也相同,其实就是把oldCap遍历成两个新的链表,
通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与
newTab[j+oldCap]上面去
以上就是整个resize的过程。
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
这里调用了getNode方法,源码如下:
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
//如果hashMap没有元素直接返回,如果有元素,下面成立
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果根据hash得到的位置,只有一个元素,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不是只有一个元素
if ((e = first.next) != null) {
//红黑树情况
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
//遍历整个链表获取最后一个元素
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
整体思想来看就是,首先根据key得到hashcode,然后根据hashcode得到该key在数组table的存储位置,接着在该位置寻找key值和hashcode值一致的节点即可,如果没有找到,返回null。
当利用一个构造方法传入一个map创建一个HashMap的时候会发现调用该方法。
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
//获取传入map的大小
int s = m.size();
//如果有元素
if (s > 0) {
//真个HashMap为空,直接构造一个新的HashMap
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果大于阈值,那么进行resize
else if (s > threshold)
resize();
//否则遍历传入的map,将对应的元素放入新创建的hashMap中,看到调用了putVal方法
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
可以看到该方法也是通过getNode实现的。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
看到该方法调用了removeNode方法。removeNode方法的思想为:先根据key的hash值找到table的位置i,然后在该位置下的链表寻找key和hash均满足条件的节点。删除节点和链表删除节点方法一致。
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
//判断hashmap此时是否有元素,有元素成立
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//定义几个使用的变量
Node node = null, e; K k; V v;
//p此时代表tab[index = (n - 1) & hash])--头结点
//如果p的位置此时就是要删除的位置
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {//不是头结点
if (p instanceof TreeNode)//红黑树情况
node = ((TreeNode)p).getTreeNode(hash, key);
else {//循环遍历寻找对应的元素,找到了对应的元素
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//如果没有找到返回null
//找到了条件成立
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)//红黑树情况处理
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p)//如果第一个节点就是我们要找的节点
tab[index] = node.next;
else//其他情况的删除元素处理
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
static Class> comparableClassFor(Object x) {
if (x instanceof Comparable) { // 判断是否实现了Comparable接口
Class> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class)
return c; // 如果是String类型,直接返回String.class
if ((ts = c.getGenericInterfaces()) != null) { // 判断是否有直接实现的接口
for (int i = 0; i < ts.length; ++i) { // 遍历直接实现的接口
// 该接口实现了泛型
if (((t = ts[i]) instanceof ParameterizedType) &&
//获取接口不带参数部分的类型对象
//该类型是Comparable
((p = (ParameterizedType)t).getRawType() == Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&// 获取泛型参数数组
//只有一个泛型参数,且该实现类型是该类型本身
as.length == 1 && as[0] == c)
return c; // 返回该类型
}
}
}
return null;
}
getOrDefault()方法是jdk1.8新增的方法。当Map集合中有这个key时,就使用这个key值,如果没有就使用默认值defaultValue。
其他新增方法不再一一介绍。
其他方法如size(),isEmpty(),clear(),keySet(),values()等不在一一粘贴。