# JavaSE 基础 — Day12 笔记 > **日期**:2026-07-28 > **项目**:`c260728` > **作者**:WanJL --- ## 一、Java 异常处理(Exception Handling) ### 1.1 异常的概念 **异常(Exception)** 就是程序出现不正常的情况。 在 Java 中,异常和错误都继承自 `Throwable` 类: ``` Throwable(顶级父类) ├── Exception(异常类 — 程序本身可以处理的问题) │ ├── RuntimeException(运行时异常) │ │ ├── ArithmeticException(算术异常,如除零) │ │ ├── ArrayIndexOutOfBoundsException(数组下标越界) │ │ ├── NullPointerException(空指针异常) │ │ └── ... │ └── 非 RuntimeException(编译期异常 — 编译时必须处理) │ ├── IOException │ ├── SQLException │ └── ... └── Error(错误类 — 严重问题,代码无法处理) ├── StackOverflowError(栈溢出,如无限制递归) ├── OutOfMemoryError(内存溢出) └── ... ``` > 📄 来源:`Demo01.java` **核心要点:** - **`Exception`** 及其子类可以被我们捕获处理或抛出,是程序可以处理的问题。 - **`Error`** 是内存级别的严重问题,不是我们可以捕获抛出的,需要改代码。 - 只有 `Exception` 类或其子类,才可以被我们捕获处理或抛出。 ### 1.2 编译期异常 vs 运行时异常 | 类型 | 说明 | 示例 | |------|------|------| | **编译期异常**(Checked Exception) | 编译时就会报错,必须处理(捕获/抛出)才能通过编译 | `IOException`, `SQLException` | | **运行时异常**(Runtime Exception) | 编译时不报错,运行后才报错。所有运行时异常的父类是 `RuntimeException` | `ArithmeticException`, `NullPointerException`, `ArrayIndexOutOfBoundsException` | **Java 程序执行流程:** ```text .java(源代码) ---[javac 编译]---> .class(字节码) ---[java 解释]---> 运行结果 编译期 运行期 ``` - **编译期异常**:在 Java 源代码编译成字节码的过程中出现,必须处理。 - **运行期异常**:编译阶段没有任何错误,但在从字节码文件解释运行获取结果期间出现错误。 > 📄 来源:`Demo01.java` ### 1.3 JVM 对异常的默认处理 如果程序出了问题,我们不做任何处理,最终会由 **JVM 做默认处理**,处理步骤: 1. 把 **异常的名称、错误原因、以及异常出现的位置** 等信息输出在控制台 2. **程序停止运行** > 📄 来源:`Demo01.java` ### 1.4 异常处理方式一:try-catch 捕获处理 #### 语法格式 ```java try { // 可能出现异常的代码 } catch (异常类型1 变量名) { // 异常类型1的处理方式 } catch (异常类型2 变量名) { // 异常类型2的处理方式 } catch (异常类型n 变量名) { // 异常类型n的处理方式 } ``` #### 多个 catch 块的执行顺序 - 多个 `catch` 块遵循 **从上到下匹配** 的原则 - 一旦匹配到某个异常类型,就执行对应的 catch 块,然后跳出 try-catch 结构 - 建议将 **子类异常放在前面,父类异常放在后面**,否则父类异常会"拦住"子类异常,导致子类 catch 块不可达 #### 代码示例(更新版——含异常常用方法) ```java int a = 0; int[] arr; try { arr = new int[]{}; arr[3] = 5; // 数组下标越界 → ArrayIndexOutOfBoundsException System.out.println("55555555555555555"); // 不会执行 } catch (ArithmeticException e) { System.out.println("不能除以0"); System.out.println("以及一些其他的异常处理操作....."); System.out.println(e.getMessage()); // 获取异常描述信息 } catch (RuntimeException e) { System.out.println("出现了运行时异常"); System.out.println("输出的异常信息:" + e.getMessage()); // 异常描述 System.out.println("输出完整的异常信息:" + e.fillInStackTrace()); // 完整堆栈追踪 System.out.println(e.getCause()); // 异常原因(若无返回null) } catch (Exception e) { System.out.println("出现了异常..但不是运行时异常"); System.out.println(e.getMessage()); } System.out.println(a); System.out.println("........后续的代码正在运行................"); ``` > 📄 来源:`Demo01.java`(更新后) **输出结果分析:** 由于 `arr[3] = 5` 抛出的是 `ArrayIndexOutOfBoundsException`,它是 `RuntimeException` 的子类,因此会被第二个 catch 块捕获,输出"出现了运行时异常",然后程序继续执行后续代码。 ### 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` 抛出: - 谁调用了这个方法,谁去处理 - 调用者如果不处理,可以继续抛出 - 最终抛给 JVM,JVM 进行默认的处理 ```java public void method() throws IOException { // 方法体中的编译期异常不处理,交给调用者 } ``` > 📄 来源:`Demo01.java`(概念说明) ### 1.7 异常处理的编程思想 — 鲁棒性(健壮性) - **处理异常**:能处理就处理,处理不了的也要让程序能够继续运行,这才是最重要的 - **鲁棒性(Robustness)**:程序出现异常但能够处理或能够正常运行,体现程序的健壮性 - 实际开发场景:兜住异常,记录日志,给开发人员发邮件 > 📄 来源:`Demo01.java` ### 1.8 finally — 一定会被执行的代码 #### 为什么需要 finally 无论是自己处理异常(try-catch),还是抛出异常(throw),**异常代码后面的代码一般都不会继续执行了**。但有些代码是**必须执行**的,比如: - 关闭资源(`Scanner`、文件流、数据库连接等) - 关闭线程 - 释放锁 `finally` 块就可以保证这些代码**无论是否发生异常,都一定会被执行**。 #### 语法格式 — 三种组合 | 组合 | 格式 | 说明 | |------|------|------| | **最完整** | `try { ... } catch (E e) { ... } finally { ... }` | 捕获异常 + 释放资源 | | **基本** | `try { ... } catch (E e) { ... }` | 仅捕获处理异常 | | **扫描但不处理** | `try { ... } finally { ... }` | 不捕获异常,但确保资源关闭(异常会继续向上抛) | #### 代码示例 ```java Scanner sc = new Scanner(System.in); System.out.println("请输入:"); String s = sc.next(); System.out.println("您输出的是:" + s); try { int i = 10 / 0; // 会抛出 ArithmeticException } catch (Exception e) { System.out.println(e.getMessage()); // 捕获并处理 } finally { // finally 块中的代码一定会被执行 sc.close(); // 关闭 Scanner 资源 System.out.println("关闭资源...."); } ``` > 📄 来源:`Demo04.java` **关键特点:** - `finally` 块中的代码**无论 try 块是否发生异常,都一定会执行** - 即使 catch 块中有 `return` 语句,`finally` 块也会在 `return` 之前执行 - `finally` 块通常用于**资源清理**,如关闭 `Scanner`、关闭文件流等 --- ## 二、自定义异常(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 --- ## 四、Object 类(java.lang.Object)—— 所有类的根父类 ### 4.1 Object 类概述 `java.lang.Object` 是 Java 中 **所有类的父类**(根父类)。Java 中任何一个类如果没有显式继承其他类,默认都继承自 `Object` 类。 #### 常用方法 | 方法 | 说明 | |------|------| | `boolean equals(Object obj)` | 比较两个对象是否"相等" | | `int hashCode()` | 返回对象的哈希码值 | | `String toString()` | 返回对象的字符串表示形式 | > 📄 来源:`course/api/Demo01.java` ### 4.2 equals() 方法 #### Object 默认实现 在 `Object` 类中,`equals()` 的默认实现使用 `==` 比较,即 **比较两个对象的引用地址**(哈希码值)是否相等。 ```java // Object 类中 equals 的默认实现 public boolean equals(Object obj) { return (this == obj); } ``` #### 重写 equals() 的场景 在实际开发中,我们通常希望 **按照业务规则** 来判断两个对象是否相等。例如,两个 `Person` 对象如果姓名和年龄相同,就应该视为同一个人。 ```java Person p = new Person("张三", 25); Person p1 = new Person("李四", 12); System.out.println(p == p1); // false — == 比较的是地址值 System.out.println(p.equals(p1)); // false — 未重写时与 == 相同 String s1 = new String("张三"); String s2 = new String("张三"); System.out.println(s1 == s2); // false — 两个不同的 String 对象 System.out.println(s1.equals(s2)); // true — String 类已重写 equals ``` > 📄 来源:`course/api/Demo01.java` #### 重写 equals() 的规范 ```java @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } ``` > 📄 来源:`course/api/Person.java` ### 4.3 hashCode() 方法 `hashCode()` 返回对象的 **哈希码值**,在 `Object` 类中默认返回的是该对象在内存中的地址。 #### 重写规则 - **如果两个对象通过 `equals()` 比较相等,那么它们的 `hashCode()` 值必须相等** - 重写 `equals()` 时一般都要同时重写 `hashCode()` ```java @Override public int hashCode() { return Objects.hash(name, age); // 使用 Objects.hash() 工具方法 } ``` > 📄 来源:`course/api/Person.java` #### Objects 工具类(JDK7+) `java.util.Objects` 提供了很多安全操作对象的方法: | 方法 | 说明 | |------|------| | `Objects.equals(Object a, Object b)` | 安全的 equals 比较(自动处理 null) | | `Objects.hash(Object... values)` | 根据多个字段生成哈希码 | | `Objects.requireNonNull(Object obj)` | 检查对象是否为 null,为 null 则抛异常 | ```java Objects.equals(name, person.name); // 等价于 name != null && name.equals(person.name) Objects.hash(name, age); // 根据 name 和 age 生成哈希值 ``` > 📄 来源:`course/api/Person.java` ### 4.4 toString() 方法 `toString()` 将对象转换为字符串表示。Object 默认实现为 `包名.类名 @ 哈希码值的十六进制`。 ```java // 默认输出如:course.api.Person@3feba861 // 重写后: @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } ``` > 📄 来源:`course/api/Person.java` ### 4.5 IDEA 快速生成 使用 IntelliJ IDEA 开发时,通过 **右键 → Generate → equals() and hashCode() / toString()** 即可快速生成。 > 📄 来源:`course/api/Demo01.java` --- ## 五、课后作业回顾 — StringUtilTest 字符串工具测试 ### 5.1 需求说明 给定有前后空格和重复项的字符串数据 `" 张三, 李四, 王五, 赵六, 张三, 钱七 "`,需要: 1. 去除每个元素的前后空格 2. 去重(去除重复的姓名) 3. 统计每个姓名的出现次数 ### 5.2 相关 API | 方法 | 说明 | |------|------| | `String.split(String regex)` | 按指定正则表达式分割字符串为字符串数组 | | `String.trim()` | 去除字符串前后的空白字符 | | `Arrays.toString(Object[] a)` | 将数组转换为字符串表示形式 | ### 5.3 核心算法 #### 三步去重算法 ```java String data = " 张三, 李四, 王五, 赵六, 张三, 钱七 "; // 第一步:分割 + 去空格 String[] split = data.split(","); for (int i = 0; i < split.length; i++) { split[i] = split[i].trim(); } // 第二步:统计不重复元素个数 int count = 0; for (int i = 0; i < split.length; i++) { boolean b = false; for (int j = 0; j < i; j++) { if (split[i].equals(split[j])) { b = true; break; } } if (b == false) { count++; } } // 第三步:创建新数组,放入不重复元素 String[] newArr = new String[count]; int index = 0; for (int i = 0; i < split.length; i++) { boolean b = false; for (int j = 0; j < i; j++) { if (split[i].equals(split[j])) { b = true; break; } } if (b == false) { newArr[index] = split[i]; index++; } } ``` #### 统计元素出现次数 ```java for (int i = 0; i < newArr.length; i++) { int count02 = 0; for (int j = 0; j < split.length; j++) { if (newArr[i].equals(split[j])) { count02++; } } System.out.println(newArr[i] + ":" + count02 + "次"); } ``` > 📄 来源:`StringUtilTest.java` **关键知识点:** - `split[i].equals(split[j])` — 字符串比较使用 `equals()`,不能使用 `==` - 双重循环 + 标记位(`boolean b`)实现去重 - 先统计个数再创建数组,避免数组长度不确定的问题 --- ## 六、课后作业回顾 — DataMathTest 日期时间计算 ### 6.1 需求说明 计算当前日期距离 2026-12-31 还有多少天。 ### 6.2 相关 API | 方法 | 说明 | |------|------| | `LocalDate.of(int year, int month, int dayOfMonth)` | 创建指定日期的 `LocalDate` 对象 | | `LocalDate.toEpochDay()` | 返回从 1970-01-01 到当前日期的天数 | ### 6.3 代码实现 ```java LocalDate localDate = LocalDate.of(2026, 7, 27); LocalDate localDate2 = LocalDate.of(2026, 12, 31); long now = localDate.toEpochDay(); // 距离 1970-01-01 有多少天 long epochDay = localDate2.toEpochDay(); // 距离 1970-01-01 有多少天 System.out.println("距离 2026-12-31 还有:" + (epochDay - now) + "天"); ``` > 📄 来源:`DataMathTest.java` **输出结果:** `距离 2026-12-31 还有:157 天` **关键知识点:** - `toEpochDay()` 返回的是 `long` 类型,表示从 1970-01-01(Epoch)到当前日期的天数 - 两个日期的天数相减即可得到间隔天数 - 也可以使用 `LocalDate.until(ChronoUnit.DAYS)` 或 `Period.between()` 实现 --- ## 七、随堂练习要点 ### 7.1 异常处理练习要点 - 理解 `Throwable` → `Exception` → `RuntimeException` 的继承体系 - 区分编译期异常和运行时异常 - 掌握 `try-catch` 多 catch 块的写法,注意异常类型顺序(子类→父类) - 掌握 `finally` 块的使用:`try-catch-finally`(完整)、`try-finally`(扫描但不处理) - 理解 `finally` 块一定会被执行的特性,常用于关闭资源(如 `Scanner.close()`) - 理解 `JVM` 默认处理异常的方式(打印信息 + 终止程序) - 熟悉异常常用方法:`getMessage()`、`fillInStackTrace()`、`getCause()`、`printStackTrace()` - 掌握 `throw` 手动抛出异常对象(在方法体内部使用) - 掌握 `throws` 声明异常(在方法签名上使用),理解两者的区别 - 掌握自定义异常的创建步骤(继承 Exception → 无参/有参构造方法) - 体会异常处理对程序 **鲁棒性(健壮性)** 的提升 ### 7.2 字符串处理综合练习要点 - 使用 `split(",")` 分割带分隔符的字符串 - 使用 `trim()` 去除字符串前后空格 - 掌握数组去重算法(双重循环 + 标记位) - 掌握元素出现次数的统计算法 - 理解 `equals()` 在字符串比较中的必要性 ### 7.3 日期时间计算练习要点 - 使用 `LocalDate.of()` 创建指定日期 - 使用 `toEpochDay()` 计算两个日期之间的天数差 - 了解其他可选方案:`until()`、`Period.between()`、`ChronoUnit.DAYS.between()` ### 7.4 Object 类练习要点 - 理解 `Object` 是所有类的根父类 - 掌握 `equals()` 的重写规范:判断 null → 判断类型 → 比较关键字段 - 掌握 `hashCode()` 与 `equals()` 的约定:equals 相等则 hashCode 必须相等 - 掌握 `toString()` 重写,便于输出对象属性 - 熟悉 `Objects` 工具类的常用方法:`Objects.equals()`、`Objects.hash()`、`Objects.requireNonNull()` - 学会使用 IDEA 快捷键生成 `equals()`、`hashCode()`、`toString()` --- ## 八、拓展阅读 1. **Java 异常处理最佳实践** — 何时使用 checked vs unchecked exception,何时自定义异常 2. **try-with-resources** — JDK7+ 自动关闭资源的异常处理方式(`try(Resource r = new Resource()){...}`),是 `finally` 关闭资源方式的进化版,无需手动写 `finally` 块 3. **`Objects` 工具类源码分析** — 阅读 `java.util.Objects` 的 `equals()`、`hash()`、`requireNonNull()` 源码,理解其安全机制 4. **`equals()` 与 `hashCode()` 约定详解** — 理解为什么重写 equals 必须重写 hashCode(HashMap/HashSet 等集合类的依赖) 5. **`toString()` 与日志框架** — 在 SLF4J/Logback 等日志框架中,`toString()` 的正确重写对日志输出非常重要 3. **异常链(Exception Chaining)** — 通过 `initCause()` 或构造方法将底层异常包装为高层异常 4. **`Objects.requireNonNull()`** — 一种优雅避免 NullPointerException 的方式(JDK7+) 5. **`Period` 与 `Duration`** — Java 8 日期时间 API 中更灵活的时间间隔计算 6. **`LinkedHashSet` 去重** — 利用集合类简化去重操作(替代手动数组去重)