|
@@ -89,30 +89,58 @@ try {
|
|
|
- 一旦匹配到某个异常类型,就执行对应的 catch 块,然后跳出 try-catch 结构
|
|
- 一旦匹配到某个异常类型,就执行对应的 catch 块,然后跳出 try-catch 结构
|
|
|
- 建议将 **子类异常放在前面,父类异常放在后面**,否则父类异常会"拦住"子类异常,导致子类 catch 块不可达
|
|
- 建议将 **子类异常放在前面,父类异常放在后面**,否则父类异常会"拦住"子类异常,导致子类 catch 块不可达
|
|
|
|
|
|
|
|
-#### 代码示例
|
|
|
|
|
|
|
+#### 代码示例(更新版——含异常常用方法)
|
|
|
|
|
|
|
|
```java
|
|
```java
|
|
|
int a = 0;
|
|
int a = 0;
|
|
|
|
|
+int[] arr;
|
|
|
try {
|
|
try {
|
|
|
- int[] arr = {};
|
|
|
|
|
|
|
+ arr = new int[]{};
|
|
|
arr[3] = 5; // 数组下标越界 → ArrayIndexOutOfBoundsException
|
|
arr[3] = 5; // 数组下标越界 → ArrayIndexOutOfBoundsException
|
|
|
|
|
+ System.out.println("55555555555555555"); // 不会执行
|
|
|
} catch (ArithmeticException e) {
|
|
} catch (ArithmeticException e) {
|
|
|
System.out.println("不能除以0");
|
|
System.out.println("不能除以0");
|
|
|
System.out.println("以及一些其他的异常处理操作.....");
|
|
System.out.println("以及一些其他的异常处理操作.....");
|
|
|
|
|
+ System.out.println(e.getMessage()); // 获取异常描述信息
|
|
|
} catch (RuntimeException e) {
|
|
} catch (RuntimeException e) {
|
|
|
System.out.println("出现了运行时异常");
|
|
System.out.println("出现了运行时异常");
|
|
|
|
|
+ System.out.println("输出的异常信息:" + e.getMessage()); // 异常描述
|
|
|
|
|
+ System.out.println("输出完整的异常信息:" + e.fillInStackTrace()); // 完整堆栈追踪
|
|
|
|
|
+ System.out.println(e.getCause()); // 异常原因(若无返回null)
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
System.out.println("出现了异常..但不是运行时异常");
|
|
System.out.println("出现了异常..但不是运行时异常");
|
|
|
|
|
+ System.out.println(e.getMessage());
|
|
|
}
|
|
}
|
|
|
System.out.println(a);
|
|
System.out.println(a);
|
|
|
System.out.println("........后续的代码正在运行................");
|
|
System.out.println("........后续的代码正在运行................");
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-> 📄 来源:`Demo01.java`
|
|
|
|
|
|
|
+> 📄 来源:`Demo01.java`(更新后)
|
|
|
|
|
|
|
|
**输出结果分析:** 由于 `arr[3] = 5` 抛出的是 `ArrayIndexOutOfBoundsException`,它是 `RuntimeException` 的子类,因此会被第二个 catch 块捕获,输出"出现了运行时异常",然后程序继续执行后续代码。
|
|
**输出结果分析:** 由于 `arr[3] = 5` 抛出的是 `ArrayIndexOutOfBoundsException`,它是 `RuntimeException` 的子类,因此会被第二个 catch 块捕获,输出"出现了运行时异常",然后程序继续执行后续代码。
|
|
|
|
|
|
|
|
-### 1.5 异常处理方式二:throws 抛出
|
|
|
|
|
|
|
+### 1.5 异常常用方法(Throwable 常用 API)
|
|
|
|
|
+
|
|
|
|
|
+当 catch 捕获到异常对象 `e` 后,可以调用 `Throwable` 类提供的方法来获取异常的详细信息:
|
|
|
|
|
+
|
|
|
|
|
+| 方法 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `String getMessage()` | 返回异常的详细描述信息(最常用) |
|
|
|
|
|
+| `Throwable fillInStackTrace()` | 返回完整的堆栈追踪信息 |
|
|
|
|
|
+| `Throwable getCause()` | 返回异常的原因(若无 cause 则返回 null) |
|
|
|
|
|
+| `void printStackTrace()` | 将异常的堆栈追踪信息输出到标准错误流 |
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+catch (RuntimeException e) {
|
|
|
|
|
+ System.out.println(e.getMessage()); // 如:"Index 3 out of bounds for length 0"
|
|
|
|
|
+ System.out.println(e.fillInStackTrace()); // 完整堆栈信息
|
|
|
|
|
+ System.out.println(e.getCause()); // 异常原因(通常为 null)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> 📄 来源:`Demo01.java`(catch 块中的新增调用)
|
|
|
|
|
+
|
|
|
|
|
+### 1.6 异常处理方式二:throws 抛出
|
|
|
|
|
|
|
|
我们不处理异常,交给调用者去处理。如果方法内包含编译期异常,可以不在方法内处理,只使用 `throws` 抛出:
|
|
我们不处理异常,交给调用者去处理。如果方法内包含编译期异常,可以不在方法内处理,只使用 `throws` 抛出:
|
|
|
- 谁调用了这个方法,谁去处理
|
|
- 谁调用了这个方法,谁去处理
|
|
@@ -127,7 +155,7 @@ public void method() throws IOException {
|
|
|
|
|
|
|
|
> 📄 来源:`Demo01.java`(概念说明)
|
|
> 📄 来源:`Demo01.java`(概念说明)
|
|
|
|
|
|
|
|
-### 1.6 异常处理的编程思想 — 鲁棒性(健壮性)
|
|
|
|
|
|
|
+### 1.7 异常处理的编程思想 — 鲁棒性(健壮性)
|
|
|
|
|
|
|
|
- **处理异常**:能处理就处理,处理不了的也要让程序能够继续运行,这才是最重要的
|
|
- **处理异常**:能处理就处理,处理不了的也要让程序能够继续运行,这才是最重要的
|
|
|
- **鲁棒性(Robustness)**:程序出现异常但能够处理或能够正常运行,体现程序的健壮性
|
|
- **鲁棒性(Robustness)**:程序出现异常但能够处理或能够正常运行,体现程序的健壮性
|
|
@@ -137,16 +165,180 @@ public void method() throws IOException {
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 二、课后作业回顾 — StringUtilTest 字符串工具测试
|
|
|
|
|
|
|
+## 二、自定义异常(Custom Exception)
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 为什么需要自定义异常
|
|
|
|
|
+
|
|
|
|
|
+当 Java 官方提供的异常类不能满足我们的业务需求时,可以**自定义异常类**。例如,在年龄校验场景中,需要一个 `AgeOutOfBoundsException` 来表示年龄越界的业务异常。
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 自定义异常的实现步骤
|
|
|
|
|
+
|
|
|
|
|
+| 步骤 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| 1 | 定义(创建)异常类 |
|
|
|
|
|
+| 2 | 编写继承关系:继承 `Exception`(编译期异常)或继承 `RuntimeException`(运行时异常) |
|
|
|
|
|
+| 3 | 提供 **无参构造方法** |
|
|
|
|
|
+| 4 | 提供 **有参构造方法**(带 String message 参数) |
|
|
|
|
|
+
|
|
|
|
|
+> 📄 来源:`Demo02.java`
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 自定义异常代码示例
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+// 自定义异常类 — 继承 Exception(编译期异常)
|
|
|
|
|
+public class AgeOutOfBoundsException extends Exception {
|
|
|
|
|
+
|
|
|
|
|
+ // 无参构造方法
|
|
|
|
|
+ public AgeOutOfBoundsException() {
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 有参构造方法 — 传入异常描述信息
|
|
|
|
|
+ public AgeOutOfBoundsException(String message) {
|
|
|
|
|
+ super(message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 带 cause 的构造方法
|
|
|
|
|
+ public AgeOutOfBoundsException(String message, Throwable cause) {
|
|
|
|
|
+ super(message, cause);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 仅带 cause 的构造方法
|
|
|
|
|
+ public AgeOutOfBoundsException(Throwable cause) {
|
|
|
|
|
+ super(cause);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> 📄 来源:`AgeOutOfBoundsException.java`
|
|
|
|
|
+
|
|
|
|
|
+**注意事项:**
|
|
|
|
|
+- 继承 `Exception` → 该类为 **编译期异常(Checked Exception)**,调用者必须处理(try-catch 或 throws)
|
|
|
|
|
+- 继承 `RuntimeException` → 该类为 **运行时异常(Unchecked Exception)**,编译器不强制处理
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 三、throw 和 throws 关键字
|
|
|
|
|
+
|
|
|
|
|
+### 3.1 throw — 手动抛出异常
|
|
|
|
|
+
|
|
|
|
|
+`throw` 关键字用于在 **方法体内部** 手动抛出一个异常对象。
|
|
|
|
|
+
|
|
|
|
|
+#### 语法格式
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+throw new 异常类名();
|
|
|
|
|
+throw new 异常类名("异常描述信息");
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 使用场景
|
|
|
|
|
+
|
|
|
|
|
+当方法内部检测到某种业务条件不满足时,手动创建异常对象并抛出。
|
|
|
|
|
+
|
|
|
|
|
+#### 代码示例(Person 类的 setAge 方法)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public class Person {
|
|
|
|
|
+ private String name;
|
|
|
|
|
+ private int age;
|
|
|
|
|
+
|
|
|
|
|
+ // setAge 方法 — 使用 throw 手动抛出异常
|
|
|
|
|
+ public void setAge(int age) throws AgeOutOfBoundsException {
|
|
|
|
|
+ if (age < 0 || age > 130) {
|
|
|
|
|
+ // 手动创建异常对象并抛出
|
|
|
|
|
+ throw new AgeOutOfBoundsException("年龄越界了,最小是1岁,最大是130岁");
|
|
|
|
|
+ }
|
|
|
|
|
+ this.age = age;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> 📄 来源:`Person.java`
|
|
|
|
|
+
|
|
|
|
|
+**逻辑说明:**
|
|
|
|
|
+- `if (age < 0 || age > 130)` 为 `true`,表示年龄不合法
|
|
|
|
|
+- `throw new AgeOutOfBoundsException(...)` — 手动创建异常对象并抛出,程序跳转到异常处理流程
|
|
|
|
|
+- 如果 `throw` 被执行,后续的 `this.age = age;` 将不会执行
|
|
|
|
|
+
|
|
|
|
|
+### 3.2 throws — 声明可能抛出的异常
|
|
|
|
|
+
|
|
|
|
|
+`throws` 关键字用在 **方法声明后面**,后面跟着异常类名,表示该方法可能会抛出某种异常,调用者需要对此做准备。
|
|
|
|
|
+
|
|
|
|
|
+#### 语法格式
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public void 方法名(参数列表) throws 异常类名1, 异常类名2 {
|
|
|
|
|
+ // 方法体
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 代码示例
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public class Demo03 {
|
|
|
|
|
+ public static void main(String[] args) throws AgeOutOfBoundsException {
|
|
|
|
|
+ Person p = new Person();
|
|
|
|
|
+ p.setName("张三");
|
|
|
|
|
+ p.setAge(200); // 因为 setAge 声明了 throws AgeOutOfBoundsException
|
|
|
|
|
+ // 所以编译期会提示需要处理(try-catch 或继续 throws)
|
|
|
|
|
+ System.out.println("姓名:" + p.getName());
|
|
|
|
|
+ System.out.println("年龄:" + p.getAge());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> 📄 来源:`Demo03.java`
|
|
|
|
|
+
|
|
|
|
|
+### 3.3 throw 与 throws 的区别对比
|
|
|
|
|
+
|
|
|
|
|
+| 对比维度 | `throw` | `throws` |
|
|
|
|
|
+|---------|---------|----------|
|
|
|
|
|
+| **位置** | 方法体 **内部** | 方法声明 **后面** |
|
|
|
|
|
+| **后面跟的** | 异常 **对象**(`new XXXException()`) | 异常 **类名**(`XXXException`) |
|
|
|
|
|
+| **作用** | 手动 **抛出** 异常对象 | **声明** 方法可能抛出的异常 |
|
|
|
|
|
+| **数量** | 一次只能抛出一个异常对象 | 一次可以声明多个异常(用逗号分隔) |
|
|
|
|
|
+| **使用方式** | `throw new 异常类名();` | `throws 异常类名1, 异常类名2` |
|
|
|
|
|
+
|
|
|
|
|
+**一句话总结:**
|
|
|
|
|
+- **`throw`** — 在方法内部 **实际抛出** 一个异常("干活的")
|
|
|
|
|
+- **`throws`** — 在方法签名上 **声明** 有风险,提醒调用者去处理("贴告示的")
|
|
|
|
|
+
|
|
|
|
|
+> 📄 来源:`Demo03.java`
|
|
|
|
|
+
|
|
|
|
|
+### 3.4 自定义异常 + throw/throws 的完整调用链路
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+【自定义异常类】 AgeOutOfBoundsException extends Exception
|
|
|
|
|
+ ↑ 继承
|
|
|
|
|
+【Person.setAge()】 throw new AgeOutOfBoundsException("...") ← 使用 throw 手动抛出
|
|
|
|
|
+ ↑ 声明
|
|
|
|
|
+ public void setAge(int age) throws AgeOutOfBoundsException
|
|
|
|
|
+ ↑ 调用
|
|
|
|
|
+【Demo03.main()】 有 choices:
|
|
|
|
|
+ a) try-catch 处理
|
|
|
|
|
+ b) 继续 throws 给 JVM
|
|
|
|
|
+ ↑ 最终
|
|
|
|
|
+【JVM】 默认处理:打印异常信息 + 终止程序
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**处理选择:**
|
|
|
|
|
+- 调用 `setAge(20)` 时,编译期会提示该方法可能有异常
|
|
|
|
|
+- 可以选择 **try-catch 自己处理**
|
|
|
|
|
+- 也可以选择 **不处理,继续往上抛**(`throws`),最终抛给 JVM
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 四、课后作业回顾 — StringUtilTest 字符串工具测试
|
|
|
|
|
|
|
|
-### 2.1 需求说明
|
|
|
|
|
|
|
+### 4.1 需求说明
|
|
|
|
|
|
|
|
给定有前后空格和重复项的字符串数据 `" 张三, 李四, 王五, 赵六, 张三, 钱七 "`,需要:
|
|
给定有前后空格和重复项的字符串数据 `" 张三, 李四, 王五, 赵六, 张三, 钱七 "`,需要:
|
|
|
1. 去除每个元素的前后空格
|
|
1. 去除每个元素的前后空格
|
|
|
2. 去重(去除重复的姓名)
|
|
2. 去重(去除重复的姓名)
|
|
|
3. 统计每个姓名的出现次数
|
|
3. 统计每个姓名的出现次数
|
|
|
|
|
|
|
|
-### 2.2 相关 API
|
|
|
|
|
|
|
+### 4.2 相关 API
|
|
|
|
|
|
|
|
| 方法 | 说明 |
|
|
| 方法 | 说明 |
|
|
|
|------|------|
|
|
|------|------|
|
|
@@ -154,7 +346,7 @@ public void method() throws IOException {
|
|
|
| `String.trim()` | 去除字符串前后的空白字符 |
|
|
| `String.trim()` | 去除字符串前后的空白字符 |
|
|
|
| `Arrays.toString(Object[] a)` | 将数组转换为字符串表示形式 |
|
|
| `Arrays.toString(Object[] a)` | 将数组转换为字符串表示形式 |
|
|
|
|
|
|
|
|
-### 2.3 核心算法
|
|
|
|
|
|
|
+### 4.3 核心算法
|
|
|
|
|
|
|
|
#### 三步去重算法
|
|
#### 三步去重算法
|
|
|
|
|
|
|
@@ -223,20 +415,20 @@ for (int i = 0; i < newArr.length; i++) {
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 三、课后作业回顾 — DataMathTest 日期时间计算
|
|
|
|
|
|
|
+## 五、课后作业回顾 — DataMathTest 日期时间计算
|
|
|
|
|
|
|
|
-### 3.1 需求说明
|
|
|
|
|
|
|
+### 5.1 需求说明
|
|
|
|
|
|
|
|
计算当前日期距离 2026-12-31 还有多少天。
|
|
计算当前日期距离 2026-12-31 还有多少天。
|
|
|
|
|
|
|
|
-### 3.2 相关 API
|
|
|
|
|
|
|
+### 5.2 相关 API
|
|
|
|
|
|
|
|
| 方法 | 说明 |
|
|
| 方法 | 说明 |
|
|
|
|------|------|
|
|
|------|------|
|
|
|
| `LocalDate.of(int year, int month, int dayOfMonth)` | 创建指定日期的 `LocalDate` 对象 |
|
|
| `LocalDate.of(int year, int month, int dayOfMonth)` | 创建指定日期的 `LocalDate` 对象 |
|
|
|
| `LocalDate.toEpochDay()` | 返回从 1970-01-01 到当前日期的天数 |
|
|
| `LocalDate.toEpochDay()` | 返回从 1970-01-01 到当前日期的天数 |
|
|
|
|
|
|
|
|
-### 3.3 代码实现
|
|
|
|
|
|
|
+### 5.3 代码实现
|
|
|
|
|
|
|
|
```java
|
|
```java
|
|
|
LocalDate localDate = LocalDate.of(2026, 7, 27);
|
|
LocalDate localDate = LocalDate.of(2026, 7, 27);
|
|
@@ -257,17 +449,21 @@ System.out.println("距离 2026-12-31 还有:" + (epochDay - now) + "天");
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 四、随堂练习要点
|
|
|
|
|
|
|
+## 六、随堂练习要点
|
|
|
|
|
|
|
|
-### 4.1 异常处理练习要点
|
|
|
|
|
|
|
+### 6.1 异常处理练习要点
|
|
|
|
|
|
|
|
- 理解 `Throwable` → `Exception` → `RuntimeException` 的继承体系
|
|
- 理解 `Throwable` → `Exception` → `RuntimeException` 的继承体系
|
|
|
- 区分编译期异常和运行时异常
|
|
- 区分编译期异常和运行时异常
|
|
|
- 掌握 `try-catch` 多 catch 块的写法,注意异常类型顺序(子类→父类)
|
|
- 掌握 `try-catch` 多 catch 块的写法,注意异常类型顺序(子类→父类)
|
|
|
- 理解 `JVM` 默认处理异常的方式(打印信息 + 终止程序)
|
|
- 理解 `JVM` 默认处理异常的方式(打印信息 + 终止程序)
|
|
|
|
|
+- 熟悉异常常用方法:`getMessage()`、`fillInStackTrace()`、`getCause()`
|
|
|
|
|
+- 掌握 `throw` 手动抛出异常对象(在方法体内部使用)
|
|
|
|
|
+- 掌握 `throws` 声明异常(在方法签名上使用),理解两者的区别
|
|
|
|
|
+- 掌握自定义异常的创建步骤(继承 Exception → 无参/有参构造方法)
|
|
|
- 体会异常处理对程序 **鲁棒性(健壮性)** 的提升
|
|
- 体会异常处理对程序 **鲁棒性(健壮性)** 的提升
|
|
|
|
|
|
|
|
-### 4.2 字符串处理综合练习要点
|
|
|
|
|
|
|
+### 6.2 字符串处理综合练习要点
|
|
|
|
|
|
|
|
- 使用 `split(",")` 分割带分隔符的字符串
|
|
- 使用 `split(",")` 分割带分隔符的字符串
|
|
|
- 使用 `trim()` 去除字符串前后空格
|
|
- 使用 `trim()` 去除字符串前后空格
|
|
@@ -275,7 +471,7 @@ System.out.println("距离 2026-12-31 还有:" + (epochDay - now) + "天");
|
|
|
- 掌握元素出现次数的统计算法
|
|
- 掌握元素出现次数的统计算法
|
|
|
- 理解 `equals()` 在字符串比较中的必要性
|
|
- 理解 `equals()` 在字符串比较中的必要性
|
|
|
|
|
|
|
|
-### 4.3 日期时间计算练习要点
|
|
|
|
|
|
|
+### 6.3 日期时间计算练习要点
|
|
|
|
|
|
|
|
- 使用 `LocalDate.of()` 创建指定日期
|
|
- 使用 `LocalDate.of()` 创建指定日期
|
|
|
- 使用 `toEpochDay()` 计算两个日期之间的天数差
|
|
- 使用 `toEpochDay()` 计算两个日期之间的天数差
|
|
@@ -283,10 +479,11 @@ System.out.println("距离 2026-12-31 还有:" + (epochDay - now) + "天");
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 五、拓展阅读
|
|
|
|
|
|
|
+## 七、拓展阅读
|
|
|
|
|
|
|
|
-1. **Java 异常处理最佳实践** — 何时使用 checked vs unchecked exception
|
|
|
|
|
-2. **try-with-resources** — JDK7+ 自动关闭资源的异常处理方式
|
|
|
|
|
-3. **自定义异常** — 继承 `Exception` 或 `RuntimeException` 创建业务异常
|
|
|
|
|
-4. **`Period` 与 `Duration`** — Java 8 日期时间 API 中更灵活的时间间隔计算
|
|
|
|
|
-5. **`LinkedHashSet` 去重** — 利用集合类简化去重操作(替代手动数组去重)
|
|
|
|
|
|
|
+1. **Java 异常处理最佳实践** — 何时使用 checked vs unchecked exception,何时自定义异常
|
|
|
|
|
+2. **try-with-resources** — JDK7+ 自动关闭资源的异常处理方式(`try(Resource r = new Resource()){...}`)
|
|
|
|
|
+3. **异常链(Exception Chaining)** — 通过 `initCause()` 或构造方法将底层异常包装为高层异常
|
|
|
|
|
+4. **`Objects.requireNonNull()`** — 一种优雅避免 NullPointerException 的方式(JDK7+)
|
|
|
|
|
+5. **`Period` 与 `Duration`** — Java 8 日期时间 API 中更灵活的时间间隔计算
|
|
|
|
|
+6. **`LinkedHashSet` 去重** — 利用集合类简化去重操作(替代手动数组去重)
|