- Java 集合
- Java 集合概览
- 1. List, Set, Queue, Map 四者的区别?
- 2. ArrayList 和 Array(数组)的区别?
- 3. ArrayList 和 Vector 的区别?
- 4. Vector 和 Stack 的区别?(了解即可)
- 5. ArrayList 可以添加 null 值吗?
- 6. ArrayList 插入和删除元素的时间复杂度?
- 7. LinkedList 插入和删除元素的时间复杂度?
- 8. LinkedList 为什么不能实现 RandomAccess 接口?
- 9. ArrayList 与 LinkedList 区别?
- 10. 说一说 ArrayList 的扩容机制
- 11. Comparable 和 Comparator 的区别
- 12. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
- 13. Queue 与 Deque 的区别
- 14. ArrayDeque 与 LinkedList 的区别
- 15. 说一说 PriorityQueue
- 16. 什么是 BlockingQueue?
- 17. BlockingQueue 的实现类有哪些?
- 18. ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
- 19. HashMap 和 Hashtable 的区别
- 20. HashMap 和 HashSet 区别
- 21. HashMap 和 TreeMap 区别
- 22. HashSet 如何检查重复?
- 23. HashMap 的底层实现
- 24. HashMap 的长度为什么是 2 的幂次方
- 25. HashMap 多线程操作导致死循环问题
- 26. HashMap 为什么线程不安全?
- 27. HashMap 常见的遍历方式?
- 28. ConcurrentHashMap 和 Hashtable 的区别
- 29. JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
- 30. ConcurrentHashMap 为什么 key 和 value 不能为 null?
- 31. ConcurrentHashMap 能保证复合操作的原子性吗?
- 32. ArrayList 扩容机制分析
- 33. HashMap 扩容机制分析
- 34. CopyOnWriteArrayList 简介
Java 集合
Java 集合概览
答:
Java 集合,也叫作容器,主要是由两大接口派生而来
Collection
接口Map
接口
Java 集合框架如下图所示:
1. List, Set, Queue, Map 四者的区别?
答:
- List:存储的元素是有序的、可重复的。
- Set:存储的元素是无序的、不可重复的。
- Queue:存储的元素可重复,元素先进先出。
- Map:使用键值对(key-value)存储元素。key 是无序的、不可重复的
2. ArrayList 和 Array(数组)的区别?
答:
ArrayList
支持动态的扩容,Array被创建后不能改变长度了。ArrayList
只能存储对象类型,Array可以存储对象类型和基本数据类型。
3. ArrayList 和 Vector 的区别?
答:
- ArrayList线程不安全 。
- Vector线程安全 。
4. Vector 和 Stack 的区别?(了解即可)
答:
- Vector 和 Stack 两者都是线程安全的,都是使用
synchronized
关键字进行同步处理。 - Stack 继承自 Vector,是一个先进后出的栈,而 Vector 是一个列表。
注意: Vector 和 Stack 已经被淘汰,推荐使用并发集合类(例如 ConcurrentHashMap
、CopyOnWriteArrayList
)
5. ArrayList 可以添加 null 值吗?
答:
- 可以
6. ArrayList 插入和删除元素的时间复杂度?
答:
插入:
- 头部插入:O(n)
- 尾部插入:如果容量未到极限O(1),到达极限O(n)
- 指定位置插入:O(n)
删除:
- 头部删除:O(n)
- 尾部删除:O(1)
- 指定位置删除:O(n)
7. LinkedList 插入和删除元素的时间复杂度?
答:
插入:
- 头部插入:O(1)
- 尾部插入:O(1)
- 指定位置插入:O(n)
删除:
- 头部删除:O(1)
- 尾部删除:O(1)
- 指定位置删除:O(n)
8. LinkedList 为什么不能实现 RandomAccess 接口?
答:
RandomAccess
是一个标记接口,用来表明实现该接口的类支持随机访问
(即可以通过索引快速访问元素)。- 由于 LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现
RandomAccess
接口
9. ArrayList 与 LinkedList 区别?
答:
- ArrayList 底层使用的是
Object 数组
,因为内存地址连续,可以实现随机访问。ArrayList会预留一定的空间容量。 - LinkedList 底层使用的是
双向链表
(JDK1.6 之前为循环链表),不支持随机访问。LinkedList存储的每个元素要比ArrayList的多(因为要存放后继和前驱节点)。
10. 说一说 ArrayList 的扩容机制
答:
11. Comparable 和 Comparator 的区别
答:
Comparable
接口和Comparator
接口都是 Java 中用于排序的接口。- Comparable:
- 要排序的对象实现
Comparable
接口,重写compareTo
方法
- 要排序的对象实现
- Comparator:
- 通常需要使用匿名内部类来实现
Comparator
接口,并重写其中的compare
方法
- 通常需要使用匿名内部类来实现
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
12. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
答:
相同点:
- 他们都是
set
接口的实现类,都能保证元素的唯一性,都不是线程安全的。
不同点:
底层实现的数据结构不一样。
HashSet
底层使用的是HashMap
实现的。LinkedHashSet
的底层数据结构是链表和哈希表
TreeSet
底层是红黑树
实现的。
底层实现的数据结构不一样,又会影响他们的使用场景不一样。
LinkedHashSet
能够保证插入的元素顺序。TreeSet
支持自定义排序规则
13. Queue 与 Deque 的区别
答:
Queue
是单端队列,只能从一端插入元素,另一端删除元素。Deque
是双端队列,在队列的两端均可以插入或删除元素。可以模拟栈
14. ArrayDeque 与 LinkedList 的区别
答:
ArrayDeque
和 LinkedList
都实现了 Deque 接口,两者都具有队列的功能
区别:
ArrayDeque
是基于可变长的数组和双指针来实现,而LinkedList
则通过链表来实现。ArrayDeque
不支持存储NULL
数据,但LinkedList
支持。ArrayDeque
插入时可能存在扩容,LinkedList
插入时需要申请堆空间。
15. 说一说 PriorityQueue
答:
PriorityQueue
优先级队列,实现了 Queue
接口。总是优先级最高的元素先出队。
特点:
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据。PriorityQueue
插入元素和删除堆顶元素的时间复杂度为O(logn)
。PriorityQueue
是非线程安全的,且不支持存储NULL
和non-comparable
的对象。PriorityQueue
默认是小顶堆,但可以接收一个Comparator
作为构造参数,从而来自定义元素优先级。
16. 什么是 BlockingQueue?
答:
BlockingQueue
(阻塞队列)是一个接口,继承自 Queue
。
- 当要取队列中的元素时,如果队列为空,则会阻塞等待,直到有元素为止。
- 当要将元素放入队列时,如果队列满了,则会阻塞等待,直到有其他元素出队列。
BlockingQueue 常用于生产者-消费者模型中,生产者线程会向队列中添加数据,而消费者线程会从队列中取出数据进行处理。
17. BlockingQueue 的实现类有哪些?
答:
Java 中常用的阻塞队列实现类有以下几种:
ArrayBlockingQueue
:使用数组实现的有界阻塞队列。在创建时需要指定容量大小,并支持公平和非公平两种方式的锁访问机制。LinkedBlockingQueue
:使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定则默认为Integer.MAX_VALUE。和ArrayBlockingQueue不同的是, 它仅支持非公平的锁访问机制。PriorityBlockingQueue
:支持优先级排序的无界阻塞队列。元素必须实现Comparable接口
或者在构造函数中传入Comparator对象
,并且不能插入null
元素。SynchronousQueue
:同步队列,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。因此,SynchronousQueue通常用于线程之间的直接传递数据。DelayQueue
:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。
18. ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
答:
- 底层实现:ArrayBlockingQueue 基于数组实现,而 LinkedBlockingQueue 基于链表实现。
- 是否有界:ArrayBlockingQueue 是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue 创建时可以不指定容量大小,默认是
Integer.MAX_VALUE
,也就是无界的。但也可以指定队列大小,从而成为有界的。 - 锁是否分离: ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue中的锁是分离的,即生产用的是
putLock
,消费是takeLock
,这样可以防止生产者和消费者线程之间的锁争夺。
19. HashMap 和 Hashtable 的区别
答:
HashMap
不是线程安全,Hashtable
线程安全HashMap
的 key 和 value 可以存储 null 值。Hashtable
的 key 和 value 不可以存储 null 值- JDK8 之后,
HashMap
的底层数据结构发生了变化(当链表长度大于阈值(默认为8
)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64
,那么会选择先进行数组扩容,而不是转换为红黑树)),Hashtable
的底层数据结构是数组 + 链表。
20. HashMap 和 HashSet 区别
答:
- HashSet 的底层实现就是
HashMap
。 - HashSet 实例化时,内部其实就是创建了一个
HashMap
。 - 调用 HashSet 的 add()方法,就是执行
HashMap
的put ()
方法。
21. HashMap 和 TreeMap 区别
答:
- TreeMap 实现了
NavigableMap
接口和SortedMap
接口。 - 相比于 HashMap ,多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。
NavigableMap
接口提供了丰富的方法来搜索和操作键值对,这些方法是基于红黑树的属性实现的,保证了搜索操作的时间复杂度为O(log n)
22. HashSet 如何检查重复?
答:
- 当一个元素加入
HashSet
时,首先会根据hashcode
生成对应的哈希码,确定元素在哈希表中的位置。 - 当对应位置没有元素时,则会加入到该位置。
- 如果该位置已经有元素了,则会调用
equals()
方法比较对象的属性是否相同。如果相同则认为是重复元素,则加入失败。 - 如果对象属性不相同,则会加入到对应位置的链表或是红黑树中。
23. HashMap 的底层实现
答:
哈希流程:
HashMap
首先获得 key 的hashcode
,之后再经过扰动函数(hash函数)处理过后得到 hash 值(再经过hash函数是为了减少碰撞冲突)- 然后通过
(n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度) - 如果当前位置存在元素的话,就通过
eaquls()
判断key是否相同。相同的话,直接覆盖,不相同通过拉链法解决冲突。
JDK 8之前
- HashMap 底层是数组和链表
JDK 8之后
- 当链表长度大于阈值(默认为
8
)(将链表转换成红黑树前会判断,如果当前数组的长度小于64
,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 - 为什么使用红黑树?: 红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
24. HashMap 的长度为什么是 2 的幂次方
答:
- 提高运算速度。当长度为 2 的幂次方时,
hash%length
与hash&(length-1)
等价,但是位操作效率更高。 - 减少冲突
25. HashMap 多线程操作导致死循环问题
答:
- JDK 8之前,HashMap的链表是采取头插法,再多线程情况下可能导致形成环形链表,使查询陷入死循环。
- JDK 8之后,采用尾插法,避免形成环形链表。
- 但是在并发环境下,推荐使用
ConcurrentHashMap
。
26. HashMap 为什么线程不安全?
答:
- 比如有2个线程同时进行
put
操作。 - 线程1要插入的元素经过哈希后,确定了要插入的位置。并且此时时间片执行完毕。
- 线程2获得CPU,线程2同时也要插入对应的位置,线程2插入对应的位置。
- 线程1获得CPU后,会直接插入到该位置,那么就会覆盖线程2的数据。
27. HashMap 常见的遍历方式?
答:
- 迭代器(Iterator)方式遍历
- For Each 方式遍历
- Streams API 遍历
28. ConcurrentHashMap 和 Hashtable 的区别
答:
实现线程安全的方式不同:
Hashtable
使用的是全局锁,任何时刻只能有一个线程进行操作。在并发环境下,效率是非常低。- JDK 8之前的
ConcurrentHashMap
:分段的数组 + 链表
实现的,锁的是每一个段,每个段就相当于是一个Hashtable。- 在并发场景下,多线程访问不同的段,就不会存在锁竞争。
- JDK 8之后的
ConcurrentHashMap
:Node 数组 + 链表 / 红黑树
实现的,采用CAS + synchronized
来保证并发安全。synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发
29. JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
答:
- 线程安全实现方式:JDK 1.7 采用
Segment
分段锁来保证安全, Segment 是继承自ReentrantLock
。JDK1.8 采用Node + CAS + synchronized
保证线程安全,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点。 - Hash 碰撞解决方法:JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树。
- 并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是
16
。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
30. ConcurrentHashMap 为什么 key 和 value 不能为 null?
答:
- 避免二义性,如果 key 和 value 可以为null,那么在并发场景下,不知道这个值本身就是 null,还是其他线程修改的null。
31. ConcurrentHashMap 能保证复合操作的原子性吗?
答:
- 可以使用
ConcurrentHashMap
提供的原子性复合操作。 - 如
putIfAbsent
、compute
、computeIfAbsent
、computeIfPresent
、merge
等
32. ArrayList 扩容机制分析
答:
- 当添加元素到
ArrayList
时,它会检查当前元素数量是否已经超过了当前容量(内部通过 capacity 属性记录容量),如果超过了,则会进行扩容操作。 - 当 ArrayList 需要扩容时,它会创建一个新的更大的数组,通常是原来容量的 1.5 倍。然后将旧的元素复制到新的数组中。
33. HashMap 扩容机制分析
答:
- HashMap 默认的初始化大小为
16
。之后每次扩充,容量变为原来的2 倍
。 - HashMap 总是使用 2 的幂作为哈希表的大小。
34. CopyOnWriteArrayList 简介
答:
CopyOnWriteArrayList
是并发安全的 List- 采用了
写时复制
(Copy-On-Write) 的策略 - 即在进行修改操作(add,set、remove)的时候,先创建底层数组的副本,对副本数组进行修改
- 修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。