20260728-笔记.md 19 KB

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 程序执行流程:

.java(源代码)  ---[javac 编译]--->  .class(字节码)  ---[java 解释]--->  运行结果
                   编译期                                  运行期
  • 编译期异常:在 Java 源代码编译成字节码的过程中出现,必须处理。
  • 运行期异常:编译阶段没有任何错误,但在从字节码文件解释运行获取结果期间出现错误。

📄 来源:Demo01.java

1.3 JVM 对异常的默认处理

如果程序出了问题,我们不做任何处理,最终会由 JVM 做默认处理,处理步骤:

  1. 异常的名称、错误原因、以及异常出现的位置 等信息输出在控制台
  2. 程序停止运行

📄 来源:Demo01.java

1.4 异常处理方式一:try-catch 捕获处理

语法格式

try {
    // 可能出现异常的代码
} catch (异常类型1 变量名) {
    // 异常类型1的处理方式
} catch (异常类型2 变量名) {
    // 异常类型2的处理方式
} catch (异常类型n 变量名) {
    // 异常类型n的处理方式
}

多个 catch 块的执行顺序

  • 多个 catch 块遵循 从上到下匹配 的原则
  • 一旦匹配到某个异常类型,就执行对应的 catch 块,然后跳出 try-catch 结构
  • 建议将 子类异常放在前面,父类异常放在后面,否则父类异常会"拦住"子类异常,导致子类 catch 块不可达

代码示例(更新版——含异常常用方法)

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() 将异常的堆栈追踪信息输出到标准错误流
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 进行默认的处理

    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 { ... } 不捕获异常,但确保资源关闭(异常会继续向上抛)

代码示例

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 自定义异常代码示例

// 自定义异常类 — 继承 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 关键字用于在 方法体内部 手动抛出一个异常对象。

语法格式

throw new 异常类名();
throw new 异常类名("异常描述信息");

使用场景

当方法内部检测到某种业务条件不满足时,手动创建异常对象并抛出。

代码示例(Person 类的 setAge 方法)

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 关键字用在 方法声明后面,后面跟着异常类名,表示该方法可能会抛出某种异常,调用者需要对此做准备。

语法格式

public void 方法名(参数列表) throws 异常类名1, 异常类名2 {
    // 方法体
}

代码示例

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 的完整调用链路

【自定义异常类】                   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 字符串工具测试

4.1 需求说明

给定有前后空格和重复项的字符串数据 " 张三, 李四, 王五, 赵六, 张三, 钱七 ",需要:

  1. 去除每个元素的前后空格
  2. 去重(去除重复的姓名)
  3. 统计每个姓名的出现次数

4.2 相关 API

方法 说明
String.split(String regex) 按指定正则表达式分割字符串为字符串数组
String.trim() 去除字符串前后的空白字符
Arrays.toString(Object[] a) 将数组转换为字符串表示形式

4.3 核心算法

三步去重算法

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++;
    }
}

统计元素出现次数

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 日期时间计算

5.1 需求说明

计算当前日期距离 2026-12-31 还有多少天。

5.2 相关 API

方法 说明
LocalDate.of(int year, int month, int dayOfMonth) 创建指定日期的 LocalDate 对象
LocalDate.toEpochDay() 返回从 1970-01-01 到当前日期的天数

5.3 代码实现

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() 实现

六、随堂练习要点

6.1 异常处理练习要点

  • 理解 ThrowableExceptionRuntimeException 的继承体系
  • 区分编译期异常和运行时异常
  • 掌握 try-catch 多 catch 块的写法,注意异常类型顺序(子类→父类)
  • 掌握 finally 块的使用:try-catch-finally(完整)、try-finally(扫描但不处理)
  • 理解 finally 块一定会被执行的特性,常用于关闭资源(如 Scanner.close()
  • 理解 JVM 默认处理异常的方式(打印信息 + 终止程序)
  • 熟悉异常常用方法:getMessage()fillInStackTrace()getCause()printStackTrace()
  • 掌握 throw 手动抛出异常对象(在方法体内部使用)
  • 掌握 throws 声明异常(在方法签名上使用),理解两者的区别
  • 掌握自定义异常的创建步骤(继承 Exception → 无参/有参构造方法)
  • 体会异常处理对程序 鲁棒性(健壮性) 的提升

6.2 字符串处理综合练习要点

  • 使用 split(",") 分割带分隔符的字符串
  • 使用 trim() 去除字符串前后空格
  • 掌握数组去重算法(双重循环 + 标记位)
  • 掌握元素出现次数的统计算法
  • 理解 equals() 在字符串比较中的必要性

6.3 日期时间计算练习要点

  • 使用 LocalDate.of() 创建指定日期
  • 使用 toEpochDay() 计算两个日期之间的天数差
  • 了解其他可选方案:until()Period.between()ChronoUnit.DAYS.between()

七、拓展阅读

  1. Java 异常处理最佳实践 — 何时使用 checked vs unchecked exception,何时自定义异常
  2. try-with-resources — JDK7+ 自动关闭资源的异常处理方式(try(Resource r = new Resource()){...}),是 finally 关闭资源方式的进化版,无需手动写 finally
  3. 异常链(Exception Chaining) — 通过 initCause() 或构造方法将底层异常包装为高层异常
  4. Objects.requireNonNull() — 一种优雅避免 NullPointerException 的方式(JDK7+)
  5. PeriodDuration — Java 8 日期时间 API 中更灵活的时间间隔计算
  6. LinkedHashSet 去重 — 利用集合类简化去重操作(替代手动数组去重)