0. 前言:
程序员面试本是一件再平常不过的事情,记得刚毕业的时候面试题背的滚瓜烂熟。但是在职程序员面试却是另一回事了,我们往往没有太多时间复习,特别是大龄程序员,工作日忙于工作,周末还要照顾家庭,一旦面临被优化的风险就很被动,难以在短时间内复习并找到工作。不要问我是怎么知道的,都是切身体会,在复习的过程中我也走了不少弯路,所幸最终结果令自己满意。
为了不让和我一样的程序员遇到同样的问题,我打算写这一系列的文章,这些文章不会像其他面经一般大而全,这些文章仅记录我在复习过程中认为重要的知识点,如果能帮助到你就太好了。
1. Java有哪些顶层集合接口
一句话回答:Java集合框架主要包含四个顶层接口:Collection
、List
、Set
和 Map
。
细节解释:
- Collection:是最基本的集合接口,它是一个扩展自
Iterable
接口的根接口。它定义了集合的基本操作,如添加、删除和遍历元素。 - List:继承自
Collection
接口,是一个有序的集合,允许存储重复元素。List
接口提供了一些额外的操作,如按索引访问元素、添加元素到特定位置等。 - Set:同样继承自
Collection
接口,是一个不允许存储重复元素的集合。Set
接口主要用于存储唯一元素,并且没有定义元素的顺序。 - Map:是一个与
Collection
接口并列的接口,用于存储键值对。它不是集合的一部分,但通常与集合一起使用,因为它可以作为集合的属性存储键值对。Map
接口提供了键值对的存储和检索功能。
以下是上述接口常见的实现类:
-
List接口下的实现类(继承自
Collection
):ArrayList
:基于动态数组实现,提供快速随机访问。适合随机访问频繁的场景。LinkedList
:基于链表实现,提供快速的插入和删除操作。适合插入和删除操作频繁的场景。Vector
:类似于ArrayList
,但它是线程安全的。Stack
:继承自Vector
,实现了栈的功能,LIFO(后进先出)。
-
Set接口下的实现类(继承自
Collection
):HashSet
:基于HashMap
实现,提供快速查找和插入操作,但不允许重复元素。LinkedHashSet
:类似于HashSet
,但维护元素的插入顺序。TreeSet
:基于NavigableMap
实现,可以按照自然顺序或自定义顺序对元素进行排序。
-
Map接口下的实现类:
HashMap
:基于哈希表实现,提供快速的键值对查找和插入操作。键值对无序。LinkedHashMap
:类似于HashMap
,但维护插入顺序或访问顺序。TreeMap
:基于红黑树实现,可以按照键的自然顺序或自定义顺序对键值对进行排序。Hashtable
:类似于HashMap
,但它是线程安全的,不允许空键和空值。
这些实现类提供了不同的性能特性和功能,开发者可以根据具体需求选择合适的集合类。
下文将挑选几个个人认为常考,但是平时容易疏于学习的类介绍。
2. Vector 和Hashtable如何保证线程安全
一句话回答:Vector
和 Hashtable
都是通过同步机制来实现线程安全的集合类。
细节解释:
-
Vector:
Vector
类似于ArrayList
,但它是同步的。这意味着它的方法被synchronized
关键字修饰,从而在多线程环境中,每次只有一个线程可以访问这些方法。- 这种同步机制虽然确保了线程安全,但也带来了性能开销,特别是在高并发的情况下。因此,在单线程或低并发的场景下,使用
ArrayList
可能更合适。 - 示例代码:
Vector<String> vector = new Vector<>(); vector.add("Java"); vector.add("Python");
-
Hashtable:
Hashtable
类似于HashMap
,但它是线程安全的。Hashtable
通过在构造函数中传入一个synchronized
参数来实现同步。- 所有公共方法和内部迭代器都被
synchronized
关键字修饰,确保了在多线程环境中的线程安全。 - 由于同步机制,
Hashtable
的性能通常不如HashMap
。另外,Hashtable
不允许空键和空值,这也是与HashMap
的一个区别。 - 示例代码:
Hashtable<String, String> hashtable = new Hashtable<>(); hashtable.put("Java", "JVM"); hashtable.put("Python", "CPython");
这两种类通过同步机制确保了在多线程环境中的线程安全,但同时也牺牲了一些性能。在需要线程安全且并发需求不高的场景下,可以考虑使用它们。对于高并发的场景,推荐使用 Collections.synchronizedList
或 Collections.synchronizedMap
包装原始集合,或者使用 ConcurrentHashMap
等并发集合类。
3. ConcurrentHashMap原理
一句话回答:ConcurrentHashMap
通过分段锁(Segmentation)和细粒度锁(Fine-grained locking)来实现高并发环境下的线程安全。
细节解释:
ConcurrentHashMap
是 Java 并发包 java.util.concurrent
中的一个线程安全的哈希表实现。以下是它的一些关键原理:
- 分段锁(Segmentation):
- 在
ConcurrentHashMap
的早期版本(如 Java 5 和 Java 6)中,它使用分段锁来实现线程安全。这意味着哈希表被分为多个段(Segment),每个段可以独立地被锁定。这样,当一个线程在操作一个段时,其他线程可以同时操作其他段,从而提高了并发性能。
- 在
- 细粒度锁(Fine-grained locking):
- 从 Java 7 开始,
ConcurrentHashMap
采用了细粒度锁的策略。它不再使用分段锁,而是使用内部的Node
数组来存储键值对。每个Node
可以被视为一个锁,当进行修改操作时,只需要锁定涉及到的那个Node
,而不是整个哈希表。
- 从 Java 7 开始,
- 哈希算法优化:
ConcurrentHashMap
使用了一些优化的哈希算法来减少哈希碰撞,从而减少链表的长度,提高查找效率。
- 数据结构:
- 当哈希表中的元素数量达到一定阈值时,
ConcurrentHashMap
会将链表转换为红黑树,以保持操作的对数时间复杂度。
- 当哈希表中的元素数量达到一定阈值时,
- volatile 变量:
ConcurrentHashMap
使用volatile
关键字来保证变量的可见性,确保线程间对共享变量的修改能够立即被其他线程看到。
- 原子类:
- 它使用
java.util.concurrent.atomic
包中的原子类(如AtomicInteger
)来支持无锁的线程安全操作。
- 它使用
- CAS 操作:
ConcurrentHashMap
在内部使用compare-and-swap
(CAS)操作来实现无锁的更新,进一步提高了并发性能。
ConcurrentHashMap
通过这些机制实现了高效的并发访问,适用于高并发的多线程环境。它提供了接近 HashMap
的性能,同时保持了线程安全性。
4. TreeMap原理
一句话回答:TreeMap
是基于红黑树实现的有序映射表,它保证数据按照自然顺序或自定义顺序进行排序。
细节解释:
TreeMap
是 Java 中 Map
接口的一个实现,它内部使用红黑树(一种自平衡二叉搜索树)来存储键值对。以下是 TreeMap
的一些关键原理:
- 红黑树结构:
- 红黑树是一种特殊的二叉搜索树,它通过颜色和特定的规则来保持树的平衡,确保树的高度大致为对数级别,从而提供快速的查找、插入和删除操作。
- 排序保证:
TreeMap
保证所有的键都会按照自然顺序(实现Comparable
接口的键)或通过构造函数提供的Comparator
来排序。
- 自平衡特性:
- 红黑树的自平衡特性确保了即使在大量插入和删除操作后,树的查找效率仍然保持在对数时间复杂度。
- 旋转操作:
- 红黑树通过左旋和右旋操作来调整树的结构,以保持其平衡。这些旋转操作是红黑树插入和删除过程中的关键步骤。
- 颜色标记:
- 每个节点都有红色或黑色两种颜色标记。红黑树通过颜色标记和特定的规则来确保树的平衡。
- 插入和删除过程:
- 当插入或删除节点时,
TreeMap
会检查红黑树的平衡性,并执行必要的旋转操作来恢复平衡。
- 当插入或删除节点时,
- 迭代器:
TreeMap
提供了两种类型的迭代器:KeySet
迭代器和EntrySet
迭代器。这些迭代器可以按照键的排序顺序遍历键值对。
- 性能特点:
- 由于红黑树的自平衡特性,
TreeMap
在插入、删除和查找操作上都具有较好的性能,时间复杂度为 O(log n)。
- 由于红黑树的自平衡特性,
TreeMap
适用于需要有序映射的场景,例如实现有序的键值存储、实现有序集合等。它的有序特性使得它在处理排序数据时非常有用。
5. HashSet原理
一句话回答:HashSet
是基于 HashMap
实现的,它通过哈希表来存储唯一的元素,并保证元素的唯一性。
细节解释:
HashSet
是 Java 中 Set
接口的一个实现,其内部使用 HashMap
来存储元素。以下是 HashSet
的一些关键原理:
- 基于HashMap:
HashSet
实际上是一个HashMap
的封装,其中HashMap
的键是HashSet
存储的元素,而值是一个固定的虚拟对象。
- 元素唯一性:
- 由于
HashMap
的键是唯一的,所以HashSet
能够保证存储的元素也是唯一的。
- 由于
- 哈希函数:
HashSet
使用元素的hashCode()
方法来计算哈希值,然后根据这个哈希值将元素存储在HashMap
的不同位置。
- 冲突解决:
- 当两个元素的哈希值相同时,
HashSet
会使用HashMap
的机制来解决冲突,例如链表或红黑树。
- 当两个元素的哈希值相同时,
- 性能特点:
HashSet
的基本操作(如添加、删除和查找)通常具有常数时间复杂度 O(1),这是通过哈希表的快速查找特性实现的。
- 迭代器:
HashSet
提供迭代器来遍历集合中的元素,迭代器的顺序是不确定的,因为HashMap
不保证元素的顺序。
- 空元素:
HashSet
允许存储一个null
元素,但只能存储一个。
- 容量和加载因子:
HashSet
可以选择初始容量和加载因子,这些参数会影响HashMap
的性能和内存使用。
- 扩容机制:
- 当
HashSet
存储的元素数量超过当前容量和加载因子的乘积时,HashMap
会进行扩容操作,重新计算所有元素的哈希值并重新分配位置。
- 当
HashSet
是实现集合去重的常用数据结构,适用于需要快速查找和插入的场景,同时保持元素的唯一性。
6. 集合类的扩容原理
一句话回答:Java 中的集合类,如 ArrayList
、HashSet
和 HashMap
,通过动态扩容机制来适应不断增长的元素数量,它们在达到一定阈值时会增加容量。
细节解释:
- ArrayList 的扩容原理:
ArrayList
基于动态数组实现。当添加元素时,如果当前数组已满,ArrayList
会创建一个新的数组,其容量是原始容量的1.5倍(newCapacity = oldCapacity + (oldCapacity >> 1)
)。- 然后将旧数组中的元素复制到新数组中。这种机制确保了
ArrayList
在添加元素时的摊销时间复杂度为 O(1)。
- HashMap 的扩容原理:
HashMap
基于哈希表实现。当元素数量超过容量与加载因子(capacity * load factor
)的乘积时,HashMap
会进行扩容。- 扩容包括创建一个新的哈希表,其容量通常是原始容量的两倍,并将所有元素重新映射到新哈希表上。这个过程称为“rehashing”。
- HashSet 的扩容原理:
HashSet
内部使用HashMap
来存储元素,因此其扩容原理与HashMap
类似。当元素数量达到阈值时,HashSet
会触发HashMap
的扩容机制。
- LinkedList 的扩容原理:
LinkedList
基于双向链表实现,它没有固定的扩容机制。由于链表的特性,添加元素通常只需要 O(1) 时间复杂度,不需要像数组那样进行复制。
- 扩容的影响:
- 扩容操作是昂贵的,因为它涉及到创建新的内部数据结构和复制现有元素。因此,合理地选择初始容量和加载因子可以减少扩容的次数,提高性能。
- 加载因子:
- 加载因子是一个衡量
HashMap
填充程度的参数,它影响HashMap
进行扩容的时机。加载因子越小,HashMap
在元素数量增加时扩容越频繁,但查找效率可能更高。
- 加载因子是一个衡量
集合类的扩容原理是为了在动态数据集中保持操作效率,同时避免过度使用内存。合理地选择初始容量和加载因子可以优化性能和内存使用。