Java 集合框架与数据结构

本文系统讲解 Java 集合框架与常用数据结构,覆盖 CollectionListSetMapQueue、泛型、迭代遍历、去重、排序和常见实现类的使用场景。文章结合业务案例说明不同集合的特点,帮助读者在实际开发中正确选择数据容器。

一、基础与总览

1. 为什么要学习 Java 集合?

在 Java 开发中,集合几乎无处不在。

比如:

  • 查询数据库后,需要用 List 接收多条数据
  • 需要根据用户 ID 快速找到用户信息,可以用 Map
  • 需要去重,可以用 Set
  • 需要排队处理任务,可以用 Queue
  • 需要线程安全的数据结构,可以用 ConcurrentHashMap

简单来说:

数组适合存放固定数量的数据,而集合适合存放动态变化的数据。

数组的长度一旦创建就固定了:

Java
int[] arr = new int[3];

如果后面想放第 4 个元素,就需要创建新数组再复制数据,比较麻烦。

而集合可以动态扩容:

Java
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");

集合的核心价值就是:

帮我们更方便、更高效地管理一组对象。

2. Java 集合体系总览

Java 集合主要分为两大类:

  1. Collection 体系:单列集合,每个元素是一个独立对象
  2. Map 体系:双列集合,每个元素是一个键值对 key-value

1. Collection 体系

Collection 是单列集合的根接口。

它下面主要有三大分支:

Text
Collection
├── List
│   ├── ArrayList
│   ├── LinkedList
│   └── Vector
│
├── Set
│   ├── HashSet
│   ├── LinkedHashSet
│   └── TreeSet
│
└── Queue
    ├── LinkedList
    ├── PriorityQueue
    └── ArrayDeque

2. Map 体系

Map 是键值对集合的根接口。

Text
Map
├── HashMap
├── LinkedHashMap
├── TreeMap
├── Hashtable
└── ConcurrentHashMap

3. Collection 接口

1. Collection 是什么?

Collection 表示一组对象。

它定义了集合最基础的操作,例如:

Java
boolean add(E e);          // 添加元素
boolean remove(Object o);  // 删除元素
boolean contains(Object o);// 判断是否包含元素
int size();                // 获取元素个数
boolean isEmpty();         // 判断是否为空
void clear();              // 清空集合
Iterator<E> iterator();    // 获取迭代器

2. 示例

Java
Collection<String> collection = new ArrayList<>();

collection.add("Java");
collection.add("MySQL");
collection.add("Redis");

System.out.println(collection.size());
System.out.println(collection.contains("Java"));

collection.remove("Redis");

for (String item : collection) {
    System.out.println(item);
}

注意:

实际开发中,我们很少直接使用 Collection 类型创建对象,更多是使用它的子接口,比如 ListSetQueue


二、List 体系

1. List 集合

1. List 的特点

List 是有序、可重复的集合。

这里的“有序”指的是:

元素按照添加顺序保存。

例如:

Java
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

遍历时通常也是:

Text
A B C

List 允许重复元素:

Java
list.add("A");
list.add("A");

最终集合中可以有两个 A

2. List 常用方法

Java
void add(int index, E element); // 在指定位置添加元素
E get(int index);               // 根据下标获取元素
E set(int index, E element);    // 修改指定位置元素
E remove(int index);            // 删除指定位置元素
int indexOf(Object o);          // 查找元素第一次出现的位置

示例:

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

list.add("Java");
list.add("MySQL");
list.add("Redis");

System.out.println(list.get(0)); // Java

list.set(1, "Oracle");

list.remove(2);

System.out.println(list);

2. ArrayList 深入理解

1. ArrayList 是什么?

ArrayList 是最常用的 List 实现类。

底层是数组。

可以简单理解为:

ArrayList = 可以自动扩容的数组。

2. ArrayList 的特点

特点说明
底层结构数组
是否有序有序
是否允许重复允许
是否允许 null允许
查询速度
增删速度中间位置较慢
线程安全不安全

3. 为什么 ArrayList 查询快?

因为它底层是数组。

数组支持通过下标直接访问元素:

Java
list.get(3);

底层大致相当于:

Java
elementData[3];

数组通过下标定位元素非常快,时间复杂度是:

Text
O(1)

4. 为什么 ArrayList 中间插入和删除慢?

假设有一个数组:

Text
[A, B, C, D, E]

如果要在 B 后面插入 X

Text
[A, B, X, C, D, E]

那么 C、D、E 都要往后移动。

删除也类似。

所以在中间位置插入或删除元素时,ArrayList 需要移动大量元素,效率较低。

5. ArrayList 扩容机制

ArrayList 底层数组容量不够时,会自动扩容。

常见理解方式:

Text
原数组容量不够
↓
创建一个更大的新数组
↓
把旧数组元素复制过去
↓
新元素加入新数组

在 JDK 8 中,ArrayList 默认第一次添加元素时会创建容量为 10 的数组,后续扩容大约是原容量的 1.5 倍。

示例:

Text
10 → 15 → 22 → 33 → 49 ...

注意:

扩容本身是有成本的,因为需要复制数组。所以如果你提前知道大概数据量,可以指定初始容量。

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

这可以减少扩容次数,提高性能。

6. ArrayList 使用场景

适合:

  • 查询多
  • 遍历多
  • 尾部添加多
  • 中间插入删除少

不适合:

  • 频繁在中间插入
  • 频繁在中间删除
  • 多线程并发修改

3. LinkedList 深入理解

1. LinkedList 是什么?

LinkedList 也是 List 的实现类。

但它底层不是数组,而是双向链表。

可以简单理解为:

LinkedList = 一节一节连起来的数据结构。

每个节点大致包含三部分:

Text
前一个节点地址 | 当前数据 | 后一个节点地址

2. LinkedList 的特点

特点说明
底层结构双向链表
是否有序有序
是否允许重复允许
查询速度较慢
头尾增删
中间查找
线程安全不安全

3. 为什么 LinkedList 查询慢?

数组可以通过下标直接定位:

Java
array[100]

但是链表不能直接定位第 100 个节点。

它需要从头或尾一个一个找。

所以查询效率通常是:

Text
O(n)

4. 为什么 LinkedList 头尾增删快?

因为链表添加或删除节点,只需要修改节点之间的引用关系。

例如删除节点 B

Text
A <-> B <-> C

删除后:

Text
A <-> C

只需要把 AC 连接起来即可。

5. LinkedList 常用方法

Java
LinkedList<String> list = new LinkedList<>();

list.addFirst("A");
list.addLast("B");

System.out.println(list.getFirst());
System.out.println(list.getLast());

list.removeFirst();
list.removeLast();

6. LinkedList 使用场景

适合:

  • 频繁在头部或尾部插入删除
  • 实现队列
  • 实现栈

不适合:

  • 频繁随机访问元素
  • 大量根据下标查询

4. ArrayList 和 LinkedList 对比

对比项ArrayListLinkedList
底层结构数组双向链表
查询快,O(1)慢,O(n)
尾部添加
中间插入慢,需要移动元素找到位置后快,但查找位置慢
删除元素慢,需要移动元素找到位置后快
内存占用较少较多,因为每个节点要存前后引用
使用频率非常高相对较低

实际开发建议:

大多数情况下优先使用 ArrayList。

只有在明确需要频繁头尾操作,或者需要队列/栈结构时,再考虑 LinkedList 或 ArrayDeque。


三、Set 体系

1. Set 集合

1. Set 的特点

Set 是无重复元素的集合。

它的核心特点是:

Text
不允许重复

示例:

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

set.add("Java");
set.add("Java");
set.add("MySQL");

System.out.println(set);

结果中只会有一个 Java

2. Set 常见实现类

实现类特点
HashSet无序、不重复、查询快
LinkedHashSet按添加顺序保存、不重复
TreeSet自动排序、不重复

2. HashSet 深入理解

1. HashSet 是什么?

HashSet 是最常用的 Set 实现类。

它底层其实是基于 HashMap 实现的。

可以简单理解为:

HashSet 只使用了 HashMap 的 key,不关心 value。

例如:

Java
Set<String> set = new HashSet<>();
set.add("Java");

底层大致类似:

Java
map.put("Java", 一个固定对象);

2. HashSet 如何判断重复?

HashSet 判断两个对象是否重复,主要依赖两个方法:

Java
hashCode()
equals()

判断过程可以简单理解为:

Text
先比较 hashCode
如果 hashCode 不同,认为不是同一个对象
如果 hashCode 相同,再用 equals 比较
如果 equals 为 true,认为重复

3. 示例:自定义对象去重失败

Java
class Student {
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Set<Student> set = new HashSet<>();
set.add(new Student("张三", 18));
set.add(new Student("张三", 18));

System.out.println(set.size());

结果可能是:

Text
2

为什么?

因为两个 new Student("张三", 18) 是两个不同对象。

如果没有重写 equals()hashCode(),默认比较的是对象地址。

4. 正确做法:重写 equals 和 hashCode

Java
import java.util.Objects;

class Student {
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

这样再放入 HashSet:

Java
Set<Student> set = new HashSet<>();
set.add(new Student("张三", 18));
set.add(new Student("张三", 18));

System.out.println(set.size()); // 1

5. HashSet 使用场景

适合:

  • 去重
  • 判断元素是否存在
  • 不关心顺序

例如:

Java
List<String> names = Arrays.asList("张三", "李四", "张三", "王五");
Set<String> uniqueNames = new HashSet<>(names);
System.out.println(uniqueNames);

3. LinkedHashSet

1. LinkedHashSet 是什么?

LinkedHashSetHashSet 的子类。

它既能去重,又能保留添加顺序。

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

set.add("B");
set.add("A");
set.add("C");
set.add("A");

System.out.println(set);

输出顺序通常是:

Text
[B, A, C]

2. 使用场景

适合:

  • 需要去重
  • 又要保留原始添加顺序

例如:

Java
List<String> list = Arrays.asList("A", "B", "A", "C", "B");
List<String> result = new ArrayList<>(new LinkedHashSet<>(list));

System.out.println(result); // [A, B, C]

这是一种常见的“List 去重并保序”的写法。


4. TreeSet

1. TreeSet 是什么?

TreeSet 是可以自动排序的 Set。

它底层基于红黑树实现。

特点:

Text
不重复 + 自动排序

示例:

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

set.add(3);
set.add(1);
set.add(2);

System.out.println(set);

输出:

Text
[1, 2, 3]

2. TreeSet 排序规则

TreeSet 有两种排序方式:

  1. 自然排序:元素类实现 Comparable
  2. 比较器排序:创建 TreeSet 时传入 Comparator

3. 自然排序 Comparable

Java
class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Student other) {
        return this.age - other.age;
    }

    @Override
    public String toString() {
        return name + ":" + age;
    }
}

使用:

Java
Set<Student> set = new TreeSet<>();
set.add(new Student("张三", 20));
set.add(new Student("李四", 18));
set.add(new Student("王五", 22));

System.out.println(set);

4. 比较器排序 Comparator

Java
Set<Student> set = new TreeSet<>((s1, s2) -> s1.getAge() - s2.getAge());

更推荐写法:

Java
Set<Student> set = new TreeSet<>(Comparator.comparingInt(Student::getAge));

如果年龄相同,还想按姓名排序:

Java
Set<Student> set = new TreeSet<>(
    Comparator.comparingInt(Student::getAge)
              .thenComparing(Student::getName)
);

5. TreeSet 注意点

TreeSet 判断重复不是依赖 equals(),而是依赖比较结果。

如果比较结果是 0,TreeSet 就认为两个元素重复。

例如只按年龄比较:

Java
Comparator.comparingInt(Student::getAge)

那么两个年龄相同的学生会被认为重复,即使名字不同。

所以排序规则一定要写完整。


四、Map 体系

1. Map 集合

1. Map 是什么?

Map 是键值对集合。

一个元素由两部分组成:

Text
key -> value

例如:

Text
1001 -> 张三
1002 -> 李四
1003 -> 王五

在 Java 中:

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

map.put(1001, "张三");
map.put(1002, "李四");
map.put(1003, "王五");

2. Map 的特点

  • key 不允许重复
  • value 可以重复
  • 一个 key 对应一个 value
  • 如果 key 重复,新的 value 会覆盖旧的 value

示例:

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

map.put("name", "张三");
map.put("name", "李四");

System.out.println(map.get("name")); // 李四

3. Map 常用方法

Java
V put(K key, V value);          // 添加或修改
V get(Object key);              // 根据 key 获取 value
V remove(Object key);           // 根据 key 删除
boolean containsKey(Object key);// 是否包含 key
boolean containsValue(Object value);// 是否包含 value
Set<K> keySet();                // 获取所有 key
Collection<V> values();         // 获取所有 value
Set<Map.Entry<K,V>> entrySet(); // 获取所有键值对

2. HashMap 深入理解

1. HashMap 是什么?

HashMap 是最常用的 Map 实现类。

它的核心特点是:

Text
根据 key 快速存取 value

示例:

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

scoreMap.put("张三", 90);
scoreMap.put("李四", 85);
scoreMap.put("王五", 95);

System.out.println(scoreMap.get("张三"));

2. HashMap 底层结构

在 JDK 8 之后,HashMap 底层结构可以理解为:

Text
数组 + 链表 + 红黑树

存储过程简化理解

当执行:

Java
map.put("name", "张三");

大致流程是:

Text
1. 计算 key 的 hash 值
2. 根据 hash 值计算数组下标
3. 如果该位置为空,直接放入
4. 如果该位置已有元素,说明发生 hash 冲突
5. 冲突时用链表或红黑树保存多个元素

3. 什么是 hash 冲突?

HashMap 底层有一个数组。

不同的 key 经过 hash 计算后,有可能落到同一个数组位置。

这就叫 hash 冲突。

例如:

Text
key1 -> 下标 3
key2 -> 下标 3

它们都想放到数组下标 3 的位置。

HashMap 的解决方式是:

Text
数组位置上挂链表
链表过长后转成红黑树

4. 为什么链表会转红黑树?

如果很多元素都落在同一个数组位置上,链表会变得很长。

链表查询需要一个一个找,效率是:

Text
O(n)

红黑树查询效率更高,通常是:

Text
O(log n)

所以 JDK 8 以后,当链表达到一定长度并且数组容量足够时,链表会转成红黑树。

常见面试点:

Text
链表长度 >= 8 且数组容量 >= 64 时,链表转红黑树。

如果数组容量还比较小,HashMap 会优先扩容,而不是直接树化。

5. HashMap 扩容机制

HashMap 默认初始容量通常是 16,负载因子是 0.75。

扩容阈值:

Text
容量 × 负载因子

默认情况下:

Text
16 × 0.75 = 12

也就是说,当元素数量超过 12 时,HashMap 会扩容。

扩容通常变为原来的 2 倍:

Text
16 → 32 → 64 → 128 ...

6. 为什么 HashMap 容量通常是 2 的幂?

HashMap 根据 hash 值计算下标时,常用类似下面的方式:

Java
index = (n - 1) & hash;

其中 n 是数组长度。

n 是 2 的幂时,使用位运算可以更高效,并且能让元素分布更均匀。

7. HashMap 的 key 为什么要重写 equals 和 hashCode?

如果用自定义对象作为 HashMap 的 key,必须正确重写:

Java
equals()
hashCode()

否则可能出现“看起来相同的 key,却取不到 value”的问题。

错误示例:

Java
class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

Map<User, String> map = new HashMap<>();
map.put(new User("张三"), "管理员");

System.out.println(map.get(new User("张三"))); // null

原因:

两个 new User("张三") 是两个不同对象,默认比较地址。

正确做法是重写 equals()hashCode()


3. LinkedHashMap

1. LinkedHashMap 是什么?

LinkedHashMap 是 HashMap 的子类。

它在 HashMap 的基础上维护了一个双向链表,所以可以保留顺序。

默认保留插入顺序。

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

map.put("B", 2);
map.put("A", 1);
map.put("C", 3);

System.out.println(map.keySet());

输出:

Text
[B, A, C]

2. 使用场景

适合:

  • 需要 key-value 存储
  • 又希望保留插入顺序

例如:

Java
Map<String, Object> result = new LinkedHashMap<>();
result.put("code", 200);
result.put("message", "success");
result.put("data", null);

这样输出 JSON 时字段顺序更可控。


4. TreeMap

1. TreeMap 是什么?

TreeMap 是可以根据 key 自动排序的 Map。

底层基于红黑树。

示例:

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

map.put(3, "C");
map.put(1, "A");
map.put(2, "B");

System.out.println(map);

输出:

Text
{1=A, 2=B, 3=C}

2. TreeMap 使用场景

适合:

  • 需要按 key 排序
  • 需要范围查询

例如:

Java
TreeMap<Integer, String> map = new TreeMap<>();
map.put(90, "优秀");
map.put(60, "及格");
map.put(0, "不及格");

System.out.println(map.ceilingEntry(70));
System.out.println(map.floorEntry(70));

5. HashMap、Hashtable、ConcurrentHashMap 对比

1. Hashtable

Hashtable 是比较老的类。

特点:

  • 线程安全
  • 方法上使用 synchronized
  • 性能较低
  • 不允许 null key 和 null value

现在实际开发中一般不推荐使用。

2. HashMap

特点:

  • 非线程安全
  • 性能高
  • 允许一个 null key
  • 允许多个 null value

适合普通单线程场景。

3. ConcurrentHashMap

特点:

  • 线程安全
  • 并发性能比 Hashtable 更好
  • 不允许 null key 和 null value

适合多线程并发场景。

4. 对比表

线程安全是否允许 null性能使用建议
HashMap不安全允许单线程常用
Hashtable安全不允许不推荐
ConcurrentHashMap安全不允许多线程推荐

五、Queue 与双端队列

1. Queue 队列

1. Queue 是什么?

Queue 表示队列。

队列的特点通常是:

Text
先进先出 FIFO

就像排队买票:

Text
先来的人先买

2. Queue 常用方法

方法异常版安全版
添加元素add(e)offer(e)
获取并删除队头remove()poll()
获取但不删除队头element()peek()

更推荐使用安全版:

Java
offer()
poll()
peek()

示例:

Java
Queue<String> queue = new LinkedList<>();

queue.offer("A");
queue.offer("B");
queue.offer("C");

System.out.println(queue.poll()); // A
System.out.println(queue.poll()); // B
System.out.println(queue.peek()); // C

2. Deque 双端队列

1. Deque 是什么?

Deque 是双端队列。

它允许从队头和队尾两端添加、删除元素。

常用实现类:

Java
ArrayDeque
LinkedList

2. 使用 ArrayDeque 实现栈

栈的特点:

Text
后进先出 LIFO

示例:

Java
Deque<String> stack = new ArrayDeque<>();

stack.push("A");
stack.push("B");
stack.push("C");

System.out.println(stack.pop()); // C
System.out.println(stack.pop()); // B

现在如果需要栈结构,更推荐使用 ArrayDeque,而不是老的 Stack 类。


六、遍历、工具与高级特性

1. 集合遍历方式

1. 普通 for 循环

适合 List,因为 List 有下标。

Java
List<String> list = Arrays.asList("A", "B", "C");

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

2. 增强 for 循环

适合大多数集合。

Java
for (String item : list) {
    System.out.println(item);
}

本质上是使用迭代器。

3. Iterator 迭代器

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

while (iterator.hasNext()) {
    String item = iterator.next();
    System.out.println(item);
}

4. Lambda 遍历

Java
list.forEach(item -> System.out.println(item));

可以简写为:

Java
list.forEach(System.out::println);

5. Map 遍历方式

方式一:遍历 keySet

Java
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);

for (String key : map.keySet()) {
    Integer value = map.get(key);
    System.out.println(key + "=" + value);
}

方式二:遍历 entrySet

Java
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

推荐使用 entrySet(),因为它可以同时拿到 key 和 value,效率更好。

方式三:Lambda

Java
map.forEach((key, value) -> {
    System.out.println(key + "=" + value);
});

2. 集合删除元素的常见坑

1. 错误写法:增强 for 中直接 remove

Java
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String item : list) {
    if ("B".equals(item)) {
        list.remove(item);
    }
}

这可能会抛出:

Text
ConcurrentModificationException

原因:

增强 for 底层使用的是 Iterator,但你直接调用了集合本身的 remove(),破坏了迭代器的预期。

2. 正确写法一:使用 Iterator 删除

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

while (iterator.hasNext()) {
    String item = iterator.next();
    if ("B".equals(item)) {
        iterator.remove();
    }
}

3. 正确写法二:removeIf

Java
list.removeIf(item -> "B".equals(item));

这是实际开发中更简洁的写法。


3. 泛型 Generics

1. 为什么需要泛型?

没有泛型时,集合中可以放任何对象。

Java
List list = new ArrayList();
list.add("Java");
list.add(123);

取出来时需要强制转换:

Java
String s = (String) list.get(0);

如果类型转换错误,运行时才会报错。

泛型可以在编译期限制类型。

Java
List<String> list = new ArrayList<>();
list.add("Java");
// list.add(123); // 编译错误

2. 泛型的好处

  • 类型安全
  • 减少强制类型转换
  • 代码更清晰
  • 编译期提前发现错误

3. 常见泛型写法

Java
List<String> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();
Set<Long> ids = new HashSet<>();

4. 通配符 ?

上界通配符

Java
List<? extends Number>

表示:

Text
可以接收 Number 或 Number 的子类集合

例如:

Java
List<Integer> integers = new ArrayList<>();
List<? extends Number> numbers = integers;

适合读取。

下界通配符

Java
List<? super Integer>

表示:

Text
可以接收 Integer 或 Integer 的父类集合

适合写入。

5. PECS 原则

PECS 是泛型中一个重要原则:

Text
Producer Extends, Consumer Super

意思是:

  • 如果一个集合主要用于生产数据,也就是读取,用 extends
  • 如果一个集合主要用于消费数据,也就是写入,用 super

示例:

Java
public void readNumbers(List<? extends Number> list) {
    Number number = list.get(0);
}

public void writeIntegers(List<? super Integer> list) {
    list.add(1);
}

4. Collections 工具类

Collections 是 Java 提供的集合工具类。

常用方法:

Java
Collections.sort(list);       // 排序
Collections.reverse(list);    // 反转
Collections.shuffle(list);    // 打乱
Collections.max(list);        // 最大值
Collections.min(list);        // 最小值
Collections.unmodifiableList(list); // 不可修改集合

示例:

Java
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));

Collections.sort(list);
System.out.println(list); // [1, 2, 3]

Collections.reverse(list);
System.out.println(list); // [3, 2, 1]

自定义排序:

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

students.sort(Comparator.comparingInt(Student::getAge));

5. Arrays 工具类

Arrays 是数组工具类。

常用方法:

Java
Arrays.asList();
Arrays.sort();
Arrays.toString();
Arrays.copyOf();

示例:

Java
int[] arr = {3, 1, 2};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));

Arrays.asList 的坑

Java
List<String> list = Arrays.asList("A", "B", "C");
list.add("D");

会报错:

Text
UnsupportedOperationException

原因:

Arrays.asList() 返回的不是普通 ArrayList,而是数组内部的固定长度 List。

正确写法:

Java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.add("D");

6. Stream 与集合

1. Stream 是什么?

Stream 是 Java 8 引入的一种处理集合数据的方式。

它可以让代码更简洁,更像是在描述“我要做什么”。

2. 常见操作

filter 过滤

Java
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> result = nums.stream()
        .filter(n -> n > 3)
        .collect(Collectors.toList());

System.out.println(result); // [4, 5]

map 转换

Java
List<String> names = Arrays.asList("zhangsan", "lisi");

List<String> upperNames = names.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList());

distinct 去重

Java
List<Integer> result = nums.stream()
        .distinct()
        .collect(Collectors.toList());

sorted 排序

Java
List<Integer> result = nums.stream()
        .sorted()
        .collect(Collectors.toList());

collect 收集

Java
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());

joining 拼接字符串

当流中的元素本身就是字符串,或者可以先映射成字符串时,可以使用 Collectors.joining() 把它们拼成一个结果。

Java
List<String> tags = Arrays.asList("Java", "Spring", "Redis");

String text = tags.stream()
        .collect(Collectors.joining(", "));

System.out.println(text); // Java, Spring, Redis

也可以指定前缀和后缀:

Java
String text = tags.stream()
        .collect(Collectors.joining(", ", "[", "]"));

System.out.println(text); // [Java, Spring, Redis]

它特别适合和 mapfilter 组合使用,例如先筛选、再转换、最后拼接成展示文本。

groupingBy 分组

Java
Map<Integer, List<Student>> groupMap = students.stream()
        .collect(Collectors.groupingBy(Student::getAge));

toMap 转 Map

Java
Map<Long, Student> studentMap = students.stream()
        .collect(Collectors.toMap(Student::getId, student -> student));

如果 key 可能重复,需要指定合并规则:

Java
Map<String, Student> map = students.stream()
        .collect(Collectors.toMap(
                Student::getName,
                student -> student,
                (oldValue, newValue) -> newValue
        ));

7. 集合的线程安全问题

1. 为什么会有线程安全问题?

例如多个线程同时操作同一个 ArrayList:

Java
List<Integer> list = new ArrayList<>();

for (int i = 0; i < 1000; i++) {
    new Thread(() -> list.add(1)).start();
}

可能出现:

  • 数据丢失
  • 数组越界
  • 结果数量不正确
  • 并发修改异常

因为 ArrayList 本身不是线程安全的。

2. 常见线程安全集合

场景推荐
线程安全 ListCopyOnWriteArrayList
线程安全 SetCopyOnWriteArraySet
线程安全 MapConcurrentHashMap
阻塞队列ArrayBlockingQueue / LinkedBlockingQueue

3. Collections.synchronizedList

Java
List<String> list = Collections.synchronizedList(new ArrayList<>());

这种方式可以让集合变成线程安全,但并发性能一般。

4. CopyOnWriteArrayList

适合读多写少的场景。

写操作时会复制一份新数组,写完后再替换旧数组。

优点:

  • 读操作不加锁
  • 遍历安全

缺点:

  • 写操作成本高
  • 占用更多内存

5. ConcurrentHashMap

适合高并发 Map 场景。

常用示例:

Java
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);

七、实战与总结

1. 常见集合选择建议

1. 需要有序、可重复

选择:

Java
ArrayList

2. 需要去重,不关心顺序

选择:

Java
HashSet

3. 需要去重,并保留添加顺序

选择:

Java
LinkedHashSet

4. 需要去重,并自动排序

选择:

Java
TreeSet

5. 需要 key-value 存储

选择:

Java
HashMap

6. 需要 key-value,并保留插入顺序

选择:

Java
LinkedHashMap

7. 需要 key 自动排序

选择:

Java
TreeMap

8. 多线程环境下使用 Map

选择:

Java
ConcurrentHashMap

9. 栈或双端队列

选择:

Java
ArrayDeque

2. 实际开发案例

案例一:List 去重

不要求顺序

Java
List<String> list = Arrays.asList("A", "B", "A", "C");
Set<String> set = new HashSet<>(list);
System.out.println(set);

要求保留原顺序

Java
List<String> list = Arrays.asList("A", "B", "A", "C");
List<String> result = new ArrayList<>(new LinkedHashSet<>(list));
System.out.println(result);

案例二:根据 ID 查询用户

Java
Map<Long, User> userMap = new HashMap<>();

for (User user : users) {
    userMap.put(user.getId(), user);
}

User user = userMap.get(1001L);

如果不用 Map,每次查找都要遍历 List,效率比较低。

案例三:统计单词出现次数

Java
List<String> words = Arrays.asList("java", "mysql", "java", "redis", "java");
Map<String, Integer> countMap = new HashMap<>();

for (String word : words) {
    countMap.put(word, countMap.getOrDefault(word, 0) + 1);
}

System.out.println(countMap);

案例四:按部门分组员工

Java
Map<String, List<Employee>> groupMap = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment));

案例五:取出年龄大于 18 的用户

Java
List<User> adults = users.stream()
        .filter(user -> user.getAge() > 18)
        .collect(Collectors.toList());

3. 总结

Java 集合的学习可以抓住一条主线:

Text
List:有序、可重复
Set:去重
Map:键值对
Queue:队列

再进一步理解各个实现类的底层结构:

Text
ArrayList:数组
LinkedList:链表
HashSet:HashMap 的 key
HashMap:数组 + 链表 + 红黑树
TreeSet / TreeMap:红黑树
ConcurrentHashMap:并发安全 Map

学习集合时,不要只背 API,而要多问自己:

  1. 它底层是什么结构?
  2. 为什么查询快或慢?
  3. 为什么能去重?
  4. 为什么要重写 equals 和 hashCode?
  5. 当前业务场景应该选哪个集合?

掌握这些之后,Java 集合就不只是“会用”,而是真正理解了。