Java 集合框架:List、Set、Map 全家桶详解

学习日期:2026-06-16

难度:⭐⭐⭐⭐ 核心

前言

Java 集合可以说是日常开发中用得最多的 API,没有之一。存数据、遍历、查找、排序……处处都有它的身影。

但集合家族成员众多,初学者很容易懵:ArrayList 还是 LinkedList?HashMap 为什么线程不安全?HashSet 和 HashMap 是什么关系?

这篇把 Java 集合框架全貌理清楚,每个常用集合的特点、底层原理、使用场景一次说透。

一、集合框架总览

Java 集合主要分两大阵营:CollectionMap

Iterable

Collection

┌─────────────┼─────────────┐

│ │ │

List Set Queue

│ │ │

┌──────┴──────┐ ┌──┴────┐ ┌───┴──────┐

│ │ │ │ │ │ │

ArrayList LinkedList HashSet TreeSet ArrayDeque PriorityQueue

| | |

| | └─ LinkedHashSet

| |

Vector Stack(已废弃)

Map

┌─────────────┼─────────────┐

│ │ │

HashMap TreeMap Hashtable(老)

│ │

└─ LinkedHashMap

|

└─ ConcurrentHashMap(并发)

核心区别:

表格

接口 特点 元素顺序 允许重复 允许 null
List 有序列表 按插入顺序
Set 不重复集合 取决于实现 取决于实现
Queue 队列 FIFO / 优先级 取决于实现
Map 键值对 取决于实现 Key 不重复 取决于实现

二、List 家族

2.1 ArrayList——动态数组(最常用)

底层原理:Object 数组,默认初始容量 10,不够时扩容 1.5 倍。

// 创建

List<String> list = new ArrayList<>();

List<String> list2 = new ArrayList<>(100); // 指定初始容量(推荐)

// 增删改查

list.add("Java"); // 末尾添加

list.add(0, "Python"); // 指定位置插入

list.get(0); // 获取元素

list.set(0, "Go"); // 修改元素

list.remove(0); // 按索引删

list.remove("Java"); // 按元素删(删第一个匹配的)

// 其他常用

list.size(); // 大小

list.isEmpty(); // 是否为空

list.contains("Java"); // 是否包含

list.indexOf("Java"); // 首次出现位置

list.lastIndexOf("Java"); // 最后出现位置

list.clear(); // 清空

遍历方式

// 1. for-i 循环(可以操作索引)

for (int i = 0; i < list.size(); i++) {

System.out.println(list.get(i));

}

// 2. 增强 for(最常用)

for (String s : list) {

System.out.println(s);

}

// 3. Iterator 迭代器(遍历时删除元素要用它!)

Iterator<String> it = list.iterator();

while (it.hasNext()) {

String s = it.next();

if (s.equals("要删除")) {

it.remove(); // 安全删除

}

}

// 4. Lambda forEach(Java 8+)

list.forEach(s -> System.out.println(s));

list.forEach(System.out::println); // 方法引用简化

⚠️ 重要坑点:增强 for 循环中不能直接 add/remove 元素,会抛 ConcurrentModificationException。要删除用 Iterator。

特点总结

  • ✅ 查询快(O(1) 随机访问)
  • ❌ 中间插入删除慢(需要移动元素)
  • ✅ 适合读多写少的场景

2.2 LinkedList——双向链表

底层原理:双向链表,每个节点存元素 + 前后指针。

LinkedList<String> linked = new LinkedList<>();

// List 接口的方法都有,额外还有链表特有方法

linked.addFirst("头"); // 头部添加

linked.addLast("尾"); // 尾部添加

linked.getFirst(); // 获取头

linked.getLast(); // 获取尾

linked.removeFirst(); // 删除头

linked.removeLast(); // 删除尾

// 还能当队列、栈用

linked.offer("入队");

linked.poll(); // 出队

linked.peek(); // 看队头

特点总结

  • ✅ 头尾插入删除快(O(1))
  • ❌ 查询慢(O(n),得从头找)
  • ✅ 实现了 Deque 接口,可当队列/栈用
  • ❌ 实际开发中用得不多,性能优势不明显

💡 面试常问:ArrayList 和 LinkedList 的区别?

  • 底层:数组 vs 双向链表
  • 访问:ArrayList 随机访问 O(1),LinkedList O(n)
  • 增删:ArrayList 中间增删 O(n),LinkedList 头尾 O(1)、中间 O(n)
  • 内存:ArrayList 连续内存,LinkedList 节点分散

2.3 Vector 和 Stack(了解就行,别用)

  • Vector:ArrayList 的线程安全版,所有方法都加 synchronized,性能差,已废弃
  • Stack:继承 Vector,实现栈,设计有问题,推荐用 ArrayDeque 代替栈

三、Set 家族

3.1 HashSet——哈希表实现

底层原理:其实就是 HashMap 的 Key 部分,值是一个固定的 Object。

Set<String> set = new HashSet<>();

set.add("苹果");

set.add("香蕉");

set.add("苹果"); // 不会报错,但加不进去(重复元素返回 false)

set.size(); // 2

set.contains("苹果"); // true

set.remove("苹果");

// 遍历(没有 get 方法,不能按索引取)

for (String s : set) {

System.out.println(s);

}

set.forEach(System.out::println);

HashSet 如何判断元素重复?

先看 hashCode() 是否相等,再看 equals() 是否为 true。两者都相等才认为是同一个元素。

⚠️ 重点:自定义类要放入 HashSet,必须重写 hashCode()equals()

class Person {

private String name;

private int age;

@Override

public boolean equals(Object o) {

if (this == o) return true;

if (o == null || getClass() != o.getClass()) return false;

Person person = (Person) o;

return age == person.age && Objects.equals(name, person.name);

}

@Override

public int hashCode() {

return Objects.hash(name, age);

}

}

特点总结

  • ✅ 增删查都是 O(1),速度快
  • ❌ 元素无序(不保证顺序)
  • ❌ 元素不可重复
  • ❌ 线程不安全

3.2 LinkedHashSet——有序不重复

底层原理:继承 HashSet,底层是 LinkedHashMap,多了一个双向链表维护插入顺序。

Set<String> set = new LinkedHashSet<>();

set.add("A");

set.add("C");

set.add("B");

// 遍历顺序 = 插入顺序:A C B

set.forEach(System.out::println);

特点

  • ✅ 插入有序
  • ✅ 性能略低于 HashSet(多维护了链表)
  • ✅ 去重又要保持顺序时用

3.3 TreeSet——排序不重复

底层原理:红黑树实现,自动按元素自然排序。

TreeSet<Integer> set = new TreeSet<>();

set.add(5);

set.add(2);

set.add(8);

set.add(1);

// 遍历自动升序:1 2 5 8

set.forEach(System.out::println);

// 额外方法

set.first(); // 最小元素

set.last(); // 最大元素

set.lower(5); // 小于5的最大元素 → 2

set.higher(5); // 大于5的最小元素 → 8

set.headSet(5); // 小于5的子集

set.tailSet(5); // 大于等于5的子集

两种排序方式

// 方式一:元素实现 Comparable 接口(自然排序)

class Student implements Comparable<Student> {

private int score;

@Override

public int compareTo(Student o) {

// return 负数 放左边,正数 放右边,0 不存

return this.score - o.score; // 按分数升序

}

}

// 方式二:构造时传 Comparator(比较器排序,更灵活)

TreeSet<Student> set = new TreeSet<>((s1, s2) -> {

// 先按分数升序,同分按姓名

int cmp = s1.getScore() - s2.getScore();

return cmp != 0 ? cmp : s1.getName().compareTo(s2.getName());

});

⚠️ TreeSet 判断元素相等的依据是 compareTo 返回 0,不是 equals!返回 0 的两个元素会被认为重复,第二个加不进去。

特点总结

  • ✅ 元素自动排序
  • ✅ 有丰富的范围查询方法
  • ❌ 增删查都是 O(log n),比 HashSet 慢一点
  • ❌ 自定义类必须实现排序规则

四、Map 家族

4.1 HashMap——哈希表(最常用)

底层原理:数组 + 链表 + 红黑树(JDK 1.8+)。默认容量 16,负载因子 0.75,超过就扩容 2 倍。

// 创建

Map<String, Integer> map = new HashMap<>();

// 增删改查

map.put("Java", 90); // 添加/修改

map.put("Python", 85);

map.put("Go", 95);

map.get("Java"); // 90(获取)

map.getOrDefault("C++", 0); // 没有则返回默认值

map.containsKey("Java"); // true

map.containsValue(90); // true

map.remove("Java"); // 删除

map.size(); // 大小

map.isEmpty();

map.clear();

// 遍历 key

for (String key : map.keySet()) {

System.out.println(key + " → " + map.get(key));

}

// 遍历 entry(推荐,性能好)

for (Map.Entry<String, Integer> entry : map.entrySet()) {

System.out.println(entry.getKey() + " → " + entry.getValue());

}

// Lambda 遍历(最优雅)

map.forEach((k, v) -> System.out.println(k + " → " + v));

HashMap 底层原理(简化版)

  1. 调用 key 的 hashCode() 得到哈希值
  2. 通过哈希算法计算数组下标((n - 1) & hash
  3. 如果该位置没有元素,直接放进去
  4. 如果有元素(哈希冲突),用 equals 比较
    • 相等:覆盖旧值
    • 不等:链表挂在后面(链表长度 ≥ 8 且数组长度 ≥ 64 时转红黑树)

💡 面试高频:HashMap 的扩容机制?

  • 默认容量 16,负载因子 0.75
  • 当 size > 容量 × 0.75 时,扩容为原来的 2 倍
  • 扩容需要重新计算所有元素的位置,性能消耗大
  • 预估数据量时建议初始化容量 new HashMap<>(initialCapacity)

特点总结

  • ✅ 增删查都是 O(1),速度快
  • ❌ Key 无序
  • ❌ 线程不安全
  • ✅ Key 可以有一个 null,Value 可以多个 null

4.2 LinkedHashMap——有序的 HashMap

继承 HashMap,多了一个双向链表维护插入顺序或访问顺序。

// 插入有序(默认)

Map<String, Integer> map = new LinkedHashMap<>();

map.put("a", 1);

map.put("b", 2);

map.put("c", 3);

// 遍历顺序:a→1, b→2, c→3

// 访问有序(LRU 缓存常用)

// accessOrder = true 时,最近访问的元素移到最后

Map<String, Integer> accessOrderMap =

new LinkedHashMap<>(16, 0.75f, true);

4.3 TreeMap——排序的 Map

底层红黑树,Key 自动排序。

TreeMap<String, Integer> map = new TreeMap<>();

map.put("banana", 2);

map.put("apple", 5);

map.put("cherry", 1);

// 遍历按 key 自然排序:apple→5, banana→2, cherry→1

map.forEach((k, v) -> System.out.println(k + " → " + v));

// 额外方法

map.firstKey(); // 最小的key

map.lastKey(); // 最大的key

map.lowerKey("cherry"); // 小于 cherry 的最大key

map.higherKey("apple"); // 大于 apple 的最小key

map.headMap("cherry"); // 小于 cherry 的子map

同样支持自定义 Comparator,用法和 TreeSet 类似。

4.4 Hashtable 和 ConcurrentHashMap

表格

特性 Hashtable HashMap ConcurrentHashMap
线程安全
效率 低(全表锁) 高(分段锁/CAS)
允许 null key/value
状态 废弃 常用 并发场景用

💡 并发场景用 ConcurrentHashMap,别用 Hashtable。

五、Queue 队列

5.1 ArrayDeque——双端队列

基于数组实现的双端队列,当栈用比 Stack 好,当队列用比 LinkedList 好

Deque<String> deque = new ArrayDeque<>();

// 当栈用(LIFO)

deque.push("A"); // 压栈

deque.push("B");

deque.pop(); // 弹栈 → B

deque.peek(); // 看栈顶 → A

// 当队列用(FIFO)

deque.offer("C"); // 入队

deque.poll(); // 出队 → A

deque.peek(); // 看队头

5.2 PriorityQueue——优先队列

底层是堆,元素按优先级出队。

// 默认小顶堆

PriorityQueue<Integer> pq = new PriorityQueue<>();

pq.offer(5);

pq.offer(2);

pq.offer(8);

pq.offer(1);

pq.poll(); // 1(最小的先出来)

pq.poll(); // 2

pq.poll(); // 5

// 自定义比较器(大顶堆)

PriorityQueue<Integer> maxHeap =

new PriorityQueue<>((a, b) -> b - a);

六、Collections 工具类

集合操作的工具类,很多实用方法:

List<String> list = new ArrayList<>(Arrays.asList("c", "a", "b"));

// 排序

Collections.sort(list); // [a, b, c]

Collections.sort(list, Collections.reverseOrder()); // [c, b, a]

Collections.shuffle(list); // 随机打乱

// 最大最小

Collections.max(list); // c

Collections.min(list); // a

// 查找(前提是已排序)

Collections.binarySearch(list, "b"); // 1

// 填充

Collections.fill(list, "x"); // 全替换成 x

// 不可变集合

List<String> unmodifiable = Collections.unmodifiableList(list);

// unmodifiable.add("d"); // 抛异常,不可修改

// 单元素/空集合

List<String> singleton = Collections.singletonList("single");

List<String> empty = Collections.emptyList();

七、集合选择指南

表格

需求 推荐 说明
列表,查询多 ArrayList 最常用,默认选它
列表,头尾插删多 LinkedList / ArrayDeque 优先 ArrayDeque
去重,不关心顺序 HashSet 性能最好
去重,要插入顺序 LinkedHashSet
去重,要排序 TreeSet 红黑树,O(log n)
键值对,不关心顺序 HashMap 最常用
键值对,要插入顺序 LinkedHashMap
键值对,要key排序 TreeMap
并发场景的Map ConcurrentHashMap 别用 Hashtable
ArrayDeque 比 Stack 好
队列 ArrayDeque / LinkedList
优先级队列 PriorityQueue 堆实现

八、实战练习

练习1:统计字符串中每个字符出现的次数

public class CharCount {

public static void main(String[] args) {

String str = "hello world hello java";

Map<Character, Integer> countMap = new HashMap<>();

for (char c : str.toCharArray()) {

if (c == ' ') continue;

// 方式一:判断式

// if (countMap.containsKey(c)) {

// countMap.put(c, countMap.get(c) + 1);

// } else {

// countMap.put(c, 1);

// }

// 方式二:getOrDefault

// countMap.put(c, countMap.getOrDefault(c, 0) + 1);

// 方式三:merge(最优雅)

countMap.merge(c, 1, Integer::sum);

}

countMap.forEach((k, v) -> System.out.println(k + " 出现 " + v + " 次"));

}

}

练习2:List 去重(保持顺序)

public class ListDeduplicate {

public static void main(String[] args) {

List<Integer> list = new ArrayList<>(

Arrays.asList(1, 2, 3, 2, 1, 4, 5, 3));

// 方式一:LinkedHashSet(最简单,推荐)

List<Integer> result1 = new ArrayList<>(new LinkedHashSet<>(list));

System.out.println(result1); // [1, 2, 3, 4, 5]

// 方式二:Stream 去重(Java 8+)

List<Integer> result2 = list.stream()

.distinct()

.toList();

System.out.println(result2);

}

}

练习3:学生成绩排序

class Student {

private String name;

private int score;

public Student(String name, int score) {

this.name = name;

this.score = score;

}

public String getName() { return name; }

public int getScore() { return score; }

@Override

public String toString() {

return name + "(" + score + ")";

}

}

public class StudentSort {

public static void main(String[] args) {

List<Student> students = new ArrayList<>();

students.add(new Student("张三", 85));

students.add(new Student("李四", 92));

students.add(new Student("王五", 78));

students.add(new Student("赵六", 92));

students.add(new Student("钱七", 85));

// 按分数降序,同分按姓名字母序

students.sort((s1, s2) -> {

int scoreCompare = s2.getScore() - s1.getScore();

if (scoreCompare != 0) return scoreCompare;

return s1.getName().compareTo(s2.getName());

});

students.forEach(System.out::println);

// 李四(92)

// 赵六(92)

// 张三(85)

// 钱七(85)

// 王五(78)

}

}

练习4:简易 LRU 缓存(LinkedHashMap 版)

class LRUCache<K, V> extends LinkedHashMap<K, V> {

private final int capacity;

public LRUCache(int capacity) {

// accessOrder = true 表示按访问顺序排序

super(capacity, 0.75f, true);

this.capacity = capacity;

}

// 超过容量时删除最老的元素

@Override

protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {

return size() > capacity;

}

public static void main(String[] args) {

LRUCache<String, Integer> cache = new LRUCache<>(3);

cache.put("a", 1);

cache.put("b", 2);

cache.put("c", 3);

System.out.println(cache); // {a=1, b=2, c=3}

cache.get("a"); // 访问 a,a 变最新

cache.put("d", 4); // 超过容量,删最老的 b

System.out.println(cache); // {c=3, a=1, d=4}

}

}

九、总结速查表

核心实现对比

表格

集合 底层 有序 重复 null 增删查 线程安全
ArrayList 数组 插入序 查O(1),增删O(n)
LinkedList 双向链表 插入序 头尾O(1),查O(n)
HashSet HashMap 一个null O(1)
LinkedHashSet LinkedHashMap 插入序 O(1)
TreeSet 红黑树 排序 O(log n)
HashMap 数组+链表+红黑树 Key不重复 一key多value O(1)
LinkedHashMap HashMap+链表 插入/访问序 Key不重复 O(1)
TreeMap 红黑树 Key排序 Key不重复 O(log n)
ArrayDeque 循环数组 FIFO/LIFO O(1)
PriorityQueue 优先级 入队O(log n)
ConcurrentHashMap 分段哈希 Key不重复 O(1)

经验法则

  1. 默认用 ArrayList 和 HashMap,大多数场景它们是最优解
  2. Set 元素一定要重写 hashCode 和 equals,TreeSet/TreeMap 要重写比较方法
  3. 遍历中删除元素用 IteratorremoveIf(),别在增强 for 里删
  4. 预估集合大小时指定初始容量,避免频繁扩容
  5. 并发场景用 ConcurrentHashMap,别用 Hashtable
  6. 返回空集合用空集合对象Collections.emptyList()),别返回 null

集合框架是 Java 程序员的基本功,面试必考、工作必用。这篇把主流集合的特点、原理和用法都梳理了一遍,但更重要的还是多写多用。熟练掌握集合,代码质量和开发效率都会上一个台阶。🔥

更多推荐