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的区别 --答到面试官怀疑人生相关推荐

  1. 常见面试题:为什么HashMap不是线程安全的呢?(JDK1.7和JDK1.8角度)(看完你就能和面试官笑谈人生了)

    title: 常见面试题:为什么HashMap不是线程安全的呢?(JDK1.7和JDK1.8角度)(看完你就能和面试官笑谈人生了) tags: 面试常见题 常见面试题:为什么HashMap不是线程安全 ...

  2. 腾讯面试题:char 和 varchar的最大长度是多少,以及他们之间的区别(看完你就能和面试官笑谈人生了)

    title: 腾讯面试题:char 和 varchar的最大长度是多少,以及他们之间的区别(看完你就能和面试官笑谈人生了) tags: 面试常见题 腾讯面试题:char 和 varchar的最大长度是 ...

  3. Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666

    Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666 作为初级程序员,这样的问题在面试中,也被问到过,随着越来越了解,发现以前自己答的真水. 一般的回答 ​ 先来说说我以 ...

  4. 【python】Get与Post的区别?(面试官最想听到的答案)

    GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二. 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数. 你可能自己 ...

  5. Get与Post的区别?(面试官最想听到的答案)

    GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二. 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数. 你可能自己 ...

  6. Ds918 ds3615 ds3617区别_Get与Post的区别?(面试官最想听到的答案)

    有头发且有趣的码农万里挑一~ 64 有料叔 | 一位有故事的程序猿 GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二. 最直观的区别就是GET把参数包含在 ...

  7. HashMap、HashTable、ConcurrentHashMap、HashSet区别 线程安全类

    HashMap专题:HashMap的实现原理--链表散列 HashTable专题:Hashtable数据存储结构-遍历规则,Hash类型的复杂度为啥都是O(1)-源码分析 Hash,Tree数据结构时 ...

  8. 面试官:兄弟,说说 ArrayList 和 LinkedList 有什么区别

    作者 | 沉默王二 来源 | 沉默王二(ID:cmower) ArrayList 和 LinkedList 有什么区别,是面试官非常喜欢问的一个问题.可能大部分小伙伴和我一样,能回答出"Ar ...

  9. .实现 linkedlist 类java_面试官:兄弟,说说 ArrayList 和 LinkedList 有什么区别

    来自公众号:沉默王二 ArrayList 和 LinkedList 有什么区别,是面试官非常喜欢问的一个问题.可能大部分小伙伴和我一样,能回答出"ArrayList 是基于数组实现的,Lin ...

最新文章

  1. Android 自定义 —— View moveTo与 rMoveTo 的区别
  2. 《UNIXLinux程序设计教程》一第2章-2.0 标准输入输出
  3. leetcode111 爬楼梯 python实现
  4. 学习笔记-AngularJs(十)
  5. Android Gallery组件实现循环显示图像
  6. 速度挑战 - 2小时完成HTML5拼图小游戏
  7. 【Java】深入理解Java虚拟机的读书笔记
  8. 初级web前端必会知识点:HTML部分,看看你都会吗?
  9. MySQL执行计划 EXPLAIN参数
  10. github 仓库管理及代码上传
  11. 2020数学建模B题
  12. pam php水解加碱,2钻井液化学.ppt
  13. 开源中国众包平台 —— 为什么我们需要托管赏金
  14. FaWave(发微)多微博版内测
  15. win10设置锁屏密码_【Win10 技巧】把手机当成电脑一对一专属密匙,人机分离自动锁屏...
  16. 探究腾讯云TCA和阿里acp的区别
  17. 社交产品分析:共同看片,微光
  18. 通过wireshark抓包对nmap一些原理分析
  19. 六自由度机器人(机械臂)运动学建模及运动规划系列(三)——机器人建模及运动学分析的Matlab仿真
  20. ESP32 通过NVS存储WiFi账号和密码至Flash

热门文章

  1. [bzoj2119]股市的预测
  2. 学习linux系统应该从哪方面入手
  3. 数字图像处理的常用测试图像(含Lena)
  4. win11设置小任务栏
  5. 反⑨baka拖更大队:临时约法
  6. foobar2000设置不当造成声音有噪音.
  7. 基于Unity3D的AR小游戏开发【100011412】
  8. 淘宝去ioe用mysql,去 IOE,MySQL 完胜 PostgreSQL
  9. 分享:Word转PDF转换器有哪些?安利这3款好用工具
  10. 心理学的166个现象---之三