JDK版本1.8
|
数据结构
|
底层实现
|
初始容量
|
扩容
|
负载因子(默认值)
|
线程安全
|
链表插入值方法
|
|
HashMap
|
Node数组+链表/红黑树
|
16
|
整个map扩容,newsize = oldsize*2,size一定为2的n次幂,区分单项链表和红黑树的扩容
|
0.75
|
否
|
尾插法
|
HaspMap
- 底层Node数组+链表/红黑树实现,可以存储null键和null值,线程不安全
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂,最大容量是2的30次方大小
- 默认装载因子为0.75. 比如,刚开始的大小是16,16*0.75=12. 然后当map的大小超过12,就会进行扩容,变成32个,也就是16的两倍
- 当链表长度达到这个TREEIFY_THRESHOLD(即8)的时候,将链表转化为红黑树
- 当红黑树的长度小于这个UNTREEIFY_THRESHOLD(即6)时,把红黑树转换成链表
- 红黑树的最小的容量,至少是 4 x TREEIFY_THRESHOLD = 32。然后为了避免(resizing 和 treeification thresholds) 设置成64
- 扩容后的数组下标位置要么和原先的数组下标不变的index,要么就是index+oldTable.length
- 每次扩容时,针对链表,遍历每个链表节点的hash值和oldCap与操作区分出高低位链表,分别拆开形成两个高低位链表再放到新的数组中
- 每次扩容时,针对红黑树,遍历红黑树节点hash值计算该节点属于高位链表还是低位链表,然后高低位链表分别判断长度是否小于等于6,满足此条件则将此红黑树的TreeNode类型链表转换成Node类型链表
源码解析
1.初识HashMap实现
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
transient Node[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set> entrySet;
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
和 1.7 大体上都差不多,还是有几个重要的区别:
- TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值
- HashEntry 修改为 Node
Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。
2.put方法
2-1 put方法一览
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回
e = p;
else if (p instanceof TreeNode) //如果当前桶为红黑树,那就要按照红黑树的方式写入数据
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) { //如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)
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 && //如果在遍历过程中找到 key 相同时直接退出遍历
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key //如果 e != null 就相当于存在相同的 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;
}
put方法大致过程如下:
- 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)
- 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可
- 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回
- 如果当前桶为红黑树,那就要按照红黑树的方式写入数据
- 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)
- 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树
- 如果在遍历过程中找到 key 相同时直接退出遍历
- 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖
- 最后判断是否需要进行扩容
3.get方法
3-1 get方法一览
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
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;
}
get方法大致过程如下:
- 首先将 key hash 之后取得所定位的桶
- 如果桶为空则直接返回 null
- 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value
- 如果第一个不匹配,则判断它的下一个是红黑树还是链表
- 红黑树就按照树的查找方式返回值
- 不然就按照链表的方式遍历匹配返回值
从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。
但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。
final HashMap map = new HashMap();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
map.put(UUID.randomUUID().toString(), "");
}
}).start();
}
但是为什么呢?简单分析下
看过上文的还记得在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。
4.遍历方式
还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种:
Iterator> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry next = entryIterator.next();
System.out.println("key=" + next.getKey() + " value=" + next.getValue());
}
Iterator iterator = map.keySet().iterator();
while (iterator.hasNext()){
String key = iterator.next();
System.out.println("key=" + key + " value=" + map.get(key));
}
强烈建议使用第一种 EntrySet 进行遍历。
第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。
简单总结下 HashMap:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。