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. List,Set,Map三者的区别?
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 比较适合顺序添加、随机访问的场景。
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. 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述
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
-
优化序列化性能:ArrayList
中的 elementData
数组往往比实际存储的元素多。这是因为 ArrayList
会预分配一些空间,以减少数组扩展的次数。如果直接序列化 elementData
,会导致序列化数据中包含许多无用的空元素,浪费空间和带宽。通过将 elementData
声明为 transient
,可以避免序列化这些空元素。
-
自定义序列化逻辑:ArrayList
提供了自定义的序列化和反序列化逻辑,通过实现 writeObject
和 readObject
方法来控制具体的序列化行为:
- 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. HashSet与HashMap的区别
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. HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
在 Java 中,保存数据有两种比较简单的数据结构:数组和链表。 数组的特点是:寻址容易,插入和
删除困难;链表的特点是:寻址困难,但插入和删除容易; 所以我们将数组和链表结合在一起,发
挥两者各自的优势,使用一种叫做 拉链法 的方式可以解决哈希冲突。
HashMap JDK1.8 之前
JDK1.8 之前采用的是拉链法。 拉链法 :将链表和数组相结合。也就是说创建一个链表数组,数组
中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 HashMap JDK1.8 之后
相比于之前的版本, jdk1.8 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8 )
时,将链表转化为红黑树,以减少搜索时间。
31. 什么是红黑树
说道红黑树先讲什么是二叉树
二叉树简单来说就是 每一个节上可以关联俩个子节点
红黑树
红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红
(Red) 或黑 (Black) 。
红黑树的每个结点是黑色或者红色。不管怎么样他的根结点是黑色。每个叶子结点(叶子结点
代表终结、结尾的节点)也是黑色 [ 注意:这里叶子结点,是指为空 (NIL 或 NULL) 的叶子结点! ] 。
如果一个结点是红色的,则它的子结点必须是黑色的。
每个结点到叶子结点 NIL 所经过的黑色结点的个数一样的。 [ 确保没有一条路径会比其他路径长出俩
倍,所以红黑树是相对接近平衡的二叉树的! ]
红黑树的基本操作是 添加、删除 。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么
呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面
三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新
成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
大概就是这样子:
32. HashMap的put方法的具体流程?
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
就会触发扩容操作。
扩容操作的步骤
-
计算新容量: 新容量通常是当前容量的两倍。新的容量需要是2的幂次,以便散列函数的性能和分布均匀性。
-
创建新表: 创建一个新的更大容量的数组来替代旧的数组。
-
重新散列旧表中的元素: 遍历旧表中的每个元素,并将其重新散列到新表中的相应位置。这个步骤是必要的,因为扩容后,元素在数组中的位置可能会改变。
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. 为什么HashMap中String、Integer这样的包装类适合作为K?
答: String 、 Integer 等包装类的特性能够保证 Hash 值的不可更改性和计算准确性,能够有效的减
少 Hash 碰撞的几率
都是 final 类型,即不可变性,保证 key 的不可更改性,不会存在获取 hash 值不同的情况
内部已重写了 equals() 、 hashCode() 等方法,遵守了 HashMap内部的规范不容易出现 Hash 值计算错误的情况;
37. 如果使用Object作为HashMap的Key,应该怎么办呢?
答:重写 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.List、Set、Map的区别
List集合有序、可重复的单例集合。
Set集合无序、不可重复的单例集合。
Map集合无序、k不可重复,v可重复的双例集合。
2.List、Set、Map常用集合有哪些?
List
vector: 底层是数组,方法加了synchronized来保证线程安全,所以效率较慢,使用ArrayList替代。
ArrayList: 线程不安全,底层是数组,因为数组都是连续的地址,所以查询比较快。增删比较慢,增会生成一个新数组,把新增的元素和原有元素放到新数组中,删除会导致元素移动,所以增删速度较慢。
LinkedList: 线程不安全,底层是链表,因为地址不是连续的,都是一个节点和一个节点相连,每次查询都得重头开始查询,所以查询慢,增删只是断裂某个节点对整体影响不大,所以增删速度较快。
Set
HashSet: 底层是哈希表(数组+链表或数组+红黑树),在链表长度大于8时转为红黑树,在红黑树节点小于6时转为链表。其实就是实现了HashMap,值存入key,value是一个final修饰的对象。
TreeSet: 底层是红黑树结构,就是TreeMap实现,可以实现有序的集合。String和Integer可以根据值进行排序。如果是对象需要实现Comparator接口,重写compareTo()方法制定比较规则。
LinkedHashSet: 实现了HashSet,多一条链表来记录位置,所以是有序的。
Map<key,value>双例结构
TreeMap: 底层是红黑树,key可以按顺序排列。
HashMap: 底层是哈希表,可以很快的储存和检索,无序,大量迭代情况不佳。
LinkedHashMap: 底层是哈希表+链表,有序,大量迭代情况佳。
3.ArrayList的初始容量是多少?扩容机制是什么?扩容过程是怎样?
初始容量: 默认10,也可以通过构造方法传入大小。
扩容机制: 原数组长度 + 原数组长度/2(源码中是原数组右移一位,也就相当于除以2)
注意:扩容后的ArrayList底层数组不是原来的数组。
扩容过程: 因为ArrayList底层是数组,所以它的扩容机制和数组一样,首先新建一个新数组,长度是原数组的1.5倍,然后调用Arrays.copyof()复制原数组的值,然后赋值给新数组。
4.什么是哈希表
根据关键码值(Key value)而直接进行访问的数据结构,在一个表中,通过H(key)计算出key在表中的位置,H(key)就是哈希函数,表就是哈希表。
5.什么是哈希冲突
不同的key通过哈希函数计算出相同的储存地址,这就是哈希冲突。
6.解决哈希冲突
(1)开放地址法
如果发生哈希冲突,就会以当前地址为基准,再去寻找计算另一个位置,直到不发生哈希冲突。
寻找的方法有:① 线性探测 1,2,3,m
② 二次探测 1的平方,-1的平方,2的平方,-2的平方,k的平方,-k的平方,k<=m/2
③ 随机探测 生成一个随机数,然后从随机地址+随机数++。
(2)链地址法
冲突的哈希值,连到到同一个链表上。
(3)再哈希法(再散列方法)
多个哈希函数,发生冲突,就在用另一个算计,直到没有冲突。
(4)建立公共溢出区
哈希表分成基本表和溢出表,与基本表发生冲突的都填入溢出表。
7.HashMap的hash()算法,为什么不是h=key.hashcode(),而是key.hashcode()^ (h>>>16)
得到哈希值然后右移16位,然后进行异或运算,这样使哈希值的低16位也具有了一部分高16位的特性,增加更多的变化性,减少了哈希冲突。
8.为什么HashMap的初始容量和扩容都是2的次幂
因为计算元素存储的下标是(n-1)&哈希值,数组初始容量-1,得到的二进制都是1,这样可以减少哈希冲突,可以更好的均匀插入。
9.HashMap如果指定了不是2的次幂的容量会发生什么?
会获得一个大于指定的初始值的最接近2的次幂的值作为初始容量。
10.HashMap为什么线程不安全
jdk1.7中因为使用头插法,再扩容的时候,可能会造成闭环和数据丢失。
jdk1.8中使用尾插法,不会出现闭环和数据丢失,但是在多线程下,会发生数据覆盖。(put操作中,在putVal函数里) 值的覆盖还有长度的覆盖。
11.解决Hashmap的线程安全问题
(1)使用Hashtable解决,在方法加同步关键字,所以效率低下,已经被弃用。
(2)使用Collections.synchronizedMap(new HashMap<>()),不常用。
(3)ConcurrentHashMap(常用)
12.ConcurrentHashMap的原理
jdk1.7: 采用分段锁,是由Segment(继承ReentrantLock:可重入锁,默认是16,并发度是16)和HashEntry内部类组成,每一个Segment(锁)对应1个HashEntry(key,value)数组,数组之间互不影响,实现了并发访问。
jdk1.8: 抛弃分段锁,采用CAS(乐观锁)+synchronized实现更加细粒度的锁,Node数组+链表+红黑树结构。只要锁住链表的头节点(树的根节点),就不会影响其他数组的读写,提高了并发度。
13.为什么用synchronized代替ReentrantLock
①节省内存开销。ReentrantLock基于AQS来获得同步支持,但不是每个节点都需要同步支持,只有链表头节点或树的根节点需要同步,所以使用ReentrantLock会带来很大的内存开销。
②获得jvm支持,可重入锁只是api级别,而synchronized是jvm直接支持的,能够在jvm运行时做出相应的优化。
③在jdk1.6之后,对synchronized做了大量的优化,而且有多种锁状态,会从 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
AQS (Abstract Queued Synchronizer): 一个抽象的队列同步器,通过维护一个共享资源状态( Volatile Int State )和一个先进先出( FIFO )的线程等待队列来实现一个多线程访问共享资源的同步框架。
14.HashMap为什么使用链表
减少和解决哈希冲突,把冲突的值放在同一链表下。
15.HashMap为什么使用红黑树
当数据过多,链表遍历较慢,所以引入红黑树。
16.HashMap为什么不一上来就使用红黑树
维护成本较大,红黑树在插入新的数据后,可能会进行变色、左旋、右旋来保持平衡,所以当数据少时,就不需要红黑树。
17.说说你对红黑树的理解
①根节点是黑色。
②节点是黑色或红色。
③叶子节点是黑色。
④红色节点的子节点都是黑色。
⑤从任意节点到其子节点的所有路径都包含相同数目的黑色节点。
红黑树从根到叶子节点的最长路径不会超过最短路径的2倍。保证了红黑树的高效。
18.为什么链表长度大于8,并且表的长度大于64的时候,链表会转换成红黑树?
因为链表长度越长,哈希冲突概率就越小,当链表等于8时,哈希冲突就非常低了,是千万分之一,我们的map也不会存那么多数据,如果真要存那么多数据,那就转为红黑树,提高查询和插入的效率。
19.为什么转成红黑树是8呢?而重新转为链表阈值是6呢?
因为如果都是8的话,那么会频繁转换,会浪费资源。
20.为什么负载因子是0.75?
加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;
加载因子越小,填满的元素越少,冲突发生的机会减小,但空间浪费了更多了,而且还会提高扩容rehash操作的次数。
“冲突的机会”与“空间利用率”之间,寻找一种平衡与折中。
又因为根据泊松分布,当负载因子是0.75时,平均值时0.5,带入可得,当链表为8时,哈希冲突发生概率就很低了。
21.什么时候会扩容?
元素个数 > 数组长度 * 负载因子 例如 16 * 0.75 = 12,当元素超过12个时就会扩容。
链表长度大于8并且表长小于64,也会扩容
22.为什么不是满了扩容?
因为元素越多,空间利用率是高了,但是发生哈希冲突的几率也增加了。
23.扩容过程
jdk1.7: 会生成一个新table,重新计算每个节点放进新table,因为是头插法,在线程不安全的时候,可能会出现闭环和数据丢失。
jdk1.8: 会生成一个新table,新位置只需要看(e.hash & oldCap)结果是0还是1,0就放在旧下标,1就是旧下标+旧数组长度。避免了对每个节点进行hash计算,大大提高了效率。e.hash是数组的hash值,,oldCap是旧数组的长度。
24.HashMap和Hashtable的区别
①HashMap,允许key和value为null,Hashtable不允许为null。
②HashMap线程不安全,Hashtable线程安全。
25.集合为什么要用迭代器(Iterator)
更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
如果不用迭代器,只能for循环,还必须知道集合的数据结构,复用性不强。