HashMap 1.7和1.8的区别 --答到面试官怀疑人生
HashMap源码分析
笔记首页
序号 | 内容 | 链接地址 |
---|---|---|
1 | HashMap的继承体系,HashMap的内部类,成员变量 | https://blog.csdn.net/weixin_44141495/article/details/108327490 |
2 | HashMap的常见方法的实现流程 | https://blog.csdn.net/weixin_44141495/article/details/108329558 |
3 | HashMap的一些特定算法,常量的分析 | https://blog.csdn.net/weixin_44141495/article/details/108305494 |
4 | HashMap的线程安全问题(1.7和1.8) | https://blog.csdn.net/weixin_44141495/article/details/108250160 |
5 | HashMap的线程安全问题解决方案 | https://blog.csdn.net/weixin_44141495/article/details/108420327 |
6 | Map的四种遍历方式,以及删除操作 | https://blog.csdn.net/weixin_44141495/article/details/108329525 |
7 | HashMap1.7和1.8的区别 | https://blog.csdn.net/weixin_44141495/article/details/108402128 |
文章目录
- HashMap源码分析
- HashMap 1.7和1.8的区别
- 1结构区别
- 2.节点区别
- 3.Hash算法区别
- 4对Null的处理
- 5初始化的区别
- 6扩容的区别
- 扩容的时机
- 扩容的实现细节
- 7节点插入的区别
- 总结
HashMap 1.7和1.8的区别
1结构区别
Jdk1.8
HashMap1.8的底层数据结构是数组+链表+红黑树。
Jdk1.7
HashMap 1.7的底层数据结构是数组加链表
区别:
- 一般情况下,以默认容量16为例,阈值等于12就扩容,单条链表能达到长度为8的概率是相当低的,除非Hash攻击或者HashMap容量过大出现某些链表过长导致性能急剧下降的问题,红黑树主要是为了结果这种问题。
- 在正常情况下,效率相差并不大。
2.节点区别
jdk 1.7
static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash;
}
jdk 1.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 linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // needed to unlink next upon deletionboolean 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 >= toSizeint 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-1i |= (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角度)(看完你就能和面试官笑谈人生了)
title: 常见面试题:为什么HashMap不是线程安全的呢?(JDK1.7和JDK1.8角度)(看完你就能和面试官笑谈人生了) tags: 面试常见题 常见面试题:为什么HashMap不是线程安全 ...
- 腾讯面试题:char 和 varchar的最大长度是多少,以及他们之间的区别(看完你就能和面试官笑谈人生了)
title: 腾讯面试题:char 和 varchar的最大长度是多少,以及他们之间的区别(看完你就能和面试官笑谈人生了) tags: 面试常见题 腾讯面试题:char 和 varchar的最大长度是 ...
- Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666
Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666 作为初级程序员,这样的问题在面试中,也被问到过,随着越来越了解,发现以前自己答的真水. 一般的回答 先来说说我以 ...
- 【python】Get与Post的区别?(面试官最想听到的答案)
GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二. 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数. 你可能自己 ...
- Get与Post的区别?(面试官最想听到的答案)
GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二. 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数. 你可能自己 ...
- Ds918 ds3615 ds3617区别_Get与Post的区别?(面试官最想听到的答案)
有头发且有趣的码农万里挑一~ 64 有料叔 | 一位有故事的程序猿 GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二. 最直观的区别就是GET把参数包含在 ...
- HashMap、HashTable、ConcurrentHashMap、HashSet区别 线程安全类
HashMap专题:HashMap的实现原理--链表散列 HashTable专题:Hashtable数据存储结构-遍历规则,Hash类型的复杂度为啥都是O(1)-源码分析 Hash,Tree数据结构时 ...
- 面试官:兄弟,说说 ArrayList 和 LinkedList 有什么区别
作者 | 沉默王二 来源 | 沉默王二(ID:cmower) ArrayList 和 LinkedList 有什么区别,是面试官非常喜欢问的一个问题.可能大部分小伙伴和我一样,能回答出"Ar ...
- .实现 linkedlist 类java_面试官:兄弟,说说 ArrayList 和 LinkedList 有什么区别
来自公众号:沉默王二 ArrayList 和 LinkedList 有什么区别,是面试官非常喜欢问的一个问题.可能大部分小伙伴和我一样,能回答出"ArrayList 是基于数组实现的,Lin ...
最新文章
- Android 自定义 —— View moveTo与 rMoveTo 的区别
- 《UNIXLinux程序设计教程》一第2章-2.0 标准输入输出
- leetcode111 爬楼梯 python实现
- 学习笔记-AngularJs(十)
- Android Gallery组件实现循环显示图像
- 速度挑战 - 2小时完成HTML5拼图小游戏
- 【Java】深入理解Java虚拟机的读书笔记
- 初级web前端必会知识点:HTML部分,看看你都会吗?
- MySQL执行计划 EXPLAIN参数
- github 仓库管理及代码上传
- 2020数学建模B题
- pam php水解加碱,2钻井液化学.ppt
- 开源中国众包平台 —— 为什么我们需要托管赏金
- FaWave(发微)多微博版内测
- win10设置锁屏密码_【Win10 技巧】把手机当成电脑一对一专属密匙,人机分离自动锁屏...
- 探究腾讯云TCA和阿里acp的区别
- 社交产品分析:共同看片,微光
- 通过wireshark抓包对nmap一些原理分析
- 六自由度机器人(机械臂)运动学建模及运动规划系列(三)——机器人建模及运动学分析的Matlab仿真
- ESP32 通过NVS存储WiFi账号和密码至Flash