我将围绕Java集合框架的基础概念、常见集合类的特性与应用场景,以及开发中可能遇到的问题与解决方案,为你总结Java集合相关的面试题。
Java面试题总结 - Java集合篇(附答案)
一、Java集合框架是什么?说出一些集合框架的优点?
每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector、Stack、HashTable和Array。随着集合的广泛使用,Java 1.2提出了囊括所有集合接口、实现和算法的集合框架。在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久。它还包括在Java并发包中,阻塞接口以及它们的实现。
集合框架的部分优点如下:
- 降低开发成本:使用核心集合类降低开发成本,而非实现我们自己的集合类。
- 提高代码质量:随着使用经过严格测试的集合框架类,代码质量会得到提高。
- 降低维护成本:通过使用JDK附带的集合类,可以降低代码维护成本。
- 复用性和可操作性 。
二、集合框架中的泛型有什么优点?
Java 1.5引入了泛型,所有的集合接口和实现都大量地使用它。泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。
三、Java集合框架的基础接口有哪些?
- Collection:为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。
- Set:是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。
- List:是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。
- Map:是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。
- 一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。
四、为何Collection不从Cloneable和Serializable接口继承?
Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。很多Collection实现有一个公有的clone方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为Collection是一个抽象表现。重要的是实现。
当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。
在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。
五、为何Map接口不继承Collection接口?
尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承Collection毫无意义,反之亦然。
如果Map继承Collection接口,那么元素去哪儿?Map包含key - value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范。
六、Iterator是什么?
Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。
七、Enumeration和Iterator接口的区别?
- 速度和内存:Enumeration的速度是Iterator的两倍,也使用更少的内存。Enumeration是非常基础的,也满足了基础的需要。
- 安全性:与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。
- 移除元素:迭代器允许调用者从集合中移除元素,而Enumeration不能做到。为了使它的功能更加清晰,迭代器方法名已经经过改善。
八、为何没有像Iterator.add()这样的方法,向集合中添加元素?
语义不明,已知的是,Iterator的协议不能确保迭代的次序。然而要注意,ListIterator没有提供一个add操作,它要确保迭代的顺序。
九、为何迭代器没有一个方法可以直接获取下一个元素,而不需要移动游标?
它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。
十、Iterater和ListIterator之间有什么区别?
- 遍历范围:我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
- 遍历方向:Iterator只可以向前遍历,而LIstIterator可以双向遍历。
- 额外功能:ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
十一、遍历一个List有哪些不同的方式?
List<String> strList = new ArrayList<>();
//使用for - each循环
for(String obj : strList){
System.out.println(obj);
}
//using iterator
Iterator<String> it = strList.iterator();
while(it.hasNext()){
String obj = it.next();
System.out.println(obj);
}
使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。
十二、通过迭代器fail - fast属性,你明白了什么?
每次我们尝试获取下一个元素的时候,Iterator fail - fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail - fast来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。
十三、fail - fast与fail - safe有什么区别?
- 作用方式:Iterator的fail - fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail - fast的,而java.util.concurrent中的集合类都为fail - safe的。
- 异常抛出:Fail - fast迭代器抛出ConcurrentModificationException,而fail - safe迭代器从不抛出ConcurrentModificationException。
十四、在迭代一个集合的时候,如何避免ConcurrentModificationException?
- 同步操作:在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException。例如使用CopyOnWriteArrayList来替换ArrayList。
- 使用迭代器的remove方法:边遍历边修改Collection的唯一正确方式是使用Iterator.remove()方法。
十五、怎么确保一个集合不能被修改?
可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.lang.UnsupportedOperationException异常。
示例代码如下:
List list = new ArrayList<>();
list.add("x");
Collection clist = Collections.unmodifiableCollection(list);
clist.add("y"); // 运行时此行报错
System.out.println(list.size());
十六、ArrayList和LinkedList的区别是什么?
- 底层数据结构:ArrayList是基于数组实现的,LinkedList是基于双向链表实现的。
- 随机访问性能:ArrayList支持快速随机访问,时间复杂度为O(1),因为它可以通过数组索引直接访问元素。而LinkedList的随机访问性能较差,时间复杂度为O(n),因为它需要遍历链表来找到指定位置的元素。
- 插入和删除性能:在ArrayList中,插入和删除元素可能需要移动大量元素,尤其是在数组中间位置进行操作时,时间复杂度为O(n)。而LinkedList的插入和删除操作只需要修改相邻节点的引用,时间复杂度为O(1),但如果要找到指定位置的节点,仍然需要O(n)的时间。
- 内存占用:ArrayList的内存占用相对较小,因为它只需要存储元素本身。而LinkedList每个节点除了存储元素外,还需要存储两个引用(指向前一个和后一个节点),因此内存占用较大。
十七、HashMap和HashTable的区别是什么?
- 线程安全性:HashMap是非线程安全的,HashTable是线程安全的,HashTable使用了synchronized关键字来保证线程安全。
- null键值对:HashMap的key和value都允许为null,但是HashTable的key和value都不允许为null。
- 性能:由于HashTable是线程安全的,所以在单线程环境下,HashMap的性能要高于HashTable。
- 初始容量和扩容机制:HashMap的初始容量为16,扩容每次都是2的n次幂。HashTable的初始容量为11,扩容是原来的2倍加1。
十八、HashSet和TreeSet的区别是什么?
- 底层数据结构:HashSet基于HashMap实现,使用哈希表存储元素。TreeSet基于红黑树实现,元素按照自然顺序或自定义顺序排序。
- 元素顺序:HashSet中的元素是无序的,插入顺序和存储顺序不一定相同。而TreeSet中的元素是有序的,可以按照自然顺序(如数字从小到大、字符串按字典序)或通过实现Comparator接口来定义自定义顺序。
- 添加和查询性能:HashSet的添加和查询操作平均时间复杂度为O(1),因为它使用哈希表进行快速查找。TreeSet的添加和查询操作时间复杂度为O(log n),因为红黑树的高度是对数级别的。
- 唯一性保证:HashSet通过hashCode()和equals()方法来保证元素的唯一性。TreeSet通过比较元素的顺序来保证唯一性,如果两个元素比较相等,则不会同时存在于TreeSet中。
十九、ConcurrentHashMap的实现原理是什么?
- JDK 1.7:在JDK 1.7中,ConcurrentHashMap采用分段锁机制,它将数据分成多个段(Segment),每个段都有自己的锁。当一个线程访问某个段时,不会影响其他段的访问,从而提高了并发性能。每个Segment类似于一个小型的HashMap,包含一个数组和链表。
- JDK 1.8:在JDK 1.8中,ConcurrentHashMap摒弃了分段锁机制,采用CAS(Compare and Swap)操作和Synchronized关键字相结合的方式。它的底层数据结构和HashMap类似,也是数组 + 链表 + 红黑树。在进行插入、删除、查询等操作时,首先通过CAS操作尝试更新数据,如果失败,则使用Synchronized关键字对节点进行加锁,以保证线程安全。
二十、如何遍历HashMap和HashTable?
遍历HashMap
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("one", 1);
hashMap.put("two", 2);
// 使用entrySet遍历
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
// 使用keySet遍历
for (String key : hashMap.keySet()) {
System.out.println(key + " : " + hashMap.get(key));
}
遍历HashTable
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("one", 1);
hashtable.put("two", 2);
// 使用entrySet遍历
for (Map.Entry<String, Integer> entry : hashtable.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
// 使用keySet遍历
for (String key : hashtable.keySet()) {
System.out.println(key + " : " + hashtable.get(key));
}
二十一、如何实现一个线程安全的List?
- 使用Vector:Vector是Java早期提供的一个线程安全的List实现,它的方法都使用了synchronized关键字进行同步。但是由于同步机制的开销,性能相对较低。
- 使用Collections.synchronizedList():可以通过Collections类的synchronizedList()方法将一个普通的List转换为线程安全的List。
List<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);
- 使用CopyOnWriteArrayList:CopyOnWriteArrayList是Java并发包提供的一个线程安全的List实现。它的原理是在进行写操作(如添加、删除元素)时,会先复制一个新的数组,在新数组上进行操作,操作完成后再将原数组引用指向新数组。读操作则直接读取原数组,这样读操作和写操作可以并发进行,并且读操作不会受到写操作的影响。
二十二、如何实现一个线程安全的Set?
- 使用Collections.synchronizedSet():通过Collections类的synchronizedSet()方法将一个普通的Set转换为线程安全的Set。
Set<String> set = new HashSet<>();
Set<String> synchronizedSet = Collections.synchronizedSet(set);
- 使用CopyOnWriteArraySet:CopyOnWriteArraySet是Java并发包提供的一个线程安全的Set实现,它内部是基于CopyOnWriteArrayList实现的。它的特点和CopyOnWriteArrayList类似,写操作时复制数组,读操作直接读取原数组,保证了线程安全。
二十三、如何实现一个线程安全的Map?
- 使用HashTable:HashTable是一个线程安全的Map实现,它的方法都使用了synchronized关键字进行同步,但是性能较低。
- 使用Collections.synchronizedMap():通过Collections类的synchronizedMap()方法将一个普通的Map转换为线程安全的Map。
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(map);
- 使用ConcurrentHashMap:ConcurrentHashMap是Java并发包提供的高效线程安全的Map实现,在JDK 1.7中采用分段锁机制,JDK 1.8中采用CAS和Synchronized相结合的方式,具有较高的并发性能。
二十四、如何在Java中实现LRU缓存?
LRU(Least Recently Used)缓存即最近最少使用缓存。可以使用LinkedHashMap来实现LRU缓存。LinkedHashMap维护了一个双向链表,记录了元素的访问顺序。
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
在上述代码中,通过继承LinkedHashMap并重写removeEldestEntry方法,当缓存大小超过设定的capacity时,自动移除最久未使用的元素。
二十五、如何在Java中实现FIFO队列?
可以使用Queue接口的实现类,如ArrayDeque或LinkedList来实现FIFO(先进先出)队列。
使用ArrayDeque实现FIFO队列
import java.util.ArrayDeque;
import java.util.Queue;
public class FIFOQueueExample {
public static void main(String[] args) {
Queue<Integer> queue = new ArrayDeque<>();
queue.add(1);
queue.add(2);
queue.add(3);
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
使用LinkedList实现FIFO队列
import java.util.LinkedList;
import java.util.Queue;
public class FIFOQueueExample {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
queue.add(3);
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
在这两个例子中,add
方法用于向队列中添加元素,poll
方法用于从队列头部取出元素,从而实现了FIFO的特性。
二十六、如何在Java中实现优先级队列?
可以使用PriorityQueue类来实现优先级队列。PriorityQueue是一个基于堆数据结构的无界优先级队列,默认情况下,元素按照自然顺序进行排序,也可以通过传入自定义的Comparator来实现自定义排序。
import java.util.PriorityQueue;
public class PriorityQueueExample {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.add(3);
priorityQueue.add(1);
priorityQueue.add(2);
while (!priorityQueue.isEmpty()) {
System.out.println(priorityQueue.poll());
}
}
}
在上述示例中,元素按照从小到大的顺序被取出。如果需要自定义排序,可以创建一个实现Comparator接口的类,并在创建PriorityQueue时传入该类的实例。
二十七、如何在Java中实现栈?
在 Java 里实现栈有两种比较常见的方法:一是借助 Java 集合框架中的Stack类,二是利用Deque接口的实现类,像ArrayDeque。下面为你详细介绍这两种实现方式。
方法一:使用 Java 内置的Stack类
Java 提供了java.util.Stack类,它继承自Vector,能用来实现栈的基本功能。下面是一个简单的示例:
```java
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
// 创建一个栈对象
Stack stack = new Stack<>();
// 入栈操作
stack.push("Java");
stack.push("Python");
stack.push("C++");
// 出栈操作
System.out.println("出栈元素: " + stack.pop()); // 输出: C++
// 获取栈顶元素
System.out.println("栈顶元素: " + stack.peek()); // 输出: Python
// 判断栈是否为空
System.out.println("栈是否为空: " + stack.isEmpty()); // 输出: false
// 获取栈的大小
System.out.println("栈的大小: " + stack.size()); // 输出: 2
}
}
...
方法二:使用Deque接口(推荐做法)
虽然Stack类使用起来很方便,但由于它是线程安全的,在单线程环境下会带来一些性能损耗。所以,更推荐使用Deque接口的实现类,例如ArrayDeque。
```java
import java.util.Deque;
import java.util.ArrayDeque;
public class DequeStackExample {
public static void main(String[] args) {
// 创建一个使用ArrayDeque实现的栈
Deque stack = new ArrayDeque<>();
// 入栈操作
stack.push("Apple");
stack.push("Banana");
stack.push("Cherry");
// 出栈操作
System.out.println("出栈元素: " + stack.pop()); // 输出: Cherry
// 获取栈顶元素
System.out.println("栈顶元素: " + stack.peek()); // 输出: Banana
// 判断栈是否为空
System.out.println("栈是否为空: " + stack.isEmpty()); // 输出: false
// 获取栈的大小
System.out.println("栈的大小: " + stack.size()); // 输出: 2
}
}
...
方法三:自定义栈类(基于数组)
你还可以基于数组来实现一个简单的栈,这有助于你更深入地理解栈的底层原理。
```java
public class CustomStack {
private T[] array;
private int top;
private int capacity;
@SuppressWarnings("unchecked")
public CustomStack(int capacity) {
this.capacity = capacity;
this.array = (T[]) new Object[capacity];
this.top = -1;
}
// 入栈操作
public void push(T element) {
if (isFull()) {
throw new RuntimeException("栈已满");
}
array[++top] = element;
}
// 出栈操作
public T pop() {
if (isEmpty()) {
throw new RuntimeException("栈为空");
}
return array[top--];
}
// 获取栈顶元素
public T peek() {
if (isEmpty()) {
throw new RuntimeException("栈为空");
}
return array[top];
}
// 判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
// 判断栈是否已满
public boolean isFull() {
return top == capacity - 1;
}
// 获取栈的大小
public int size() {
return top + 1;
}
public static void main(String[] args) {
CustomStack<Integer> stack = new CustomStack<>(3);
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println(stack.pop()); // 输出: 30
System.out.println(stack.peek()); // 输出: 20
}
}
...
总结
推荐做法:在实际开发中,建议优先选用Deque接口的实现类,如ArrayDeque,它的性能表现更优。
学习用途:如果是想学习栈的底层实现原理,那么基于数组或链表自定义栈类是个不错的选择。
线程安全:要是你的应用场景涉及多线程环境,可以考虑使用ConcurrentLinkedDeque或者对Deque进行同步包装。
Java 集合,面试题,ArrayList,HashMap,LinkedList,HashSet,ConcurrentHashMap, 集合框架,List,Set,Map,Collection,Vector,TreeMap,HashTable
代码获取方式
https://pan.quark.cn/s/14fcf913bae6