常见 JAVA 集合面试题整理 自用版持续更新

简介: 这是一份详尽的Java集合面试题总结,涵盖ArrayList与LinkedList、HashMap与HashTable、HashSet与TreeSet的区别,以及ConcurrentHashMap的实现原理。内容从底层数据结构、性能特点到应用场景逐一剖析,并提供代码示例便于理解。此外,还介绍了如何遍历HashMap和HashTable。无论是初学者还是进阶开发者,都能从中受益。代码资源可从[链接](https://pan.quark.cn/s/14fcf913bae6)获取。

我整合了多个技术平台上的相关内容,从常见问题入手,结合应用实例,为你梳理出这篇Java集合面试题总结,希望能助你学习一臂之力。

常见JAVA集合面试题(自用整理,持续更新)

一、ArrayList和LinkedList的区别

1. 底层数据结构

  • ArrayList:基于动态数组实现。在创建ArrayList时,如果没有指定初始容量,会默认创建一个容量为10的数组。当数组中的元素数量超过数组容量时,会进行扩容操作,新的容量一般是原容量的1.5倍。例如:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
// 当继续添加元素时,如果当前容量不足,会自动扩容
  • LinkedList:基于双向链表实现。每个节点不仅存储了元素本身,还包含了指向前一个节点和后一个节点的引用。比如:
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("a");
linkedList.add("b");
// 链表节点之间通过引用相互连接

2. 随机访问性能

  • ArrayList:支持快速随机访问,时间复杂度为O(1)。因为可以通过数组索引直接定位到元素的位置。例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
int element = list.get(1); // 可以快速获取索引为1的元素,即20
  • LinkedList:随机访问性能较差,时间复杂度为O(n)。因为需要从链表的头节点或尾节点开始,逐个遍历节点,直到找到目标位置的节点。例如:
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(10);
linkedList.add(20);
linkedList.add(30);
// 获取索引为1的元素,需要从链表头开始遍历
int element = linkedList.get(1);

3. 插入和删除性能

  • ArrayList:在数组中间位置插入或删除元素时,需要移动大量元素,时间复杂度为O(n)。但在数组尾部插入或删除元素时,时间复杂度为O(1)(不考虑扩容情况)。例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 在索引为1的位置插入元素4,后续元素都要向后移动
list.add(1, 4);
  • LinkedList:在链表的任意位置插入或删除元素,只需要修改相邻节点的引用,时间复杂度为O(1)。但如果要先定位到指定位置的节点,时间复杂度则为O(n)。例如:
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
// 在索引为1的位置插入元素4,只需修改相邻节点引用
linkedList.add(1, 4);

4. 内存占用

  • ArrayList:内存占用相对较小,主要存储元素本身。
  • LinkedList:每个节点除了存储元素,还需要存储两个引用,内存占用较大。

二、HashMap和HashTable的区别

1. 线程安全性

  • HashMap:非线程安全,在多线程环境下并发访问可能会出现数据不一致问题,甚至引发死循环(在JDK 1.7及之前的版本中,多线程扩容时可能会出现)。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
// 多线程同时进行put操作,可能会出现问题
new Thread(() -> {
   
    hashMap.put(1, "a");
}).start();
new Thread(() -> {
   
    hashMap.put(2, "b");
}).start();
  • HashTable:线程安全,它的大部分方法(如put、get等)都使用了synchronized关键字进行同步。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
// 多线程环境下使用相对安全
new Thread(() -> {
   
    hashtable.put(1, "a");
}).start();
new Thread(() -> {
   
    hashtable.put(2, "b");
}).start();

2. Null键和Null值

  • HashMap:允许null键和null值。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(null, "nullValue");
hashMap.put(1, null);
  • HashTable:不允许null键和null值,否则会抛出NullPointerException。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
// 以下代码会抛出NullPointerException
// hashtable.put(null, "value"); 
// hashtable.put(1, null);

3. 迭代器

  • HashMap:迭代器是fail - fast的,即当迭代过程中集合结构被修改(除了通过迭代器自身的remove方法),会立即抛出ConcurrentModificationException。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
Iterator<Map.Entry<Integer, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
   
    Map.Entry<Integer, String> entry = iterator.next();
    // 这里如果直接调用hashMap.put(3, "c");会抛出异常
    iterator.remove();
}
  • HashTable:迭代器不是fail - fast的,在迭代过程中集合结构被修改,不会立即抛出异常,但可能导致迭代结果不准确。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "a");
hashtable.put(2, "b");
Enumeration<Map.Entry<Integer, String>> enumeration = Collections.enumeration(hashtable.entrySet());
while (enumeration.hasMoreElements()) {
   
    Map.Entry<Integer, String> entry = enumeration.nextElement();
    // 这里如果调用hashtable.put(3, "c");不会立即抛出异常
}

4. 默认容量和扩容机制

  • HashMap:默认初始容量为16,负载因子为0.75。当HashMap中的元素个数超过容量 负载因子(即16 0.75 = 12)时,会进行扩容,新容量为原容量的2倍。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
for (int i = 0; i < 13; i++) {
   
    hashMap.put(i, "value" + i);
    // 当添加到第13个元素时,会触发扩容
}
  • HashTable:默认初始容量为11,负载因子为0.75。当HashTable中的元素个数超过容量 负载因子(即11 0.75,取整后为8)时,会进行扩容,新容量为原容量的2倍 + 1(即11 * 2 + 1 = 23)。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
for (int i = 0; i < 9; i++) {
   
    hashtable.put(i, "value" + i);
    // 当添加到第9个元素时,会触发扩容
}

三、HashSet和TreeSet的区别

1. 底层数据结构

  • HashSet:基于HashMap实现,内部使用HashMap来存储元素,HashSet的元素实际上是作为HashMap的键来存储的,值是一个固定的Object对象。例如:
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(1);
hashSet.add(2);
// 内部通过HashMap存储,键为1和2,值为固定对象
  • TreeSet:基于红黑树实现,红黑树是一种自平衡的二叉搜索树,能保证元素的有序性。例如:
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(3);
treeSet.add(1);
treeSet.add(2);
// 内部通过红黑树存储,元素会按照从小到大的顺序排列

2. 元素顺序

  • HashSet:元素无序,元素的插入顺序和存储顺序不一定相同。例如:
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(3);
hashSet.add(1);
hashSet.add(2);
// 遍历hashSet时,元素顺序可能不是3, 1, 2
for (int i : hashSet) {
   
    System.out.print(i + " ");
}
  • TreeSet:元素有序,可以按照自然顺序(如数字从小到大、字符串按字典序)或通过实现Comparator接口来定义自定义顺序。例如:
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(3);
treeSet.add(1);
treeSet.add(2);
// 遍历treeSet时,元素顺序是1, 2, 3
for (int i : treeSet) {
   
    System.out.print(i + " ");
}

3. 添加和查询性能

  • HashSet:添加和查询操作平均时间复杂度为O(1),因为使用哈希表进行快速查找。例如:
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(1);
boolean contains = hashSet.contains(1); // 快速判断是否包含元素1,时间复杂度O(1)
  • TreeSet:添加和查询操作时间复杂度为O(log n),因为红黑树的高度是对数级别的。例如:
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
boolean contains = treeSet.contains(1); // 判断是否包含元素1,时间复杂度O(log n)

4. 唯一性保证

  • HashSet:通过hashCode()和equals()方法来保证元素的唯一性。当向HashSet中添加元素时,首先会计算元素的hashCode值,根据hashCode值确定元素在哈希表中的存储位置。如果该位置已经有元素存在,再通过equals()方法比较两个元素是否相等,如果相等则不添加。例如:
class CustomObject {
   
    private int id;
    public CustomObject(int id) {
   
        this.id = id;
    }
    @Override
    public int hashCode() {
   
        return id;
    }
    @Override
    public boolean equals(Object obj) {
   
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        CustomObject other = (CustomObject) obj;
        return id == other.id;
    }
}
HashSet<CustomObject> hashSet = new HashSet<>();
hashSet.add(new CustomObject(1));
hashSet.add(new CustomObject(1)); // 由于hashCode和equals方法判断为相同元素,不会重复添加
  • TreeSet:通过比较元素的顺序来保证唯一性,如果两个元素比较相等(通过自然顺序或自定义比较器判断),则不会同时存在于TreeSet中。例如:
class CustomComparator implements Comparator<Integer> {
   
    @Override
    public int compare(Integer o1, Integer o2) {
   
        return o1 - o2;
    }
}
TreeSet<Integer> treeSet = new TreeSet<>(new CustomComparator());
treeSet.add(1);
treeSet.add(1); // 由于比较器判断为相同元素,不会重复添加

四、ConcurrentHashMap的实现原理

1. JDK 1.7版本

  • 分段锁机制:ConcurrentHashMap将数据分成多个段(Segment),每个段相当于一个小的HashTable,都有自己独立的锁。在JDK 1.7中,默认有16个Segment。当一个线程对某个Segment进行写操作时,只会锁住这个Segment,其他线程仍然可以访问其他Segment,从而提高并发性能。例如:
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
// 多个线程可以同时对不同的Segment进行操作
new Thread(() -> {
   
    concurrentHashMap.put(1, "a");
}).start();
new Thread(() -> {
   
    concurrentHashMap.put(17, "b"); // 17和1可能位于不同的Segment,可并发操作
}).start();
  • 数据结构:每个Segment内部是一个数组 + 链表的结构,和HashMap类似。当发生哈希冲突时,通过链表来解决。例如:
// 假设某个Segment的数组长度为4
Segment<K, V> segment = new Segment<>(16, 0.75f);
// 当有元素put进来时,如果哈希冲突,会在链表上进行存储

2. JDK 1.8版本

  • CAS + Synchronized:摒弃了分段锁机制,采用CAS操作和Synchronized关键字相结合的方式。在JDK 1.8中,当多个线程进行读操作时,不需要加锁,可以直接并发进行。当进行写操作(如put)时,首先会尝试使用CAS操作来插入元素,如果CAS操作失败,再使用Synchronized关键字对当前节点进行加锁。例如:
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
// 多个线程读操作可并发
new Thread(() -> {
   
    String value = concurrentHashMap.get(1);
}).start();
new Thread(() -> {
   
    String value = concurrentHashMap.get(2);
}).start();
// 写操作先尝试CAS,失败后加锁
new Thread(() -> {
   
    concurrentHashMap.put(3, "c");
}).start();
  • 数据结构:和HashMap类似,采用数组 + 链表 + 红黑树的结构。当链表长度超过8时,会将链表转换为红黑树,以提高查找效率。例如:
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
// 当某个桶中的链表长度超过8时,会转换为红黑树
for (int i = 0; i < 10; i++) {
   
    concurrentHashMap.put(i, "value" + i);
    // 假设这些元素哈希冲突,都在同一个桶中,当添加到第9个元素时,链表可能转换为红黑树
}

五、如何遍历HashMap和HashTable

1. 遍历HashMap

(1)使用entrySet()遍历键值对

HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
for (Map.Entry<Integer, String> entry : hashMap.entrySet()) {
   
    Integer key = entry.getKey();
    String value = entry.getValue();
    System.out.println("Key: " + key + ", Value: " + value);
}

(2)使用keySet()遍历键,再通过键获取值

HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
for (Integer key : hashMap.keySet()) {
   
    String value = hashMap.get(key);
    System.out.println("Key: " + key + ", Value: " + value);
}

(3)使用values()遍历值(只能获取值,无法获取键)

HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
for (String value : hashMap.values()) {
   
    System.out.println("Value: " + value);
}

(4)使用迭代器遍历

HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
Iterator<Map.Entry<Integer, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
   
    Map.Entry<Integer, String> entry = iterator.next();
    Integer key = entry.getKey();
    String value = entry.getValue();
    System.out.println("Key: " + key + ", Value: " + value);
}

2. 遍历HashTable

(1)使用entrySet()遍历键值对

Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "a");
hashtable.put(2, "b");
for (Map.Entry<Integer, String> entry : hashtable.entrySet()) {
   
    Integer key = entry.getKey();
    String value = entry.getValue();
    System.out.println("Key: " + key + ", Value: " + value);
}

(2)使用keySet()遍历键,再通过键获取值

Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "a");
hashtable.put(2, "b");
for (Integer key : hashtable.keySet()) {
   
    String value = hashtable.get(key);
    System.out.println("Key: " + key + ", Value: " + value);
}

Java 集合,ArrayList,HashMap,LinkedList,HashSet,ConcurrentHashMap, 集合框架,List,Set,Map,Collection,Vector,TreeMap,LinkedHashSet,Queue



代码获取方式
https://pan.quark.cn/s/14fcf913bae6


相关文章
|
5月前
|
Java 大数据 API
Java Stream API:现代集合处理与函数式编程
Java Stream API:现代集合处理与函数式编程
323 100
|
5月前
|
Java API 数据处理
Java Stream API:现代集合处理新方式
Java Stream API:现代集合处理新方式
342 101
|
5月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
4月前
|
存储 算法 安全
Java集合框架:理解类型多样性与限制
总之,在 Java 题材中正确地应对多样化与约束条件要求开发人员深入理解面向对象原则、范式编程思想以及JVM工作机理等核心知识点。通过精心设计与周密规划能够有效地利用 Java 高级特征打造出既健壮又灵活易维护系统软件产品。
143 7
|
6月前
|
存储 缓存 安全
Java集合框架(二):Set接口与哈希表原理
本文深入解析Java中Set集合的工作原理及其实现机制,涵盖HashSet、LinkedHashSet和TreeSet三大实现类。从Set接口的特性出发,对比List理解去重机制,并详解哈希表原理、hashCode与equals方法的作用。进一步剖析HashSet的底层HashMap实现、LinkedHashSet的双向链表维护顺序特性,以及TreeSet基于红黑树的排序功能。文章还包含性能对比、自定义对象去重、集合运算实战和线程安全方案,帮助读者全面掌握Set的应用与选择策略。
422 23
|
5月前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
6月前
|
安全 Java 开发者
Java集合框架:详解Deque接口的栈操作方法全集
理解和掌握这些方法对于实现像浏览器后退功能这样的栈操作来说至关重要,它们能够帮助开发者编写既高效又稳定的应用程序。此外,在多线程环境中想保证线程安全,可以考虑使用ConcurrentLinkedDeque,它是Deque的线程安全版本,尽管它并未直接实现栈操作的方法,但是Deque的接口方法可以相对应地使用。
359 12
|
6月前
|
存储 缓存 安全
Java集合框架(三):Map体系与ConcurrentHashMap
本文深入解析Java中Map接口体系及其实现类,包括HashMap、ConcurrentHashMap等的工作原理与线程安全机制。内容涵盖哈希冲突解决、扩容策略、并发优化,以及不同Map实现的适用场景,助你掌握高并发编程核心技巧。
|
7月前
|
Java 数据库连接 数据库
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
本文全面总结了Java核心知识点,涵盖基础语法、面向对象、集合框架、并发编程、网络编程及主流框架如Spring生态、MyBatis等,结合JVM原理与性能优化技巧,并通过一个学生信息管理系统的实战案例,帮助你快速掌握Java开发技能,适合Java学习与面试准备。
336 2
Java 相关知识点总结含基础语法进阶技巧及面试重点知识