HashMap 1.7和1.8的区别 --答到面试官怀疑人生
HashMap 1.7和1.8的区别
从今天起,争取一周之内整理完HashMap有关的各种问题。网上有很多HashMap的源码分析的帖子,我看了一些。当然也有写的非常好的,不过大多的源码分析帖企图一篇文章就把问题讲完,有时候看起来就很混乱。而且贴了较多的源码。这里我准备分为6个部分,分别上传五篇篇博文,希望用更清晰的方式,来总结HashMap的相关问题。
六个部分
- HashMap的继承体系,HashMap的内部类,成员变量
- HashMap的常见方法的实现流程
- HashMap的一些特定算法,常量的分析
- HashMap的线程安全问题
- HashMap的线程安全问题解决方案
- HashMap1.8和1.7的区别
文章目录
1结构区别
Jdk1.8
HashMap1.8的底层数据结构是数组+链表+红黑树。
Jdk1.7
HashMap 1.7的底层数据结构是数组加链表
区别:
- 一般情况下,以默认容量16为例,阈值等于12就扩容,单条链表能达到长度为8的概率是相当低的,除非Hash攻击或者HashMap容量过大出现某些链表过长导致性能急剧下降的问题,红黑树主要是为了结果这种问题。
- 在正常情况下,效率相差并不大。
2.节点区别
HashMap 1.7
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; }
HashMap1.8
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; }
区别:
Jdk1.8
- hash是final修饰,也就是说hash值一旦确定,就不会再重新计算hash值了。
- 新增了一个TreeNode节点,为了转换为红黑树。
Jdk1.7
- hash是可变的,因为有rehash的操作。
3.Hash算法区别
Jdk1.7
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
Jdk1.8
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
区别
- 1.8计算出来的结果只可能是一个,所以hash值设置为final修饰。
- 1.7会先判断这Object是否是String,如果是,则不采用String复写的hashcode方法,处于一个Hash碰撞安全问题的考虑
4对Null的处理
Jdk1.7
Jdk1.7中,对null值做了单独的处理
public V put(K key, V value) { //判断是否是空值 if (key == null) return putForNullKey(value); ... }
简单的说,HashMap会遍历数组的下标为0的链表,循环找key=null的键,如果找到则替换。
如果当前数组下标为0的位置为空,即e==null,那么直接执行添加操作,key=null,插入位置为0。
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
Jdk1.8
而1.8中,由于Hash算法中会将null的hash值计算为0,插入时0&任何数都是0,插入位置为数组的下标为0的位置,所以我们可以认为,1.8中null为键和其他非null是一样的,也有hash值,也能别替换。只是计算结果为0而已。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
区别
- Jdk1.7中,null是一个特殊的值,单独处理
- Jdk1.8中,null的hash值计算结果为0,其他地方和普通的key没区别。
5初始化的区别
我们常说Jdk1.8是懒加载,真的是这样吗?
Jdk1.8
transient Node<K,V>[] table;
构造方法
public HashMap(int initialCapacity, float 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 }
我们简答看一下tableSizeFor()方法,其实这个算法和Integer的highestOneBit()方法一样。
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; }
官方解释:Returns a power of two size for the given target capacity. (返回给定目标容量的二次幂。)
也就是获取比传入参数大的最小的2的N次幂。
比如:传入8,就返回8,传入9,就返回16.
Jdk1.7
Jdk1.7中,table在声明时就初始化为空表。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
构造方法和Jdk1.8一致,但是没有立刻根据给定的初始容量去计算那个2的次幂。
public HashMap(int initialCapacity, float loadFactor) { ... this.loadFactor = loadFactor; threshold = initialCapacity; } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
我们可以看一下HashMap1.7的计算容量的方法
首先是put方法时,发现是空表,初始化。传入threshold,也就是我们之前传入的initCapactity自定义初始容量
public V put(K key, V value) { //判断是否是空表 if (table == EMPTY_TABLE) { //初始化 inflateTable(threshold); } ... }
这个方法也有官方的注释,意思就是找到大于等给定toSize的最小2的次幂
private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); ... }
我们发现,这个方法没有什么操作难度,是个人都可以写的出来
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; }
最终调用了Integer的计算2次幂的方法。
public static int highestOneBit(int i) { // HD, Figure 3-1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i - (i >>> 1); }
和1.8的是一致的,但是我们阅读源码发现1.8更趋向于一个方法完成一个大的功能,比如putVal,resize,代码阅读性比较差,而1.7趋向于尽可能的方法拆分,提升阅读性,但是也增加了嵌套关系,结构复杂。
区别
Jdk1.7:
-
table是直接赋值给了一个空数组,在第一次put元素时初始化和计算容量。
-
table是单独定义的inflateTable()初始化方法创建的。
Jdk1.8
- 的table没有赋值,属于懒加载,构造方式时已经计算好了新的容量位置(大于等于给定容量的最小2的次幂)。
- table是resize()方法创建的。
6扩容的区别
无论是哪个版本,扩容都是在新增数据时添加,我们看一下具体区别吧。
扩容的时机
Jdk1.7
public V put(K key, V value) { //各种条件判断,key是否存在,是否为空... if () { ... ... //封装所需参数,准备添加 addEntry(hash, key, value, i); return null; }
我们看到,我们在准备添加数据的时候,我们先判断是否扩容,如果扩容成功了,我们要重新计算一下要插入的元素的hash值。
还有扩容并不是大于阈值就扩容的,如果我们即将插入的桶是空的,我们不会走进这个if语句块,也就是直接指向createEntry方法。
void addEntry(int hash, K key, V value, int bucketIndex) { //判断是否需要扩容 if ((size >= threshold) && (null != table[bucketIndex])) { //扩容 resize(2 * table.length); //重新计算hash值 hash = (null != key) ? hash(key) : 0; //计算所要插入的桶的索引值 bucketIndex = indexFor(hash, table.length); } //执行新增Entry方法 createEntry(hash, key, value, bucketIndex); }
Jdk1.8
虽然很想删减源码,但是也删不了几行,我以图示的方式来展现
实际上在判断是否树化的时候,也会判断扩容。如图,我们知道树化的两个条件,单条桶长度大于等于8,桶总数大于等于64才发生。但是我们可能不知道这里不满足条件还会扩容(其实我写这这篇的时候也不知道,但是准备写红黑树转换过程的时候才看到的)。那么为什么有扩容这个考虑?
我们认为:桶长度小于64。由于我们的扩容都是翻倍操作,所以我们此时的元素总数小于等于32。假设此时我们的数组容量为32,单个桶长度大于8的概率是微乎其微的,因为阈值是24,平均下来一个桶还不到一个Node节点,并且我在之前的HashMap的一些特定算法,常量的分析中,也说明了为什么选择8作为树化的阈值。
但是此时已经有一条链表长度为8了,也就是说阈值24,已经有1/3的节点在单条链表了,我们认为这个哈希表太过于集中了,所以我们进行扩容来增加哈希表内元素的散列程度。
扩容的实现细节
Jdk1.7
这是Jdk1.7的扩容,最重要的方法是transfer,转移的意思,也就是说,将旧数组的元素转移到新的数组。
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //达到最大值,无法扩容 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; //将数据转移到新的Entry[]数组中 transfer(newTable, initHashSeedAsNeeded(newCapacity));//初始化散列种子 //覆盖原数组 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
头插法
除此之外,我们不扩容的情况加,正常插入元素(createEntry方法),也是头插法。
Jdk1.8
过程稍显复杂,我们截取部分程序进行讲解
简单流程:
- j表示当前正在操作的旧数组对应的桶的下标,每次操作一个桶,直至遍历到链表尾部
- 如果正在操作的桶是空的直接下一次循环,否则进行一系列操作
- 判断是否只有一个数据,如果是的,我们直接插入到新数组
- 判断是否是数节点,如果是的,调用树的操作方法,如果不是,走do-while循环
- 循环前标记2个头节点,两个尾节点,表示插入到新位置但不改变下标和插入到新位置改变下标。
- 根据e.hash& oldCap==0来区分节点插入的位置
- 最后do-while结束,将不为空的hoHead和hiHead插入到新数组。然后重复上述操作。
有人说这个if(e.hash & oldCap==0)是这个resize算法最厉害的一行了,我觉得确实有道理。这里篇幅问题,引入别人对于这个判断的详解链接。
扩容的区别总结
Jdk1.7:
- 头插法,添加前先判断扩容,当前准备插入的位置不为空并且容量大于等于阈值才进行扩容,是两个条件!
- 扩容后可能会重新计算hash值。
Jdk1.8:
- 尾插法,初始化时,添加节点结束之后和判断树化的时候都会去判断扩容。我们添加节点结束之后只要size大于阈值,就一定会扩容,是一个条件。
- 由于hash是final修饰,通过e.hash & oldCap==0来判断新插入的位置是否为原位置。
7节点插入的区别
Jdk1.7
扩容
头插法,一个一个的添加进新数组。
新增节点
标记要插入的位置已有的元素,新插入的元素覆盖已有的元素成为新的链表的头,之前标记的已有的元素作为新插入元素的next属性传入构造器,也就是说原来的已有的链表插入到新的链表头的尾部。
Jdk1.8
扩容
1.8中是先得到要插入的链表,再一口气插入到新的数组,为维护两个链表时,是尾插法。
新增节点
从橙色框的部分可以看出,是尾插法。
区别
- jdk1.7无论是resize的转移和新增节点createEntry,都是头插法
- jdk1.8则都是尾插法,为什么这么做呢为了解决多线程的链表死循环问题。
究极总结
比较 | HashMap1.7 | HashMap1.8 |
---|---|---|
数据结构 | 数组+链表 | 数组+链表+红黑树 |
节点 | Entry | Node TreeNode |
Hash算法 | 较为复杂 | 异或hash右移16位 |
对Null的处理 | 单独写一个putForNull()方法处理 | 作为以一个Hash值为0的普通节点处理 |
初始化 | 赋值给一个空数组,put时初始化 | 没有赋值,懒加载,put时初始化 |
扩容 | 插入前扩容 | 插入后,初始化,树化时扩容 |
节点插入 | 头插法 | 尾插法 |
- 深入浅出了解HashMap 1.7和1.8的区别
- HashMap的数据结构,解决哈希冲突,JDK1.7和JDK1.8 HashMap的区别
- jdk 1.8 hashmap和1.7 hashmap区别 原理解析
- 一文读懂JDK1.7,JDK1.8,JDK1.9的hashmap,hashtable,concurrenthashmap及他们的区别
- HashMap1.8与1.7的区别
- HashMap 在 Java1.7 与 1.8 中的区别
- 面试中被问到HashMap的结构,1.7和1.8有哪些区别?这篇做深入分析!
- jdk1.7和jdk1.8中hashmap区别
- HashMap链表在Java1.7与1.8中的区别
- Hashmap的结构,1.7和1.8有哪些区别
- HashMap 1.8版本的原理介绍以及源码分析 1.7与1.8版本的区别及改进
- HashMap在Java1.7与1.8中的区别
- HashMap学习笔记,比较JDK1.7/1.8的区别
- hashmap 在1.7和1.8中的区别?concurrenthashmap
- HashMap的数据结构,哈希冲突,JDK1.7和JDK1.8 HashMap的区别
- HashMap的实现原理,以及在JDK1.7和1.8的区别
- HashMap在Java1.7与1.8中的区别
- Hashmap的结构,1.7和1.8有哪些区别
- 底层了解JDK1.7和JDK1.8的HashMap区别
- HashMap的数据结构,哈希冲突,JDK1.7和JDK1.8 HashMap的区别