面霸篇:Java 集合容器大满贯(卷二)(二)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 接上文。

Map 接口


Map 整体结构如下所示:


image.png


Hashtable 比较特别,作为类似 Vector、Stack 的早期集合相关类型,它是扩展了 Dictionary 类的,类结构上与 HashMap 之类明显不同。


HashMap 等其他 Map 实现则是都扩展了 AbstractMap,里面包含了通用方法抽象。


不同 Map 的用途,从类图结构就能体现出来,设计目的已经体现在不同接口上。


HashMap 的实现原理?


在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的,JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构。


image.png


HashMap 基于 Hash 算法实现的:


  1. 当我们往Hashmap 中 put 元素时,利用 key 的 hashCode 重新 hash 计算出当前对象的元素在数组中的下标。


  1. 存储时,如果出现 hash 值相同的 key,此时有两种情况。


  • 如果 key 相同,则覆盖原始值;


  • 如果 key 不同(出现冲突),则将当前的 key-value 放入链表中


  1. 获取时,直接找到 hash 值对应的下标,在进一步判断 key 是否相同,从而找到对应值。


  1. 理解了以上过程就不难明白 HashMap 是如何解决 hash 冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。


JDK1.7 VS JDK1.8 比较


JDK1.8主要解决或优化了一下问题:


  1. resize 扩容优化


  1. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考


  1. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。


不同 JDK 1.7 JDK 1.8
存储结构 数组 + 链表 数组 + 链表 + 红黑树
初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()
hash值计算方式 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)


如何有效避免哈希碰撞


主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的。


所以我们的思路就是让 hashCode 取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:


static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}


HashMap的put方法的具体流程?


当我们put的时候,首先计算 keyhash值,这里调用了 hash方法,hash方法实际是让key.hashCode()key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞


image.png


①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;


②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;


③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;


④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;


⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;


⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。


HashMap的扩容操作是怎么实现的?


①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;


②.每次扩展的时候,都是扩展2倍;


③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。


在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发.


但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,0 -表示还在原来位置,否则就移动到原数组位置 + oldCap。


重新进行 hash 分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。


任何类都可以作为 Key 么?


可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:


  • 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。


  • 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。


  • 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。


  • 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。
    不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。


为什么HashMap中String、Integer这样的包装类适合作为K?


String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率。


  1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况


  1. 内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;


HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?


hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;


HashMap 的长度为什么是2的幂次方


为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。


这个算法应该如何设计呢?


我们首先可能会想到采用 % 取余的操作来实现。


但是,重点来了:取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。


并且采用二进制位操作 &,相对于 % 能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。


那为什么是两次扰动呢?


答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;


HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用 synchronized + CAS算法。)


  1. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
相关文章
|
16小时前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
6 1
|
1天前
|
缓存 Java 测试技术
探讨Java中遍历Map集合的最快方式
探讨Java中遍历Map集合的最快方式
6 1
|
1天前
|
Java API
探讨Java集合的组内平均值计算
探讨Java集合的组内平均值计算
6 1
|
4天前
|
存储 算法 安全
深入理解Java集合框架:基础类型与代码效率优化
Java集合框架是编程的核心工具,包括List、Set、Queue和Map接口及多种实现类,如ArrayList、LinkedList、HashSet、TreeSet等。理解它们的内部机制有助于优化代码。选择适合的集合类型、避免类型转换、使用并发集合和管理容量可以提升效率。深入学习这些概念能改善代码性能和可维护性。
|
7天前
|
Java 开发者
Queue大比拼:为何LinkedList能在众多Java集合中脱颖而出?
【6月更文挑战第18天】**Java的LinkedList作为队列的优势在于其双向链表实现,支持O(1)时间复杂度的首尾操作,适合作为Queue接口的实现。它也是线程不安全的,但在单线程环境下性能优越,并可通过Collections同步化。此外,它的灵活性使其也能胜任栈和双端队列的角色。**
|
4天前
|
存储 安全 Java
Java集合框架核心组件理解这些基础类型能优化代码效率。
【6月更文挑战第21天】Java集合框架核心组件:ArrayList快速随机访问,适合大量查找;LinkedList擅于插入删除,不适于随机访问;HashMap是键值对存储,O(1)查找删除。选择取决于应用场景:频繁访问选ArrayList,频繁增删选LinkedList,键值查找选HashMap。理解这些基础类型能优化代码效率。
7 1
|
6天前
|
存储 安全 Java
Java集合类是Java编程语言中用于存储和操作一组对象的工具
【6月更文挑战第19天】Java集合类,如`List`、`Set`、`Map`在`java.util`包中,提供高级数据结构。常用实现包括`ArrayList`(快速随机访问)、`LinkedList`(高效插入删除)、`HashSet`(无序不重复)、`TreeSet`(排序)、`HashMap`(键值对)和`TreeMap`(排序映射)。集合动态调整大小,支持对象引用,部分保证顺序。选择合适集合优化性能和数据组织。
10 1
|
8天前
|
存储 Java
打破常规!HashSet和TreeSet教你重新认识Java集合的无序与有序
【6月更文挑战第17天】Java集合框架中的Set接口,HashSet无序而TreeSet有序。HashSet基于哈希表,元素插入顺序不可预测,适合快速去重。TreeSet利用红黑树保证有序性,支持自然排序或自定义排序。若需同时无序和有序,可先用HashSet去重,再将元素加入TreeSet,但会牺牲性能。选择时依据对顺序和性能的需求。
|
8天前
|
存储 Java 索引
告别Java集合小白!一文读懂List的精髓
【6月更文挑战第17天】Java中的List接口作为有序集合,允许存储和操作有序元素,支持重复值。ArrayList和LinkedList是常见实现类:ArrayList基于数组,适合快速访问但插入删除慢;LinkedList基于链表,插入删除快但访问慢。了解其核心概念、方法及泛型使用,能提升编程效率和代码质量。示例代码展示了添加和访问元素。通过深入学习,可以更好地掌握List的高级用法。
|
10天前
|
Java
java集合
摘要:使用`equals`方法可直接比较两个集合是否完全相同,因Java集合类已重写该方法。快速创建集合可采用`Lists.newArrayList()`。
13 3