Java 集合篇面试题全面总结及答案解析

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 本文总结了Java集合框架的核心概念、常见集合类的特性与应用场景,以及开发中可能遇到的问题与解决方案。内容涵盖集合框架的基础接口(如Collection、Set、List、Map)、泛型的优点、线程安全集合类(如ConcurrentHashMap、CopyOnWriteArrayList)、常见集合类的区别(如ArrayList与LinkedList、HashMap与HashTable)等。此外,还详细介绍了如何实现LRU缓存、FIFO队列、优先级队列及栈等数据结构,并提供了相关代码示例。通过本文,读者可以全面掌握Java集合相关的面试知识点及其实际应用技巧。

我将围绕Java集合框架的基础概念、常见集合类的特性与应用场景,以及开发中可能遇到的问题与解决方案,为你总结Java集合相关的面试题。

Java面试题总结 - Java集合篇(附答案)

一、Java集合框架是什么?说出一些集合框架的优点?

每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector、Stack、HashTable和Array。随着集合的广泛使用,Java 1.2提出了囊括所有集合接口、实现和算法的集合框架。在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久。它还包括在Java并发包中,阻塞接口以及它们的实现。
集合框架的部分优点如下:

  1. 降低开发成本:使用核心集合类降低开发成本,而非实现我们自己的集合类。
  2. 提高代码质量:随着使用经过严格测试的集合框架类,代码质量会得到提高。
  3. 降低维护成本:通过使用JDK附带的集合类,可以降低代码维护成本。
  4. 复用性和可操作性

二、集合框架中的泛型有什么优点?

Java 1.5引入了泛型,所有的集合接口和实现都大量地使用它。泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。

三、Java集合框架的基础接口有哪些?

  1. Collection:为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。
  2. Set:是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。
  3. List:是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。
  4. Map:是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。
  5. 一些其它的接口有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接口的区别?

  1. 速度和内存:Enumeration的速度是Iterator的两倍,也使用更少的内存。Enumeration是非常基础的,也满足了基础的需要。
  2. 安全性:与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。
  3. 移除元素:迭代器允许调用者从集合中移除元素,而Enumeration不能做到。为了使它的功能更加清晰,迭代器方法名已经经过改善。

八、为何没有像Iterator.add()这样的方法,向集合中添加元素?

语义不明,已知的是,Iterator的协议不能确保迭代的次序。然而要注意,ListIterator没有提供一个add操作,它要确保迭代的顺序。

九、为何迭代器没有一个方法可以直接获取下一个元素,而不需要移动游标?

它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。

十、Iterater和ListIterator之间有什么区别?

  1. 遍历范围:我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
  2. 遍历方向:Iterator只可以向前遍历,而LIstIterator可以双向遍历。
  3. 额外功能: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有什么区别?

  1. 作用方式:Iterator的fail - fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail - fast的,而java.util.concurrent中的集合类都为fail - safe的。
  2. 异常抛出:Fail - fast迭代器抛出ConcurrentModificationException,而fail - safe迭代器从不抛出ConcurrentModificationException。

十四、在迭代一个集合的时候,如何避免ConcurrentModificationException?

  1. 同步操作:在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException。例如使用CopyOnWriteArrayList来替换ArrayList。
  2. 使用迭代器的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的区别是什么?

  1. 底层数据结构:ArrayList是基于数组实现的,LinkedList是基于双向链表实现的。
  2. 随机访问性能:ArrayList支持快速随机访问,时间复杂度为O(1),因为它可以通过数组索引直接访问元素。而LinkedList的随机访问性能较差,时间复杂度为O(n),因为它需要遍历链表来找到指定位置的元素。
  3. 插入和删除性能:在ArrayList中,插入和删除元素可能需要移动大量元素,尤其是在数组中间位置进行操作时,时间复杂度为O(n)。而LinkedList的插入和删除操作只需要修改相邻节点的引用,时间复杂度为O(1),但如果要找到指定位置的节点,仍然需要O(n)的时间。
  4. 内存占用:ArrayList的内存占用相对较小,因为它只需要存储元素本身。而LinkedList每个节点除了存储元素外,还需要存储两个引用(指向前一个和后一个节点),因此内存占用较大。

十七、HashMap和HashTable的区别是什么?

  1. 线程安全性:HashMap是非线程安全的,HashTable是线程安全的,HashTable使用了synchronized关键字来保证线程安全。
  2. null键值对:HashMap的key和value都允许为null,但是HashTable的key和value都不允许为null。
  3. 性能:由于HashTable是线程安全的,所以在单线程环境下,HashMap的性能要高于HashTable。
  4. 初始容量和扩容机制:HashMap的初始容量为16,扩容每次都是2的n次幂。HashTable的初始容量为11,扩容是原来的2倍加1。

十八、HashSet和TreeSet的区别是什么?

  1. 底层数据结构:HashSet基于HashMap实现,使用哈希表存储元素。TreeSet基于红黑树实现,元素按照自然顺序或自定义顺序排序。
  2. 元素顺序:HashSet中的元素是无序的,插入顺序和存储顺序不一定相同。而TreeSet中的元素是有序的,可以按照自然顺序(如数字从小到大、字符串按字典序)或通过实现Comparator接口来定义自定义顺序。
  3. 添加和查询性能:HashSet的添加和查询操作平均时间复杂度为O(1),因为它使用哈希表进行快速查找。TreeSet的添加和查询操作时间复杂度为O(log n),因为红黑树的高度是对数级别的。
  4. 唯一性保证:HashSet通过hashCode()和equals()方法来保证元素的唯一性。TreeSet通过比较元素的顺序来保证唯一性,如果两个元素比较相等,则不会同时存在于TreeSet中。

十九、ConcurrentHashMap的实现原理是什么?

  1. JDK 1.7:在JDK 1.7中,ConcurrentHashMap采用分段锁机制,它将数据分成多个段(Segment),每个段都有自己的锁。当一个线程访问某个段时,不会影响其他段的访问,从而提高了并发性能。每个Segment类似于一个小型的HashMap,包含一个数组和链表。
  2. 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?

  1. 使用Vector:Vector是Java早期提供的一个线程安全的List实现,它的方法都使用了synchronized关键字进行同步。但是由于同步机制的开销,性能相对较低。
  2. 使用Collections.synchronizedList():可以通过Collections类的synchronizedList()方法将一个普通的List转换为线程安全的List。
List<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);
  1. 使用CopyOnWriteArrayList:CopyOnWriteArrayList是Java并发包提供的一个线程安全的List实现。它的原理是在进行写操作(如添加、删除元素)时,会先复制一个新的数组,在新数组上进行操作,操作完成后再将原数组引用指向新数组。读操作则直接读取原数组,这样读操作和写操作可以并发进行,并且读操作不会受到写操作的影响。

二十二、如何实现一个线程安全的Set?

  1. 使用Collections.synchronizedSet():通过Collections类的synchronizedSet()方法将一个普通的Set转换为线程安全的Set。
Set<String> set = new HashSet<>();
Set<String> synchronizedSet = Collections.synchronizedSet(set);
  1. 使用CopyOnWriteArraySet:CopyOnWriteArraySet是Java并发包提供的一个线程安全的Set实现,它内部是基于CopyOnWriteArrayList实现的。它的特点和CopyOnWriteArrayList类似,写操作时复制数组,读操作直接读取原数组,保证了线程安全。

二十三、如何实现一个线程安全的Map?

  1. 使用HashTable:HashTable是一个线程安全的Map实现,它的方法都使用了synchronized关键字进行同步,但是性能较低。
  2. 使用Collections.synchronizedMap():通过Collections类的synchronizedMap()方法将一个普通的Map转换为线程安全的Map。
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(map);
  1. 使用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


相关文章
|
1月前
|
Java
Java的CAS机制深度解析
CAS(Compare-And-Swap)是并发编程中的原子操作,用于实现多线程环境下的无锁数据同步。它通过比较内存值与预期值,决定是否更新值,从而避免锁的使用。CAS广泛应用于Java的原子类和并发包中,如AtomicInteger和ConcurrentHashMap,提升了并发性能。尽管CAS具有高性能、无死锁等优点,但也存在ABA问题、循环开销大及仅支持单变量原子操作等缺点。合理使用CAS,结合实际场景选择同步机制,能有效提升程序性能。
|
19天前
|
机器学习/深度学习 JSON Java
Java调用Python的5种实用方案:从简单到进阶的全场景解析
在机器学习与大数据融合背景下,Java与Python协同开发成为企业常见需求。本文通过真实案例解析5种主流调用方案,涵盖脚本调用到微服务架构,助力开发者根据业务场景选择最优方案,提升开发效率与系统性能。
170 0
|
15天前
|
Java 大数据 API
Java Stream API:现代集合处理与函数式编程
Java Stream API:现代集合处理与函数式编程
176 100
|
15天前
|
Java API 数据处理
Java Stream API:现代集合处理新方式
Java Stream API:现代集合处理新方式
184 101
|
15天前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
305 100
|
28天前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
10天前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
19天前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
19天前
|
安全 Java API
Java SE 与 Java EE 区别解析及应用场景对比
在Java编程世界中,Java SE(Java Standard Edition)和Java EE(Java Enterprise Edition)是两个重要的平台版本,它们各自有着独特的定位和应用场景。理解它们之间的差异,对于开发者选择合适的技术栈进行项目开发至关重要。
91 1
|
2月前
|
存储 缓存 安全
Java集合框架(二):Set接口与哈希表原理
本文深入解析Java中Set集合的工作原理及其实现机制,涵盖HashSet、LinkedHashSet和TreeSet三大实现类。从Set接口的特性出发,对比List理解去重机制,并详解哈希表原理、hashCode与equals方法的作用。进一步剖析HashSet的底层HashMap实现、LinkedHashSet的双向链表维护顺序特性,以及TreeSet基于红黑树的排序功能。文章还包含性能对比、自定义对象去重、集合运算实战和线程安全方案,帮助读者全面掌握Set的应用与选择策略。
163 23