集合面试题

1. 什么是集合

集合就是一个放数据的容器,准确的说是放数据对象引用的容器
集合类存放的都是对象的引用,而不是对象的本身
集合类型主要有 3 种: set( 集)、 list( 列表)和 map( 映射 )

2. 集合的特点

集合的特点主要有如下两点:
集合是用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小

3. 集合和数组的区别

数组是固定长度的;集合可变长度的。
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

4. 使用集合框架的好处

1. 容量自增长;
2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
4. 通过使用 JDK 自带的集合类,可以降低代码维护和学习新 API 成本。

5. 常用的集合类有哪些?

Map 接口和 Collection 接口是所有集合框架的父接口:
1. Collection 接口的子接口包括: Set 接口和 List 接口
2. Map 接口的实现类主要有: HashMap TreeMap Hashtable ConcurrentHashMap 以及
Properties
3. Set 接口的实现类主要有: HashSet TreeSet LinkedHashSet
4. List 接口的实现类主要有: ArrayList LinkedList Stack 以及 Vector

6. ListSetMap三者的区别?

Java 容器分为 Collection Map 两大类, Collection 集合的子接口有 Set List Queue 三种子接
口。我们比较常用的是 Set List Map 接口不是 collection 的子接口。
Collection 集合主要有 List Set 两大接口
List :一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多
null 元素,元素都有索引。常用的实现类有 ArrayList LinkedList Vector
Set :一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一
null 元素,必须保证元素唯一性。 Set 接口常用实现类是 HashSet LinkedHashSet 以及
TreeSet
Map 是一个键值对集合,存储键、值和之间的映射。 Key 无序,唯一; value 不要求有序,允许重
复。 Map 没有继承于 Collection 接口,从 Map 集合中检索元素时,只要给出键对象,就会返回对应
的值对象。
Map 的常用实现类: HashMap TreeMap HashTable LinkedHashMap 、 ConcurrentHashMap

7. 集合框架底层数据结构

Collection
1. List
Arraylist Object 数组
Vector Object 数组
LinkedList : 双向循环链表
2. Set
HashSet (无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet LinkedHashSet 继承与于 HashSet ,并且其内部是通过 LinkedHashMap
实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 Hashmap 实现一样,不
过还是有一点点区别的。
TreeSet (有序,唯一): 红黑树 ( 自平衡的排序二叉树。 )
Map
HashMap JDK1.8 之前 HashMap 由数组 + 链表组成的,数组是 HashMap 的主体,链表则是
主要为了解决哈希冲突而存在的( 拉链法 解决冲突) .JDK1.8 以后在解决哈希冲突时有了较
大的变化,当链表长度大于阈值(默认为 8 )时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap LinkedHashMap 继承自 HashMap ,所以它的底层仍然是基于拉链式散
列结构,即由数组和链表或红黑树组成。另外, LinkedHashMap 在上面结构的基础上,增加
了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的
操作,实现了访问顺序相关逻辑。
HashTable : 数组 + 链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突
而存在的
TreeMap : 红黑树(自平衡的排序二叉树)

8. 哪些集合类是线程安全的?

Vector :就比 Arraylist 多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使
用。
hashTable :就比 hashMap 多了个 synchronized ( 线程安全 ) ,不建议使用。
ConcurrentHashMap :是 Java5 中支持高并发、高吞吐量的线程安全 HashMap 实现。它由
Segment 数组结构和 HashEntry 数组结构组成。 Segment 数组在 ConcurrentHashMap 里扮演锁的
角色, HashEntry 则用于存储键 - 值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,
Segment 的结构和 HashMap 类似,是一种数组和链表结构;一个 Segment 里包含一个 HashEntry
数组,每个 HashEntry 是一个链表结构的元素;每个 Segment 守护着一个 HashEntry 数组里的元
素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。(推荐使用)
...

9. Java集合的快速失败机制 “fail-fast”

java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生
fail-fast 机制。
例如:假设存在两个线程(线程 1 、线程 2 ),线程 1 通过 Iterator 在遍历集合 A 中的元素,在某个时
候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这
个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集
合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next()
遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍
历;否则抛出异常,终止遍历。
解决办法:
1. 在遍历过程中,所有涉及到改变 modCount 值得地方全部加上 synchronized
2. 使用 CopyOnWriteArrayList 来替换 ArrayList

10. 怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变
集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
示例代码如下:

11. 迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来
获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration ,迭代器允许调用者在迭代过程
中移除元素。
因为所有 Collection 接继承了 Iterator 迭代器

12. Iterator 怎么使用?有什么特点?

Iterator 使用代码如下:
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改
的时候,就会抛出 ConcurrentModificationException 异常。

13. 如何边遍历边移除 Collection 中的元素?

边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:
一种最常见的 错误 代码如下:
运行以上错误代码会报 ConcurrentModificationException 异常 。这是因为当使用
foreach(for(Integer i : list)) 语句时,会自动生成一个 iterator 来遍历该 list ,但同时该 list 正在被
Iterator.remove() 修改。 Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

14. Iterator ListIterator 有什么区别?

Iterator 可以遍历 Set List 集合,而 ListIterator 只能遍历 List
Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前 / 后遍历)。
ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元
素、获取前面或后面元素的索引位置。

15. 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java List 遍历的最佳实践是什么?

遍历方式有以下几种:
1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,
当读取到最后一个元素后停止。
2. 迭代器遍历, Iterator Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特
点,统一遍历集合的接口。 Java Collections 中支持了 Iterator 模式。
3. foreach 循环遍历。 foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明
Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过
程中操作数据集合,例如删除、替换。
最佳实践: Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支
Random Access
如果一个数据集合实现了该接口,就意味着它支持 Random Access ,按位置读取元素的平均
时间复杂度为 O(1) ,如 ArrayList
如果没有实现该接口,表示不支持 Random Access ,如 LinkedList
推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator
foreach 遍历。

16. 说一下 ArrayList 的优缺点

ArrayList 底层以数组实现,是一种随机访问模式。 ArrayList 实现了 RandomAccess 接口,
因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下:
插入,删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性
能。
ArrayList 比较适合顺序添加、随机访问的场景。

ArrayList 的扩容机制了解吗?

ArrayList 确切地说,应该叫做动态数组,因为它的底层是通过数组来实现的,当往 ArrayList 中添加元素时,会先检查是否需要扩容,如果当前容量+1 超过数组长度,就会进行扩容。

三分恶面渣逆袭:ArrayList扩容

 

扩容后的新数组长度是原来的 1.5 倍,然后再把原数组的值拷贝到新数组中。

17. 如何实现数组和 List 之间的转换?

数组转 List :使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。

18. ArrayList LinkedList 的区别是什么?

数据结构实现: ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实
现。
随机访问效率: ArrayList LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数
据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增加和删除操作, LinkedList 要比 ArrayList 效率要高,因为
ArrayList 增删操作要影响数组内的其他数据的下标。
内存空间占用: LinkedList ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储
了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全: ArrayList LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList ,而在插入和删除操作较多
时,更推荐使用 LinkedList

19. ArrayList Vector 的区别是什么?

这两个类都实现了 List 接口( List 接口继承了 Collection 接口),他们都是有序集合
线程安全: Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非
线程安全的。
性能: ArrayList 在性能方面要优于 Vector
扩容: ArrayList Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次
会增加 1 倍,而 ArrayList 只会增加 50%
Vector 类的所有方法都是同步的。可以由两个线程安全地访问一个 Vector 对象、但是一个线程访问
Vector 的话代码要在同步操作上耗费大量的时间。
Arraylist 不是同步的,所以在不需要保证线程安全时时建议使用 Arraylist

20. 插入数据时,ArrayListLinkedListVector谁速度较快?阐述

ArrayList Vector LinkedList 的存储性能和特性?
ArrayList Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便
增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操
作,所以索引数据快而插入数据慢。
Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较
ArrayList
LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需
要记录当前项的前后项即可,所以 LinkedList 插入速度较快

21. 多线程场景下如何使用 ArrayList

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections synchronizedList
法将其转换成线程安全的容器后再使用。例如像下面这样:

22. 为什么 ArrayList elementData 加上 transient 修饰?

ArrayList 是 Java 中常用的集合类之一。其内部使用了一个动态数组 elementData 来存储元素。

transient 关键字在 Java 中用于表示一个字段不应被序列化。

elementData 的定义

ArrayList 源代码中,可以看到 elementData 是用 transient 修饰的:

private transient Object[] elementData;

为什么使用 transient

  1. 优化序列化性能ArrayList 中的 elementData 数组往往比实际存储的元素多。这是因为 ArrayList 会预分配一些空间,以减少数组扩展的次数。如果直接序列化 elementData,会导致序列化数据中包含许多无用的空元素,浪费空间和带宽。通过将 elementData 声明为 transient,可以避免序列化这些空元素。

  2. 自定义序列化逻辑ArrayList 提供了自定义的序列化和反序列化逻辑,通过实现 writeObjectreadObject 方法来控制具体的序列化行为:

  • writeObject:序列化时,只写入实际元素的数量和内容,而不是整个数组。
  • readObject:反序列化时,重新分配数组并填充元素。

这种方式避免了序列化空元素,并且使得序列化数据更加紧凑和高效

ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。 transient 的作用
是说不希望 elementData 数组被序列化,重写了 writeObject 实现:
每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后
遍历 elementData ,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后
的文件大小。

23. List Set 的区别

List , Set 都是继承自 Collection 接口
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多
null 元素,元素都有索引。常用的实现类有 ArrayList LinkedList Vector
Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一
null 元素,必须保证元素唯一性。 Set 接口常用实现类是 HashSet LinkedHashSet 以及
TreeSet
另外 List 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是 set 只能用迭代,因为他无
序,无法用下标来取得想要的值。
Set List 对比
Set :检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List :和数组类似, List 可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起
其他元素位置改变

Set接口

24. 说一下 HashSet 的实现原理?

HashSet的实现原理总结如下:

①是基于HashMap实现的,放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

②当我们试图将类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。

③HashSet的其他操作都是基于HashMap的

25. HashSet 如何检查重复? HashSet 是如何保证数据不可重复的?

向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap
的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap比较key是否相等是先比较hashcode 再比较equals )

hashCode ()与 equals ()的相关规定
1. 如果两个对象相等,则 hashcode 一定也是相同的
2. 两个对象相等 , 对两个 equals 方法返回 true
3. 两个对象有相同的 hashcode 值,它们也不一定是相等的

25.==equals的区别

1.
== 是判断两个变量或实例是不是指向同一个内存空间
equals 是判断两个变量或实例所指向的内存 空间的值是不是相同
2. == 是指对内存地址进行比较 equals() 是对字符串的内容进行比较

26. HashSetHashMap的区别

Map接口

27. 什么是Hash算法

哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈
希值。

28. 什么是链表

链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查
等功能。
链表大致分为单链表和双向链表
1. 单链表 : 每个节点包含两部分 , 一部分存放数据 , 另一部分是指向下一节点的
2. 双向链表 : 除了包含单链表的部分 , 还增加了指向前 一个节点的指针
链表的优点 插入删除速度快(因为有 next 指针指向其下一个节点,通过改变指针的指向可以方便的增加 删除元素)
内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于 node 节点的大
小),并且在需要空间的时候才创建空间)
大小没有固定,拓展很灵活。
链表的缺点
不能随机查找,必须从第一个开始遍历,查找效率低

29. 说一下HashMap的实现原理?

其实HashMap在JDK1.7及以前是一个“链表散列”的数据结构,即数组 + 链表的结合体。JDK8优化为:数组+链表+红黑树。

我们常把数组中的每一个节点称为一个桶。当向桶中添加一个键值对时,首先计算键值对中key的hash值(hash(key)),以此确定插入数组中的位置(即哪个桶),但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的最后面,链表就这样形成了。

当链表长度超过8(TREEIFY_THRESHOLD - 阈值)时,链表就自行转为红黑树。

HashMap的数据结构: 即数组和链表的结合体。
HashMap 基于 Hash 算法实现的
1. 当我们往 HashMap put 元素时,利用 key hashCode 重新 hash 计算出当前对象的元素在数
组中的下标
2. 存储时,如果出现 hash 值相同的 key ,此时有两种情况。
(1) 如果 key 相同,则覆盖原始值;
(2) 如果 key 不同(出现冲突),则将当前的 key-value 放入链表中
3. 获取时,直接找到 hash 值对应的下标,在进一步判断 key 是否相同,从而找到对应值。
4. 理解了以上过程就不难明白 HashMap 是如何解决 hash 冲突的问题,核心就是使用了数组的
存储方式,然后将冲突的 key 的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
需要注意 Jdk 1.8 中对 HashMap 的实现做了优化,当链表中的节点数据超过八个之后,该链表会转
为红黑树来提高查询效率,从原来的 O(n) O(logn)

30. HashMapJDK1.7JDK1.8中有哪些不同?HashMap的底层实现

Java 中,保存数据有两种比较简单的数据结构:数组和链表。 数组的特点是:寻址容易,插入和
删除困难;链表的特点是:寻址困难,但插入和删除容易; 所以我们将数组和链表结合在一起,发
挥两者各自的优势,使用一种叫做 拉链法 的方式可以解决哈希冲突。
HashMap JDK1.8 之前
JDK1.8 之前采用的是拉链法。 拉链法 :将链表和数组相结合。也就是说创建一个链表数组,数组
中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 HashMap JDK1.8 之后
相比于之前的版本, jdk1.8 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8
时,将链表转化为红黑树,以减少搜索时间。

31. 什么是红黑树

说道红黑树先讲什么是二叉树
二叉树简单来说就是 每一个节上可以关联俩个子节点
红黑树
红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红
(Red) 或黑 (Black)
红黑树的每个结点是黑色或者红色。不管怎么样他的根结点是黑色。每个叶子结点(叶子结点
代表终结、结尾的节点)也是黑色 [ 注意:这里叶子结点,是指为空 (NIL NULL) 的叶子结点! ]
如果一个结点是红色的,则它的子结点必须是黑色的。
每个结点到叶子结点 NIL 所经过的黑色结点的个数一样的。 [ 确保没有一条路径会比其他路径长出俩
倍,所以红黑树是相对接近平衡的二叉树的! ]
红黑树的基本操作是 添加、删除 。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么
呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面
三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新
成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
大概就是这样子:

32. HashMapput方法的具体流程?

HashMap的数据结构在jdk1.8之前是数组+链表,为了解决数据量过大、链表过长是查询效率会降低的问题变成了数组+链表+红黑树的结构,利用的是红黑树自平衡的特点。

链表的平均查找时间复杂度是O(n),红黑树是O(log(n))。

HashMap中的put方法执行过程大体如下:

1、判断键值对数组table[i]是否为空(null)或者length=0,是的话就执行resize()方法进行扩容。

2、不是就根据键值key计算hash值得到插入的数组索引i。

3、判断table[i]==null,如果是true,直接新建节点进行添加,如果是false,判断table[i]的首个元素是否和key一样,一样就直接覆盖。

4、判断table[i]是否为treenode,即判断是否是红黑树,如果是红黑树,直接在树中插入键值对。

5、如果不是treenode,开始遍历链表,判断链表长度是否大于8,如果大于8就转成红黑树,在树中执行插入操作,如果不是大于8,就在链表中执行插入;在遍历过程中判断key是否存在,存在就直接覆盖对应的value值。

6、插入成功后,就需要判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过了,执行resize方法进行扩容。

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

HashMap 的基本结构

HashMap 的底层实现主要依赖于一个数组和链表(在 Java 8 及之后还可能使用红黑树)。这个数组称为 table,它的每个位置称为一个桶(bucket)。每个桶存放一个链表,链表中的每个节点存储一个键值对。

触发扩容的条件

HashMap 的扩容是由负载因子(load factor)和当前存储元素的数量决定的。默认情况下,HashMap 的负载因子为 0.75。当元素数量超过 capacity * loadFactor 时,HashMap 就会触发扩容操作。

扩容操作的步骤

  1. 计算新容量: 新容量通常是当前容量的两倍。新的容量需要是2的幂次,以便散列函数的性能和分布均匀性。

  2. 创建新表: 创建一个新的更大容量的数组来替代旧的数组。

  3. 重新散列旧表中的元素: 遍历旧表中的每个元素,并将其重新散列到新表中的相应位置。这个步骤是必要的,因为扩容后,元素在数组中的位置可能会改变。

34. HashMap是怎么解决哈希冲突的?

  • 链地址法(Separate Chaining): 这是 HashMap 解决哈希冲突的主要方法。每个数组桶(bucket)存储的是一个链表(在 Java 8 及之后也可能是红黑树)。当多个键散列到同一个桶时,这些键值对会被存储在同一个链表中。

  • 红黑树(Treeify): 在 Java 8 及之后,当单个桶中的链表长度超过一定阈值(默认是8)时,链表会转换成红黑树。这种转换能够显著提高查找、插入和删除操作的性能,从 O(n) 提高到 O(log n)。

35. 能否使用任何类作为 Map key

可以使用任何类作为 Map key ,然而在使用之前,需要考虑以下几点:
类的所有实例需要遵循与 equals() hashCode() 相关的规则。
如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
如果一个类没有使用 equals() ,不应该在 hashCode() 中使用它。
用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的
性能。不可变的类也可以确保 hashCode() equals() 在未来不会改变,这样就会解决与可变相关
的问题了。

36. 为什么HashMapStringInteger这样的包装类适合作为K

答: String Integer 等包装类的特性能够保证 Hash 值的不可更改性和计算准确性,能够有效的减
Hash 碰撞的几率
都是 final 类型,即不可变性,保证 key 的不可更改性,不会存在获取 hash 值不同的情况
内部已重写了 equals() hashCode() 等方法,遵守了 HashMap内部的规范不容易出现 Hash 值计算错误的情况;

37. 如果使用Object作为HashMapKey,应该怎么办呢?

答:重写 hashCode() equals() 方法
1. 重写 hashCode() 是因为需要计算存储数据的存储位置 ,需要注意不要试图从散列码计算中
排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的 Hash 碰撞;
2. 重写 equals() 方法 ,需要遵守自反性、对称性、传递性、一致性以及对于任何非 null 的引用
x x.equals(null) 必须返回 false 的这几个特性, 目的是为了保证 key 在哈希表中的唯一性

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

答: hashCode() 方法返回的是 int 整数类型,其范围为 -(2 ^ 31)~(2 ^ 31 - 1) ,约有 40 亿个映射空
间,而 HashMap 的容量范围是在 16 (初始化默认值) ~2 ^ 30
存在哈希值与数组大 小范围不匹配” 的问题
那怎么解决呢?
1. HashMap 自己实现了自己的 hash() 方法,通过两次扰动使得它自己的哈希值高低位自行进
行异或运算,降低哈希碰撞概率也使得数据分布更平均;
2. 在保证数组长度为 2 的幂次方的时候,使用 hash() 运算之后的值与运算( & )(数组长度 -
1 )来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有 当数组长度为 2 的幂次方时, h&(length-1) 才等价于 h%length ,三来解决了 哈希值与数组大
小范围不匹配 的问题;

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

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

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

我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了: 取余 (%) 操作中如果除数是
2 的幂次则等价于与其除数减一的与 (&) 操作(也就是说 hash%length==hash&(length-1)
前提是 length 2 n 次方;)。 并且 采用二进制位操作 & ,相对于 % 能够提高运算效
率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

那为什么是两次扰动呢?

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

40. HashMap HashTable 有什么区别?

1. 线程安全 HashMap 是非线程安全的, HashTable 是线程安全的; HashTable 内部的方法基本
都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );
2. 效率 : 因为线程安全的问题, HashMap 要比 HashTable 效率高一点。另外, HashTable 基本被 淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap );
3. Null key Null value 的支持 HashMap 中, null 可以作为键,这样的键只有一个,可以有
一个或多个键所对应的值为 null 。但是在 HashTable put 进的键值只要有一个 null ,直接抛
NullPointerException
4. 初始容量大小和每次扩充容量大小的不同
5. 创建时如果不指定容量初始值,HashMap 默认的初始化大小为 16 。之后每次扩充,容量变为原来的 2 倍。
Hashtable 默认的初始大小为 11 ,之后每次扩充,容量变为原来 的2n+1
6. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其
扩充为 2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为
什么是 2 的幂次方。
7. 底层数据结构 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈 值(默认为8 )时,将链表转化为红黑树,以减少搜索时间。 Hashtable 没有这样的机制。
8. 推荐使用:在 Hashtable 的类注释可以看到, Hashtable 是保留类不建议使用,推荐在单线程环
境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

41. 什么是TreeMap 简介

TreeMap 是一个 有序的 key-value 集合 ,它是通过红黑树实现的。
TreeMap 基于 红黑树( Red-Black tree )实现 。该映射根据 其键的自然顺序进行排序 ,或者根据
创建映射时提供的 Comparator 进行排序 ,具体取决于使用的构造方法。
TreeMap 是线程 非同步 的。

42. 如何决定使用 HashMap 还是 TreeMap

对于在 Map 中插入、删除和定位元素这类操作, HashMap 是最好的选择。然而,假如你需要对一
个有序的 key 集合进行遍历, TreeMap 是更好的选择。基于你的 collection 的大小,也许向
HashMap 中添加元素会更快,将 map 换为 TreeMap 进行有序 key 的遍历。

43. HashMap ConcurrentHashMap 的区别

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

44. ConcurrentHashMap Hashtable 的区别?

ConcurrentHashMap Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构 JDK1.7 ConcurrentHashMap 底层采用 分段的数组 + 链表 实现, JDK1.8
采用的数据结构跟 HashMap1.8 的结构一样,数组 + 链表 / 红黑二叉树。 Hashtable JDK1.8
之前的 HashMap 的底层数据结构类似都是采用 数组 + 链表 的形式,数组是 HashMap 的主
体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式
1. JDK1.7 的时候, ConcurrentHashMap (分段锁) 对整个桶数组进行了分割分段
(Segment) ,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就
不会存在锁竞争,提高并发访问率。(默认分配 16 Segment ,比 Hashtable 效率提高 16
倍。) 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组 + 链表 + 红黑
树的数据结构来实现,并发控制使用 synchronized CAS 来操作。( JDK1.6 以后 对
synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在
JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
2. Hashtable( 同一把锁 ) : 使用 synchronized 来保证线程安全,效率非常低下。当一个线程
访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加
元素,另一个线程不能使用 put 添加元素,也不能使用 get ,竞争会越来越激烈效率越低。

45. ConcurrentHashMap 底层具体实现知道吗?实现原理是什 么?

JDK1.7
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段
数据时,其他段的数据也能被其他线程访问。
JDK1.7 中, ConcurrentHashMap 采用 Segment + HashEntry 的方式进行实现,结构如下:
一个 ConcurrentHashMap 里包含一个 Segment 数组。 Segment 的结构和 HashMap 类似,是一
种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构
的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修
改时,必须首先获得对应的 Segment 的锁。
1. 该类包含两个静态内部类 HashEntry Segment ;前者用来封装映射表的键值对,后者用来充当 锁的角色;
2. Segment 是一种可重入的锁 ,每个 Segment 守护一个 HashEntry 数组里得元
素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
JDK1.8
JDK1.8 中,放弃了 Segment 臃肿的设计,取而代之的是采用 Node + CAS + Synchronized 来保
证并发安全进行实现 synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲
突,就不会产生并发,效率又提升 N 倍。

46. Array ArrayList 有何区别?

Array 可以存储基本数据类型和对象, ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll removeAll iteration 等方法只有 ArrayList
有。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方 式相对比较慢。

47. comparable comparator的区别?

comparable 接口实际上是出自 java.lang 包,它有一个 compareTo(Object obj) 方法用来排序 comparator 接口实际上是出自 java.util 包,它有一个 compare(Object obj1, Object obj2) 方法用
来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写 compareTo 方法或 compare 方法,当
我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排
序方法的话,我们可以重写 compareTo 方法和使用自制的 Comparator 方法或者以两个
Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的
Collections.sort().

48. Collection Collections 有什么区别?

java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操
作的通用接口方法。 Collection 接口在 Java 类库中有很多具体的实现。 Collection 接口的意义是为
各种具体的集合提供了最大化的统一操作方式,其直接继承接口有 List Set
Collections 则是集合类的一个工具类 / 帮助类,其中提供了一系列静态方法,用于对集合中元素进
行排序、搜索以及线程安全等各种操作。

49. TreeMap TreeSet 在排序时如何比较元素?

都是通过 实现Comparable接口来比较元素,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小

50 .Collections 具类中的 sort()方法如何比较元素?

Collections工具类的sort方法有两种重载的形式,

第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;
第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,
自定义比较器,Collections.sort(List list,Comparator compare),创建比较器类实现接口

1.说说有哪些常见的集合框架?。

Java 集合框架可以分为两条大的支线:

①、Collection,主要由 List、Set组成:

②、Map,代表键值对的集合,典型代表就是 HashMapopen in new window

List

#2.ArrayList 和 LinkedList 有什么区别?

  • ArrayList 基于动态数组实现  LinkedList 基于链表实现

三分恶面渣逆袭:ArrayList和LinkedList的数据结构

 

多数情况下,ArrayList 更利于查找,LinkedList 更利于增删

①、由于 ArrayList 是基于数组实现的,所以 get(int index) 可以直接通过数组下标获取元素,时间复杂度是 O(1);LinkedList 是基于链表实现的,get(int index) 需要遍历链表,时间复杂度是 O(n)。

②、ArrayList 如果增删的是数组的尾部,直接插入或者删除就可以了,时间复杂度是 O(1);如果 add 的时候涉及到扩容,时间复杂度会提升到 O(n)。

但如果插入的是中间的位置,就需要把插入位置后的元素向前或者向后移动,甚至还有可能触发扩容,效率就会低很多,O(n)。

LinkedList 因为是链表结构,插入和删除只需要改变前置节点、后置节点和插入节点的引用就行了,不需要移动元素。

三分恶面渣逆袭:ArrayList和LinkedList中间删除

 

注意,这里有个陷阱,LinkedList 更利于增删不是体现在时间复杂度上,因为二者增删的时间复杂度都是 O(n),都需要遍历列表;而是体现在增删的效率上,因为 LinkedList 的增删只需要改变引用,而 ArrayList 的增删可能需要移动元素。

#是否支持随机访问?

①、ArrayList 是基于数组的,也实现了 RandomAccess 接口,所以它支持随机访问,可以通过下标直接获取元素。

②、LinkedList 是基于链表的,所以它没法根据下标直接获取元素,不支持随机访问,所以它也没有实现 RandomAccess 接口。

#内存占用有何不同?

ArrayList 是基于数组的,是一块连续的内存空间,所以它的内存占用是比较紧凑的;但如果涉及到扩容,就会重新分配内存,空间是原来的 1.5 倍,存在一定的空间浪费。

LinkedList 是基于链表的,每个节点都有一个指向下一个节点和上一个节点的引用,于是每个节点占用的内存空间稍微大一点。

3.ArrayList 的扩容机制了解吗?

ArrayList 确切地说,应该叫做动态数组,因为它的底层是通过数组来实现的,当往 ArrayList 中添加元素时,会先检查是否需要扩容,如果当前容量+1 超过数组长度,就会进行扩容。

扩容后的新数组长度是原来的 1.5 倍,然后再把原数组的值拷贝到新数组中。

4.ArrayList 怎么序列化的知道吗? 为什么用 transient 修饰数组?

ArrayList 的序列化不太一样,它使用transient修饰存储元素的elementData的数组,transient关键字的作用是让被修饰的成员属性不被序列化。

为什么最 ArrayList 不直接序列化元素数组呢?

出于效率的考虑,数组可能长度 100,但实际只用了 50,剩下的 50 不用其实不用序列化,这样可以提高序列化和反序列化的效率,还可以节省内存空间。

那 ArrayList 怎么序列化呢?

ArrayList 通过两个方法readObject、writeObject自定义序列化和反序列化策略,实际直接使用两个流ObjectOutputStreamObjectInputStream来进行序列化和反序列化。

5.快速失败(fail-fast)和安全失败(fail-safe)了解吗?

快速失败(fail—fast):快速失败是 Java 集合的一种错误检测机制

  • 在用迭代器遍历一个集合对象时,如果线程 A 遍历过程中,线程 B 对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。
  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
  • 场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如 ArrayList 类。

安全失败(fail—safe)

  • 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
  • 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
  • 缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
  • 场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如 CopyOnWriteArrayList 类。

6.有哪几种实现 ArrayList 线程安全的方法?

可以使用 Collections.synchronizedList() 方法,它将返回一个线程安全的 List。

SynchronizedList list = Collections.synchronizedList(new ArrayList());

内部是通过 synchronized 关键字open in new window加锁来实现的。

也可以直接使用 CopyOnWriteArrayListopen in new window,它是线程安全的,遵循写时复制的原则,每当对列表进行修改(例如添加、删除或更改元素)时,都会创建列表的一个新副本,这个新副本会替换旧的列表,而对旧列表的所有读取操作仍然可以继续。

CopyOnWriteArrayList list = new CopyOnWriteArrayList();

通俗的讲,CopyOnWrite 就是当我们往一个容器添加元素的时候,不直接往容器中添加,而是先复制出一个新的容器,然后在新的容器里添加元素,添加完之后,再将原容器的引用指向新的容器。多个线程在读的时候,不需要加锁,因为当前容器不会添加任何元素。这样就实现了线程安全。

7.CopyOnWriteArrayList 了解多少?

CopyOnWriteArrayList 就是线程安全版本的 ArrayList。

它的名字叫CopyOnWrite——写时复制,已经明示了它的原理。

CopyOnWriteArrayList 采用了一种读写分离的并发策略。CopyOnWriteArrayList 容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

CopyOnWriteArrayList原理

Map

8.能说一下 HashMap 的底层数据结构吗?

推荐阅读:二哥的 Java 进阶之路:详解 HashMapopen in new window

JDK 8 中 HashMap 的数据结构是数组+链表+红黑树

三分恶面渣逆袭:JDK 8 HashMap 数据结构示意图

 

HashMap 的核心是一个动态数组(Node[] table),用于存储键值对。这个数组的每个元素称为一个“桶”(Bucket),每个桶的索引是通过对键的哈希值进行哈希函数处理得到的。

当多个键经哈希处理后得到相同的索引时,会发生哈希冲突。HashMap 通过链表来解决哈希冲突——即将具有相同索引的键值对通过链表连接起来。

不过,链表过长时,查询效率会比较低,于是当链表的长度超过 8 时(且数组的长度大于 64),链表就会转换为红黑树。红黑树的查询效率是 O(logn),比链表的 O(n) 要快。数组的查询效率是 O(1)。

当向 HashMap 中添加一个键值对时,会使用哈希函数计算键的哈希码,确定其在数组中的位置,哈希函数的目标是尽量减少哈希冲突,保证元素能够均匀地分布在数组的每个位置上。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

当向 HashMap 中添加元素时,如果该位置已有元素(发生哈希冲突),则新元素将被添加到链表的末尾或红黑树中。如果键已经存在,其对应的值将被旧值覆盖。

当从 HashMap 中获取元素时,也会使用哈希函数计算键的位置,然后根据位置在数组、链表或者红黑树中查找元素。

HashMap 的初始容量是 16,随着元素的不断添加,HashMap 的容量(也就是数组大小)可能不足,于是就需要进行扩容,阈值是capacity * loadFactor,capacity 为容量,loadFactor 为负载因子,默认为 0.75。

扩容后的数组大小是原来的 2 倍,然后把原来的元素重新计算哈希值,放到新的数组中

9.你对红黑树了解多少?为什么不用二叉树/平衡树呢?

红黑树是一种自平衡的二叉查找树:

  1. 每个节点要么是红色,要么是黑色;
  2. 根节点永远是黑色;
  3. 所有的叶子节点都是是黑色的(下图中的 NULL 节点);
  4. 红色节点的子节点一定是黑色的;
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

三分恶面渣逆袭:红黑树

三分恶面渣逆袭:红黑树
#为什么不用二叉树?

二叉树是最基本的树结构,每个节点最多有两个子节点,但是二叉树容易出现极端情况,比如插入的数据是有序的,那么二叉树就会退化成链表,查询效率就会变成 O(n)。

#为什么不用平衡二叉树?

平衡二叉树比红黑树的要求更高,每个节点的左右子树的高度最多相差 1,这种高度的平衡保证了极佳的查找效率,但在进行插入和删除操作时,可能需要频繁地进行旋转来维持树的平衡,这在某些情况下可能导致更高的维护成本。

红黑树是一种折中的方案,它在保证了树平衡的同时,插入和删除操作的性能也得到了保证,查询效率是 O(logn)。

10.红黑树怎么保持平衡的?

红黑树有两种方式保持平衡:旋转染色

①、旋转:旋转分为两种,左旋和右旋

三分恶面渣逆袭:左旋

三分恶面渣逆袭:左旋

三分恶面渣逆袭:右旋

三分恶面渣逆袭:右旋

②、染⾊:

三分恶面渣逆袭:染色

11.HashMap 的 put 流程知道吗?

直接看流程图。

三分恶面渣逆袭:HashMap插入数据流程图

三分恶面渣逆袭:HashMap插入数据流程图

第一步,通过 hash 方法计算 key 的哈希值。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

第二步,数组进行第一次扩容。

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

第三步,根据哈希值计算 key 在数组中的下标,如果对应下标正好没有存放数据,则直接插入。

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

如果对应下标已经有数据了,就需要判断是否为相同的 key,是则覆盖 value,否则需要判断是否为树节点,是则向树中插入节点,否则向链表中插入数据。

注意,在链表中插入节点的时候,如果链表长度大于等于 8,则需要把链表转换为红黑树。

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);

所有元素处理完后,还需要判断是否超过阈值threshold,超过则扩容。

if (++size > threshold)
    resize();
#只重写 equals 没重写 hashcode,map put 的时候会发生什么?

如果只重写 equals 方法,没有重写 hashcode 方法,那么会导致 equals 相等的两个对象,hashcode 不相等,这样的话,这两个对象会被放到不同的桶中,这样就会导致 get 的时候,找不到对应的值。

12.HashMap 怎么查找元素的呢?

先看流程图:

HashMap查找流程图

HashMap查找流程图

HashMap 的查找就简单很多:

  1. 使用扰动函数,获取新的哈希值
  2. 计算数组下标,获取节点
  3. 当前节点和 key 匹配,直接返回
  4. 否则,当前节点是否为树节点,查找红黑树
  5. 否则,遍历链表查找

13.HashMap 的 hash 函数是怎么设计的?

HashMap 的哈希函数是先拿到 key 的 hashcode,是一个 32 位的 int 类型的数值,然后让 hashcode 的高 16 位和低 16 位进行异或操作。这么设计是为了降低哈希碰撞的概率。

static final int hash(Object key) {
    int h;
    // key的hashCode和key的hashCode右移16位做异或运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

14.为什么 hash 函数能降哈希碰撞?

在 hash 函数中,先调用了 key 的hashCode() 方法,这将会返回一个32 位的 int 类型的哈希值

但是数组长度比较小,需要hash 算法,来避免发生哈希冲突,尽可能地让元素均匀地分布在数组当中。

第一个就是数组的长度必须是 2 的整数次幂,这样可以保证 hash & (n-1) 的结果能均匀地分布在数组中。& 操作的结果就是哈希值的高位全部归零,只保留 n 个低位,用来做数组下标访问

其作用就相当于 hash % n,n 为数组的长度

第二:

将哈希值无符号右移 16 位,意味着原哈希值的高 16 位被移到了低 16 位的位置。这样,原始哈希值的高 16 位和低 16 位都可以参与到最终用于索引计算的低位中。

选择 16 位是因为它是 32 位整数的一半,这样处理既考虑了高位的信息,又没有完全忽视低位原本的信息,尝试达到一个平衡状态。

15.为什么 HashMap 的容量是 2 的倍数呢?

HashMap 的容量是 2 的倍数,或者说是 2 的整数次幂,是为了快速定位元素的下标:

HashMap 在定位元素位置时,先通过 哈希函数hash(key) = (h = key.hashCode()) ^ (h >>> 16) 计算出哈希值,再通过 hash & (n-1) 来定位元素位置的,n 为数组的大小,也就是 HashMap 的容量。

因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0。

2 的整次幂(或者叫 2 的整数倍)刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(取决于 hash 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀分布。

换句话说,& 操作的结果就是将哈希值的高位全部归零,只保留低位值

18.解决哈希冲突有哪些方法呢?

解决哈希冲突的方法我知道的有 3 种:

①、再哈希法

准备两套哈希算法,当发生哈希冲突的时候,使用另外一种哈希算法,直到找到空槽为止。对哈希算法的设计要求比较高。

②、开放地址法

遇到哈希冲突的时候,就去寻找下一个空的槽。有 3 种方法:

  • 线性探测:从冲突的位置开始,依次往后找,直到找到空槽。
  • 二次探测:直到找到空槽。

  • 双重哈希:和再哈希法类似,准备多个哈希函数,发生冲突的时候,使用另外一个哈希函数。

③、拉链法

也就是所谓的链地址法,当发生哈希冲突的时候,使用链表将冲突的元素串起来。HashMap 采用的正是拉链法

21.那扩容机制了解吗?

扩容时,HashMap 会创建一个新的数组,其容量是原数组容量的两倍。然后将键值对放到新计算出的索引位置上。一部分索引不变,另一部分索引为“原索引+旧容量”。

22.JDK 8 对 HashMap 主要做了哪些优化呢?为什么?

相比较 JDK 7,JDK 8 的 HashMap 主要做了四点优化:

①、底层数据结构由数组 + 链表改成了数组 + 链表或红黑树的结构。

原因:如果多个键映射到了同一个哈希值,链表会变得很长,在最坏的情况下,当所有的键都映射到同一个桶中时,性能会退化到 O(n),而红黑树的时间复杂度是 O(logn)。

②、链表的插入方式由头插法改为了尾插法。

原因:头插法虽然简单快捷,但扩容后容易改变原来链表的顺序。

③、扩容的时机由插入时判断改为插入后判断。

原因:可以避免在每次插入时都进行不必要的扩容检查,因为有可能插入后仍然不需要扩容。

④、优化了哈希算法。

JDK 7 进行了多次移位和异或操作来计算元素的哈希值。

JDK 8 优化了这个算法,只进行了一次异或操作,但仍然能有效地减少冲突。

24.HashMap 是线程安全的吗?多线程下会有什么问题?

HashMap 不是线程安全的,主要有以下几个问题:

①、多线程下扩容会死循环。JDK1.7 中的 HashMap 使用的是头插法插入元素,在多线程的环境下,扩容的时候就有可能导致出现环形链表,造成死循环。

不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序。

②、多线程的 put 可能会导致元素的丢失。因为计算出来的位置可能会被其他线程的 put 覆盖。本来哈希冲突是应该用链表的,但多线程时由于没有加锁,相同位置的元素可能就被干掉了。

③、put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出阈值而导致出现扩容,线程 2 此时执行 get,就有可能出现这个问题。

25.有什么办法能解决 HashMap 线程不安全的问题呢?

①、HashTable 是直接在方法上加 synchronized 关键字,比较粗暴,不再推荐使用

②、Collections.synchronizedMap 返回的是 Collectionsopen in new window 工具类的内部类

③、ConcurrentHashMap 在 JDK 7 中使用分段锁,在 JKD 8 中使用了 CAS(Compare-And-Swap)open in new windowsynchronized 关键字,性能得到进一步提升。

26.HashMap 内部节点是有序的吗?

HashMap 是无序的,根据 hash 值随机插入。如果想使用有序的 Map,可以使用 LinkedHashMap 或者 TreeMap

27.讲讲 LinkedHashMap 怎么实现有序的?

LinkedHashMap 维护了一个双向链表,有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。

28.讲讲 TreeMap 怎么实现有序的?

TreeMap 通过 key 的比较器来决定元素的顺序,如果没有指定比较器,那么 key 必须实现 Comparable 接口

TreeMap 的底层是红黑树,红黑树是一种自平衡的二叉查找树,每个节点都大于其左子树中的任何节点,小于其右子节点树种的任何节点。

插入或者删除元素时通过旋转和着色来保持树的平衡。

查找的时候通过从根节点开始,利用二叉查找树的性质,逐步向左或者右子树递归查找,直到找到目标元素

29.TreeMap 和 HashMap 的区别

①、HashMap 是基于数组+链表+红黑树实现的,put 元素的时候会先计算 key 的哈希值,然后通过哈希值计算出数组的索引,然后将元素插入到数组中,如果发生哈希冲突,会使用链表来解决,如果链表长度大于 8,会转换为红黑树。

get 元素的时候同样会先计算 key 的哈希值,然后通过哈希值计算出数组的索引,如果遇到链表或者红黑树,会通过 key 的 equals 方法来判断是否是要找的元素。

②、TreeMap 是基于红黑树实现的,put 元素的时候会先判断根节点是否为空,如果为空,直接插入到根节点,如果不为空,会通过 key 的比较器来判断元素应该插入到左子树还是右子树。

get 元素的时候会通过 key 的比较器来判断元素的位置,然后递归查找。

由于 HashMap 是基于哈希表实现的,所以在没有发生哈希冲突的情况下,HashMap 的查找效率是 O(1)。适用于查找操作比较频繁的场景。

而 TreeMap 是基于红黑树实现的,所以 TreeMap 的查找效率是 O(logn)。并且保证了元素的顺序,因此适用于需要大量范围查找或者有序遍历的场景。

Set

#30.讲讲 HashSet 的底层实现?

HashSet 其实是由 HashMap 实现的,只不过值由一个固定的 Object 对象填充,而键用于操作

实际开发中,HashSet 并不常用,比如,如果我们需要按照顺序存储一组元素,那么 ArrayList 和 LinkedList 可能更适合;如果我们需要存储键值对并根据键进行查找,那么 HashMap 可能更适合。

HashSet 主要用于去重,比如,我们需要统计一篇文章中有多少个不重复的单词,就可以使用 HashSet 来实现。

HashSet 怎么判断元素重复,重复了是否 put

HashSet 的 add 方法是通过调用 HashMap 的 put 方法实现的:所以 HashSet 判断元素重复的逻辑底层依然是 HashMap 的底层逻辑:

HashMap 在插入元素时,通常需要三步:

第一步,通过 hash 方法计算 key 的哈希值。

第二步,数组进行第一次扩容。

第三步,根据哈希值计算 key 在数组中的下标,如果对应下标正好没有存放数据,则直接插入。

如果对应下标已经有数据了,就需要判断是否为相同的 key,是则覆盖 value,否则需要判断是否为树节点,是则向树中插入节点,否则向链表中插入数据。

Java集合面试题52共有52个问题,具体问题如下: 1. Java集合框架的核心接口是什么? 2. ArrayList和LinkedList的区别是什么? 3. HashSet和TreeSet的区别是什么? 4. HashMap和Hashtable的区别是什么? 5. ConcurrentHashMap和Hashtable的区别是什么? 6. 如何实现一个线程安全的集合? 7. 如何遍历一个ArrayList? 8. 如何遍历一个LinkedList? 9. 如何遍历一个HashSet? 10. 如何遍历一个TreeSet? 11. 如何遍历一个HashMap的Key? 12. 如何遍历一个HashMap的Value? 13. 如何遍历一个HashMap的Entry? 14. 如何遍历一个Hashtable的Key? 15. 如何遍历一个Hashtable的Value? 16. 如何遍历一个Hashtable的Entry? 17. 如何遍历一个ConcurrentHashMap的Key? 18. 如何遍历一个ConcurrentHashMap的Value? 19. 如何遍历一个ConcurrentHashMap的Entry? 20. 如何使用Collections类对List进行排序? 21. 如何使用Collections类对Set进行排序? 22. 如何使用Collections类对Map的Key进行排序? 23. 如何使用Collections类对Map的Value进行排序? 24. 如何使用Comparator接口对对象进行排序? 25. 如何使用Comparable接口对对象进行排序? 26. 如何使用Iterator遍历集合? 27. 迭代器的remove()方法和集合的remove()方法有什么区别? 28. 什么是Fail-Fast机制? 29. 什么是Fail-Safe机制? 30. 如何使用ListIterator进行双向遍历? 31. 如何使用Enumeration进行遍历? 32. 如何使用Iterator进行并发修改的安全遍历? 33. 如何使用并发集合类进行并发操作? 34. 如何使用CopyOnWriteArrayList进行并发操作? 35. 如何使用CopyOnWriteArraySet进行并发操作? 36. 如何使用ConcurrentSkipListSet进行并发操作? 37. 如何使用BlockingQueue进行并发操作? 38. 如何使用LinkedBlockingQueue进行并发操作? 39. 如何使用ArrayBlockingQueue进行并发操作? 40. 如何使用PriorityBlockingQueue进行并发操作? 41. 如何使用SynchronousQueue进行并发操作? 42. 如何使用ConcurrentLinkedQueue进行并发操作? 43. 如何使用DelayQueue进行并发操作? 44. 如何使用ConcurrentHashMap进行并发操作? 45. 如何使用ConcurrentSkipListMap进行并发操作? 46. 如何使用CountDownLatch进行并发操作? 47. 如何使用CyclicBarrier进行并发操作? 48. 如何使用Semaphore进行并发操作? 49. 如何使用Exchanger进行并发操作? 50. 如何使用Lock和Condition进行并发操作? 51. 如何使用ReadWriteLock进行并发操作? 52. 如何使用AtomicInteger进行并发操作? 相关问题: 1. Java基础知识面试题有哪些? 2. Java多线程面试题有哪些? 3. Java异常处理面试题有哪些?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值