Java集合框架详解

O泡李华 11

Java 集合框架详解

本章位置:Java 核心基础
学习目标:理解 Java 集合框架的整体结构,掌握 Collection、List、Set、Map、Iterator、泛型、排序、Collections 工具类等核心内容,并能在实际开发中正确选择集合类型。


一、为什么需要集合框架

前面学习数组时,我们已经知道:

int[] numbers = new int[5];

数组能够保存多个同类型数据。

但是数组有一个明显限制:

长度固定

例如:

int[] numbers = new int[5];

创建之后:

长度永远是 5

如果后面需要:

添加第 6 个数据
删除某一个数据
动态扩容
方便查找
去重
按照键和值保存

数组就会比较麻烦。

Java 因此提供:

集合框架

英文:

Java Collections Framework

简称:

JCF

二、集合和数组的区别

对比 数组 集合
长度 固定 通常可动态变化
数据类型 可保存基本类型和引用类型 主要保存对象
添加删除 比较麻烦 API 丰富
查找 需要自己处理 很多集合提供现成功能
去重 需要自己实现 Set 天然支持
键值结构 不支持 Map 支持
排序 借助 Arrays 可使用 Collections、比较器等

三、集合框架整体结构

Java 集合可以先分成两大体系:

Collection
Map

大致结构:

Java 集合框架
│
├─ Collection
│   │
│   ├─ List
│   │   ├─ ArrayList
│   │   ├─ LinkedList
│   │   └─ Vector
│   │
│   ├─ Set
│   │   ├─ HashSet
│   │   ├─ LinkedHashSet
│   │   └─ TreeSet
│   │
│   └─ Queue / Deque
│       ├─ LinkedList
│       ├─ ArrayDeque
│       └─ PriorityQueue
│
└─ Map
    │
    ├─ HashMap
    ├─ LinkedHashMap
    ├─ TreeMap
    └─ Hashtable

学习时最重要的是:

List
Set
Map

四、Collection 是什么

Collection 是很多单列集合的顶层接口。

例如:

List
Set
Queue

都属于:

Collection

可以理解:

Collection
↓
用于保存一组对象

五、Collection 常用方法

常见方法:

add()

remove()

contains()

size()

isEmpty()

clear()

addAll()

removeAll()

retainAll()

containsAll()

toArray()

六、Collection 基础示例

import java.util.ArrayList;
import java.util.Collection;

public class Demo {

    public static void main(String[] args) {

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

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

        System.out.println(collection);
    }
}

输出类似:

[Java, MySQL, Spring]

七、size()

获取元素数量:

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

如果有 3 个元素:

3

注意:

数组:array.length

字符串:str.length()

集合:collection.size()

八、isEmpty()

判断集合是否为空:

if (collection.isEmpty()) {

    System.out.println(
            "集合为空"
    );
}

九、add()

添加元素:

collection.add("Redis");

十、remove()

删除指定元素:

collection.remove("MySQL");

十一、contains()

判断是否包含:

boolean result =
        collection.contains("Java");

十二、clear()

清空:

collection.clear();

清空之后:

collection.size()

变成:

0

十三、集合只能直接保存对象

错误:

ArrayList<int>

Java 泛型不能直接使用基本类型。

正确:

ArrayList<Integer>

因为:

int
对应包装类
Integer

十四、自动装箱让使用更简单

例如:

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

list.add(10);
list.add(20);

虽然集合保存的是:

Integer

但 Java 自动完成:

int → Integer

这就是:

自动装箱

十五、泛型

集合中经常看到:

List<String>
List<Integer>

尖括号里的:

String
Integer

就是:

泛型参数

十六、为什么需要泛型

如果没有泛型:

List list =
        new ArrayList();

list.add("Java");
list.add(100);
list.add(true);

什么都可以放进去。

取出来:

Object value =
        list.get(0);

还需要强制类型转换。

很容易出现:

ClassCastException

十七、使用泛型

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

表示:

这个集合主要保存 String

错误:

list.add(100);

编译器直接阻止。

所以泛型的重要价值:

类型安全
减少强制转换
代码更清晰

十八、泛型的钻石语法

完整:

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

Java 7 以后通常写:

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

右边:

<>

叫:

diamond operator
钻石操作符

十九、List

List 是:

有序
可重复
有索引

的集合。

例如:

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

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

允许重复:

Java
MySQL
Java

二十、List 的核心特点

1. 元素有顺序

2. 可以重复

3. 可以通过索引访问

4. 索引从 0 开始

和数组很像。


二十一、List 常用方法

除了 Collection 方法,还经常使用:

get(index)

set(index, value)

add(index, value)

remove(index)

indexOf(value)

lastIndexOf(value)

subList(...)

二十二、get()

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

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

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

输出:

Java

二十三、set()

修改指定索引:

list.set(
        1,
        "Spring"
);

原来:

MySQL

变成:

Spring

二十四、指定位置 add()

list.add(
        1,
        "Redis"
);

原来:

Java
Spring

会变成:

Java
Redis
Spring

后面的元素向后移动。


二十五、remove() 的重载问题

对于:

List<Integer>

要特别注意:

remove(1)

可能表示:

删除索引 1

而不是:

删除值 1

例如:

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

list.add(10);
list.add(20);
list.add(30);

list.remove(1);

删除的是:

20

二十六、删除 Integer 指定值

如果想删除:

值 20

可以:

list.remove(
        Integer.valueOf(20)
);

二十七、indexOf()

int index =
        list.indexOf("Java");

找不到:

-1

二十八、List 遍历方式

常见:

普通 for

增强 for

Iterator

ListIterator

forEach

二十九、普通 for 遍历

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

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

适合:

需要索引

三十、增强 for

for (String item : list) {

    System.out.println(item);
}

适合:

只读取元素

三十一、Iterator 迭代器

Iterator 是 Java 集合非常重要的遍历方式。

例如:

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

三十二、Iterator 基本方法

hasNext()

next()

remove()

三十三、Iterator 遍历

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

while (iterator.hasNext()) {

    String value =
            iterator.next();

    System.out.println(value);
}

三十四、hasNext()

判断:

后面还有没有元素

三十五、next()

获取:

下一个元素

并移动迭代器位置。


三十六、迭代器删除

遍历过程中需要删除当前元素时:

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

while (iterator.hasNext()) {

    String value =
            iterator.next();

    if ("Java".equals(value)) {

        iterator.remove();
    }
}

这是标准做法之一。


三十七、ConcurrentModificationException

看下面代码:

for (String value : list) {

    if ("Java".equals(value)) {

        list.remove(value);
    }
}

在某些情况下会出现:

ConcurrentModificationException

原因:

正在使用迭代器逻辑遍历

同时直接修改集合结构

三十八、为什么增强 for 也会出现并发修改异常

增强 for 遍历集合时:

底层通常就是 Iterator

所以遍历过程中直接:

list.remove(...)

可能破坏迭代器预期结构。


三十九、安全删除方式

方式一:

Iterator.remove()

方式二:

removeIf()

例如:

list.removeIf(
        value ->
                "Java".equals(value)
);

Lambda 后面会继续学习。


四十、ListIterator

ListIterator 是 List 专用迭代器。

它比 Iterator 功能更多。

例如:

ListIterator<String> iterator =
        list.listIterator();

可以:

向前遍历
向后遍历
修改元素
添加元素

四十一、ArrayList

ArrayList 是 Java 最常用集合之一。

基本:

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

特点:

底层主要使用动态数组

四十二、ArrayList 的核心特点

有序
可重复
支持索引
查询快
中间插入删除可能较慢

四十三、ArrayList 为什么查询快

例如:

list.get(100);

ArrayList 底层是:

数组结构

可以根据索引快速定位。

平均意义下:

随机访问非常快

四十四、ArrayList 添加元素

数组长度固定,

但是 ArrayList 可以不断:

add()

原因:

ArrayList 内部会在需要时创建更大的数组,并复制原数据。

所以:

ArrayList 不是数组长度真的变了

而是内部帮我们自动完成扩容

四十五、ArrayList 扩容简单理解

可以简单理解:

原数组空间不够
↓
创建更大的新数组
↓
复制旧数据
↓
继续添加

具体扩容策略属于源码细节,

后面学习源码时再深入。


四十六、ArrayList 适合什么场景

通常适合:

读取多

随机访问多

主要在尾部添加

中间插入删除不频繁

实际业务中:

List 默认优先考虑 ArrayList

往往是一个比较合理的起点。


四十七、LinkedList

LinkedList 是:

双向链表

实现。

例如:

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

四十八、链表简单理解

数组:

[A][B][C][D]

元素连续排列。

链表更像:

A ⇄ B ⇄ C ⇄ D

每个节点会记录:

当前数据
前一个节点
后一个节点

四十九、LinkedList 特点

有序
可重复
支持 List API
也是 Deque 实现

相对于 ArrayList:

随机索引访问较慢
首尾插入删除方便

五十、ArrayList 和 LinkedList 对比

操作 ArrayList LinkedList
底层 动态数组 双向链表
get(index) 快 较慢
尾部添加 通常快 快
首部添加 需要移动元素 较方便
中间插入删除 需移动数据 找到节点后修改链接
内存 相对紧凑 节点额外保存引用

实际开发不要只背:

“增删多用 LinkedList”

因为:

找到删除位置本身也需要成本
现代 CPU 对连续数组访问很友好

多数普通业务场景:

ArrayList 更常见

五十一、Vector

Vector 是较老的 List 实现。

特点:

很多方法带同步

现代普通代码里:

使用频率明显低于 ArrayList

了解即可。


五十二、Stack

传统:

Stack

用于栈结构。

但现代 Java 通常更推荐使用:

Deque
ArrayDeque

实现栈。


五十三、Set

Set 最大特点:

元素不能重复

例如:

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

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

最后通常只有:

Java
MySQL

五十四、Set 常见实现

HashSet

LinkedHashSet

TreeSet

五十五、HashSet

最常用 Set 实现。

例如:

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

特点:

不能重复
通常不保证插入顺序
查询效率较高

五十六、HashSet 去重基础

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

set.add(10);
set.add(20);
set.add(10);

System.out.println(set);

重复的:

10

只保留一个。


五十七、HashSet 如何判断重复

对于对象:

HashSet

主要依赖:

hashCode()

equals()

判断是否重复。

这和前面面向对象章节学习的:

equals / hashCode

直接联系起来。


五十八、自定义对象放入 HashSet

例如:

public class Student {

    private String studentNo;

    private String name;
}

如果希望:

学号相同
就认为是同一个学生

就应该合理重写:

equals()

hashCode()

五十九、HashSet 去重流程简单理解

可以先理解:

添加元素
↓
计算 hashCode
↓
确定大概位置
↓
如果位置可能冲突
再通过 equals 比较
↓
决定是否重复

这不是完整源码细节,但足够理解基本原理。


六十、为什么重写 equals 还要重写 hashCode

规则:

如果两个对象 equals() 为 true,它们的 hashCode() 必须相同。

否则放入:

HashSet
HashMap

时可能出现不符合预期的结果。

因此 IDEA 中通常:

equals 和 hashCode 一起生成

六十一、LinkedHashSet

LinkedHashSet:

在 HashSet 基础上
额外维护元素迭代顺序

通常可以保留:

插入顺序

例如:

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

适合:

既要去重
又希望保留原始添加顺序

六十二、TreeSet

TreeSet 特点:

不能重复
可以排序

例如:

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

set.add(30);
set.add(10);
set.add(20);

输出通常:

[10, 20, 30]

六十三、TreeSet 排序需要规则

对于 Integer:

Java 已经知道怎么比较

对于自定义对象:

必须告诉 TreeSet 如何比较

常见方式:

Comparable

Comparator

六十四、Comparable

让类自己具备:

自然排序能力

接口:

Comparable<T>

例如:

public class Student
        implements Comparable<Student> {

    private int age;

    @Override
    public int compareTo(
            Student other
    ) {

        return Integer.compare(
                this.age,
                other.age
        );
    }
}

六十五、compareTo() 返回规则

可以简单记:

负数
当前对象排前面

0
认为排序上相等

正数
当前对象排后面

六十六、不要直接用减法写 compareTo

不推荐:

return this.age
        - other.age;

虽然很多简单例子能工作,

但大整数情况下存在:

溢出风险

推荐:

Integer.compare(
        this.age,
        other.age
);

六十七、Comparator

如果不想修改原类,

或者需要:

多种排序规则

可以使用:

Comparator

例如:

Comparator<Student> comparator =
        (a, b) ->
                Integer.compare(
                        a.getAge(),
                        b.getAge()
                );

然后:

new TreeSet<>(
        comparator
);

六十八、Comparable 和 Comparator 区别

Comparable
类自己定义默认排序规则

Comparator
外部定义临时或额外排序规则

可以简单记:

Comparable
“我自己会比较”

Comparator
“别人告诉我怎么比较”

六十九、Map

Map 用于保存:

键值对

英文:

key-value

例如:

用户名 → 用户对象

学生学号 → 学生对象

商品 id → 商品对象

配置名称 → 配置值

七十、Map 基础结构

例如:

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

表示:

key:String

value:Integer

七十一、Map 和 Collection 的关系

重点:

Map 不是 Collection 的子接口

它们属于:

两套不同体系

所以不要说:

Map 是 Collection

七十二、Map 常用方法

常见:

put()

get()

remove()

containsKey()

containsValue()

size()

isEmpty()

clear()

keySet()

values()

entrySet()

getOrDefault()

putIfAbsent()

七十三、put()

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

map.put(
        "Java",
        90
);

map.put(
        "MySQL",
        85
);

七十四、key 不能重复

如果:

map.put(
        "Java",
        90
);

map.put(
        "Java",
        100
);

后面的值会覆盖前面的值。

最后:

Java → 100

七十五、value 可以重复

例如:

map.put("Java", 90);
map.put("Spring", 90);

这是允许的。

所以 Map:

key 不能重复

value 可以重复

七十六、get()

根据 key 获取 value:

Integer score =
        map.get("Java");

七十七、get() 找不到

如果 key 不存在:

map.get("Redis");

通常返回:

null

所以:

Integer

自动拆箱成 int 时要小心空指针。

例如:

int score =
        map.get("Redis");

可能:

NullPointerException

七十八、getOrDefault()

可以提供默认值:

int score =
        map.getOrDefault(
                "Redis",
                0
        );

不存在时得到:

0

七十九、containsKey()

if (
    map.containsKey(
            "Java"
    )
) {

}

八十、containsValue()

boolean result =
        map.containsValue(
                90
        );

八十一、remove()

map.remove("Java");

八十二、Map 遍历 key

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

    System.out.println(
            key
    );
}

八十三、Map 遍历 value

for (
    Integer value :
    map.values()
) {

    System.out.println(
            value
    );
}

八十四、Map 遍历 entry

最常用:

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

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

八十五、为什么 entrySet() 常用

如果同时需要:

key
value

使用:

entrySet()

更直观。

不要每次:

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

    Integer value =
            map.get(key);
}

如果本来就需要键值对,

entrySet() 通常更合适。


八十六、Map.forEach()

Java 8 以后:

map.forEach(
        (key, value) -> {

            System.out.println(
                    key + "=" + value
            );
        }
);

Lambda 后面会继续学。


八十七、HashMap

HashMap 是 Java 最常用的 Map 实现。

特点:

key 不重复
value 可重复
查询效率高
通常不保证插入顺序
允许 null key 和 null value

实际业务:

Map 默认优先考虑 HashMap

往往很常见。


八十八、HashMap 的 key 为什么要重写 equals 和 hashCode

HashMap 查找 key 时:

会利用 hashCode 和 equals

如果自定义对象作为 key,

没有正确重写:

equals()

hashCode()

可能出现:

明明业务上相同
却查找不到

八十九、自定义对象作为 HashMap key

例如:

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

如果业务上:

studentNo 相同
就是同一个学生

需要让 Student 的:

equals()
hashCode()

基于学号设计。


九十、HashMap 底层结构简单了解

现代 Java HashMap 可以简单理解为:

数组
+
链表
+
红黑树

大致思路:

根据 hash 找桶位置

冲突少时使用链式结构

冲突严重到一定条件时
可能树化

这里先理解思想,

不要一开始死背所有阈值和源码常量。


九十一、什么是哈希冲突

不同 key:

可能计算到相同或相近桶位置

这种情况叫:

哈希冲突

Java 会使用内部结构继续区分。

所以:

hashCode 相同
不代表 equals 一定相同

九十二、HashMap key 的设计建议

如果对象作为 key:

参与 equals/hashCode 的字段
最好在作为 key 期间保持稳定

不要把对象放入 HashMap 后,

又修改关键字段导致:

hashCode 变化

否则可能:

再也找不到这个 key

九十三、LinkedHashMap

LinkedHashMap:

在 HashMap 基础上
额外维护迭代顺序

常见:

保留插入顺序

也可以用于某些:

访问顺序
LRU 缓存

场景。


九十四、TreeMap

TreeMap:

按照 key 排序

底层基于:

红黑树

key 需要:

自然排序
或 Comparator

九十五、HashMap、LinkedHashMap、TreeMap 对比

类型 顺序 查询 典型用途
HashMap 不保证 快 默认键值存储
LinkedHashMap 常保留插入顺序 快 需要顺序
TreeMap 按 key 排序 对数级 需要有序 key

九十六、Hashtable

Hashtable 是比较老的 Map 实现。

特点:

很多方法同步
不允许 null key/value

现代普通业务代码中:

使用频率远低于 HashMap

多线程场景通常也会优先考虑:

ConcurrentHashMap

而不是 Hashtable。


九十七、Queue

Queue:

队列

通常强调:

先进先出

英文:

FIFO
First In First Out

九十八、Queue 常见方法

添加:

offer()

取出并删除:

poll()

查看队头:

peek()

九十九、Queue 基础案例

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

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

System.out.println(
        queue.poll()
);

输出:

A

一百、Deque

Deque:

双端队列

两端都可以:

添加
删除
查看

常用实现:

ArrayDeque
LinkedList

一百零一、ArrayDeque

例如:

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

可以作为:

队列
栈

使用。


一百零二、用 Deque 实现栈

栈:

后进先出

英文:

LIFO
Last In First Out

例如:

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

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

System.out.println(
        stack.pop()
);

输出:

C

一百零三、PriorityQueue

PriorityQueue:

优先队列

不是简单按照插入顺序出队,

而是根据:

优先级 / 排序规则

决定。

例如:

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

queue.offer(30);
queue.offer(10);
queue.offer(20);

System.out.println(
        queue.poll()
);

结果:

10

一百零四、Collections 工具类

注意:

Collection

是接口。

Collections

是工具类。

它们不是一个东西。


一百零五、Collections.sort()

例如:

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

list.add(30);
list.add(10);
list.add(20);

Collections.sort(list);

结果:

10
20
30

一百零六、List.sort()

现代 Java 也可以:

list.sort(
        Comparator.naturalOrder()
);

或者:

list.sort(comparator);

一百零七、Collections.reverse()

反转:

Collections.reverse(list);

一百零八、Collections.shuffle()

随机打乱:

Collections.shuffle(list);

适合:

洗牌
随机顺序

一百零九、Collections.max() / min()

Integer max =
        Collections.max(list);

Integer min =
        Collections.min(list);

一百一十、Collections.swap()

交换两个索引:

Collections.swap(
        list,
        0,
        1
);

一百一十一、Collections.frequency()

统计元素出现次数:

int count =
        Collections.frequency(
                list,
                "Java"
        );

一百一十二、Collections.unmodifiableList()

可以创建:

不可修改视图

例如:

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

对 readonly 做添加:

readonly.add("Redis");

会抛:

UnsupportedOperationException

一百一十三、List.of()

Java 9 以后:

List<String> list =
        List.of(
                "Java",
                "MySQL",
                "Spring"
        );

得到:

不可变集合

不能:

list.add(...)

一百一十四、Set.of()

Set<String> set =
        Set.of(
                "Java",
                "MySQL"
        );

同样:

不可修改

并且重复元素会直接导致创建失败。


一百一十五、Map.of()

例如:

Map<String, Integer> map =
        Map.of(
                "Java", 90,
                "MySQL", 85
        );

适合:

少量固定配置
只读数据

一百一十六、不可变集合有什么好处

避免误修改
表达数据只读语义
线程共享更安全
API 返回更稳妥

但要注意:

集合本身不可改
不代表里面对象一定不可变

一百一十七、泛型类

我们也可以自己定义泛型类。

例如:

public class Box<T> {

    private T value;

    public T getValue() {

        return value;
    }

    public void setValue(
            T value
    ) {

        this.value = value;
    }
}

使用:

Box<String> box =
        new Box<>();

box.setValue("Java");

一百一十八、T 是什么

T 只是一个:

类型参数名字

常见字母:

T   Type

E   Element

K   Key

V   Value

例如 Map:

Map<K, V>

一百一十九、泛型方法

public static <T>
void print(
        T value
) {

    System.out.println(value);
}

调用:

print("Java");
print(100);

一百二十、泛型通配符 ?

例如:

List<?> list

表示:

某种未知类型的 List

它常用于:

只需要读取
不关心具体元素类型

的通用方法。


一百二十一、? extends

例如:

List<? extends Number>

表示:

Number
或 Number 的某个子类型

例如:

List<Integer>
List<Double>

都可以匹配。


一百二十二、? super

例如:

List<? super Integer>

表示:

Integer
或 Integer 的某个父类型

例如:

List<Integer>
List<Number>
List<Object>

一百二十三、PECS 原则

泛型通配符有一个经典口诀:

Producer Extends

Consumer Super

简称:

PECS

可以简单理解:

主要读取数据
→ ? extends

主要写入数据
→ ? super

初学阶段先理解思想,不必一开始钻太深。


一百二十四、泛型不能直接使用基本类型

错误:

List<int>

正确:

List<Integer>

因为 Java 泛型主要基于:

引用类型

一百二十五、类型擦除简单了解

Java 泛型大部分信息:

主要在编译期做类型检查

运行时很多泛型信息会被:

擦除

这叫:

类型擦除

后面学习反射和泛型底层时再深入。


一百二十六、集合中 null 的问题

不同集合对:

null

支持不同。

例如:

ArrayList
HashMap
HashSet

通常允许某种程度的 null。

而:

TreeSet
TreeMap
List.of
Set.of
Map.of

等场景对 null 限制更严格。

实际开发中:

不要依赖大量 null 作为集合中的正常业务值。


一百二十七、集合中的 equals()

很多集合操作:

contains()
remove()
indexOf()

都会涉及:

equals()

例如:

list.contains(student)

是否能找到,

取决于对象:

equals()

如何定义。


一百二十八、Set / Map 更依赖 hashCode()

特别是:

HashSet
HashMap

高度依赖:

hashCode()
equals()

所以面向对象里的:

equals 和 hashCode

不是单纯“面试题”,而是真正影响集合行为。


一百二十九、List 去重的简单方式

例如:

List<String> list =
        List.of(
                "Java",
                "MySQL",
                "Java"
        );

可以:

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

得到去重结果。

如果还想保留顺序:

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

一百三十、Set 转 List

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

一百三十一、List 转数组

String[] array =
        list.toArray(
                new String[0]
        );

现代 Java 也可以使用其他形式,

初学阶段先掌握这一种。


一百三十二、数组转 List

例如:

String[] array = {
        "Java",
        "MySQL"
};

List<String> list =
        Arrays.asList(array);

注意:

Arrays.asList()
返回的是固定大小视图

不能随便:

add()
remove()

一百三十三、Arrays.asList() 的坑

例如:

List<String> list =
        Arrays.asList(
                "Java",
                "MySQL"
        );

list.add("Redis");

会抛:

UnsupportedOperationException

如果需要可变 ArrayList:

List<String> list =
        new ArrayList<>(
                Arrays.asList(
                        "Java",
                        "MySQL"
                )
        );

一百三十四、基本类型数组和 Arrays.asList()

例如:

int[] array = {
        1, 2, 3
};

List<int[]> list =
        Arrays.asList(array);

它不会变成:

List<Integer>

因为:

int[] 本身被当成一个对象

所以基本类型数组转换集合要特别注意。


一百三十五、排序自定义对象

例如 Student:

public class Student {

    private String name;

    private int age;

    private double score;

    // Getter / Setter
}

按照年龄排序:

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

这里的方法引用后面会学习。


一百三十六、倒序排序

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

一百三十七、多字段排序

先按分数降序,

分数相同按年龄升序:

students.sort(
        Comparator
                .comparingDouble(
                        Student::getScore
                )
                .reversed()
                .thenComparingInt(
                        Student::getAge
                )
);

这种写法在实际项目里很常见。


一百三十八、集合综合案例:学生管理

Student:

public class Student {

    private String studentNo;

    private String name;

    private int age;

    private double score;

    public Student(
            String studentNo,
            String name,
            int age,
            double score
    ) {

        this.studentNo =
                studentNo;

        this.name = name;

        this.age = age;

        this.score = score;
    }

    public String getStudentNo() {

        return studentNo;
    }

    public String getName() {

        return name;
    }

    public int getAge() {

        return age;
    }

    public double getScore() {

        return score;
    }

    @Override
    public String toString() {

        return "Student{"
                + "studentNo='"
                + studentNo
                + '\''
                + ", name='"
                + name
                + '\''
                + ", age="
                + age
                + ", score="
                + score
                + '}';
    }
}

一百三十九、使用 List 保存学生

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

students.add(
        new Student(
                "001",
                "张三",
                20,
                90
        )
);

students.add(
        new Student(
                "002",
                "李四",
                21,
                85
        )
);

一百四十、按学号查找学生

String targetNo =
        "002";

Student found = null;

for (
    Student student :
    students
) {

    if (
        targetNo.equals(
                student.getStudentNo()
        )
    ) {

        found = student;

        break;
    }
}

一百四十一、如果经常按学号查询怎么办

如果业务中大量:

学号 → 学生

查询,

使用:

Map<String, Student>

可能更加合适。

例如:

Map<String, Student> studentMap =
        new HashMap<>();

studentMap.put(
        "001",
        new Student(
                "001",
                "张三",
                20,
                90
        )
);

查找:

Student student =
        studentMap.get("001");

一百四十二、选择集合要根据访问方式

如果需求:

按顺序保存学生

使用:

List<Student>

如果:

学号必须唯一

可以考虑:

Set

或:

Map

如果:

经常通过学号快速找学生

更适合:

Map<String, Student>

一百四十三、综合案例:单词统计

字符串:

java spring java mysql java spring

要求统计每个单词出现次数。

使用:

Map<String, Integer>

代码:

String text =
        "java spring java mysql java spring";

String[] words =
        text.split(" ");

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

for (String word : words) {

    int count =
            countMap.getOrDefault(
                    word,
                    0
            );

    countMap.put(
            word,
            count + 1
    );
}

System.out.println(
        countMap
);

结果类似:

{java=3, spring=2, mysql=1}

一百四十四、使用 merge() 简化统计

Map 还提供:

merge()

可以写:

for (String word : words) {

    countMap.merge(
            word,
            1,
            Integer::sum
    );
}

这种写法更简洁,

方法引用后面继续学习。


一百四十五、综合案例:去重并保序

原数据:

List<String> names =
        Arrays.asList(
                "张三",
                "李四",
                "张三",
                "王五",
                "李四"
        );

使用:

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

得到:

张三
李四
王五

一百四十六、综合案例:排行榜

假设:

用户积分

需要按积分从高到低排序。

可以:

List<User> users =
        new ArrayList<>();

然后:

users.sort(
        Comparator
                .comparingInt(
                        User::getScore
                )
                .reversed()
);

如果需要:

排名

再使用普通 for:

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

    User user =
            users.get(i);

    System.out.println(
            "第 "
                    + (i + 1)
                    + " 名:"
                    + user.getName()
    );
}

一百四十七、综合案例:购物车

购物车可以用:

Map<Long, CartItem>

其中:

key
商品 ID

value
购物车项

如果重复加入同一个商品:

不新增 key

只更新数量

这就是 Map 在业务中的典型用法。


一百四十八、综合案例:按学院分组学生

例如:

计算机学院
→ 多个学生

外国语学院
→ 多个学生

可以使用:

Map<
    String,
    List<Student>
>

即:

key
学院名称

value
学生列表

这是非常常见的:

一对多

内存数据结构。


一百四十九、嵌套集合

集合可以嵌套:

List<List<String>>
Map<String, List<Student>>
Map<String, Map<String, Integer>>

真实业务中很常见。

但不要为了“高级”设计得过深。

如果嵌套结构越来越复杂,

通常应该考虑:

定义业务类

让数据结构更清晰。


一百五十、集合和面向对象结合

不推荐大量使用:

Map<String, Object>

乱装业务字段。

例如:

Map<String, Object> user =
        new HashMap<>();

然后:

name
age
phone
school

全靠字符串 key。

更好的方式通常是:

User

类。

集合应该主要负责:

保存多个对象
组织对象之间的结构