一、异常
1.1 概述
程序在运行过程中,由于意外情况导致程序发生异常事件,默认情况下发生的异常会中断程序的运行。
在Java中,把常见的异常情况,都抽象成了对应的异常类型,那么每种异常类型都代表了一种特定的异常情况。
当程序中出现一种异常情况时,也会创建并抛出一个异常类型对象,这个对象就表示当前程序所出现的问题。
如图:
例如,程序中有一种异常情况是,当前使用下标从数组中取值的时候,这个下标值超过了数组下标的最大值,那么程序中就出现了异常情况,java中把这种异常情况抽象成了一个类: java.lang.ArrayIndexOutOfBoundsException ,表示程序中出现了数组下标超过边界的异常情况。
案例展示:
观察下面各种异常情况:
//如何理解异常:
// 程序不正常情况,统称为 异常
public class Test_Basic {
public static void main(String[] args) {
// ArithmeticException
int a = 10 / 0;
String s = "abc";
//: NumberFormatException
int n = Integer.parseInt(s);
Object obj = new Object();//new String("hello");
//类型转换异常:ClassCastException
s = (String)obj;
int[] arr = {1,2,3,4};
arr = null;
//空指针异常:NullPointerException
System.out.println(arr[0]);
//数组索引越界 ArrayIndexOutOfBoundsException
System.out.println(arr[4]);
}
}
运行结果:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Test_Basic.main(Test_Basic.java:6)
可以看出,当前程序出现异常情况时,会创建并抛出和该异常情况对应的异常类的对象,这个异常对象中保存了一些信息,用来表示当前程序到底发生了什么异常情况。
通过异常信息,我们可以定位异常发生的位置,以及异常发生的原因
1.2 异常体系
异常体系中的根类是: java.lang.Throwable ,该类下面有两个子类型, java.lang.Error 和 java.lang.Exception
注意:Throwable 表示可以被抛出的
- Error ,表示错误情况,一般是程序中出现了比较严重的问题,并且程序自身并无法进行处理。
- Exception ,表示异常情况,程序中出了这种异常,大多是可以通过特定的方式进行处理和纠正的,并且处理完了之后,程序还可以继续往下正常运行
1.3 异常种类
我们平时使用的异常类型,都是两种:
- 编译时异常
- 运行时异常
编译时异常:
- 继承自 Exception 类的子类型,也称为checked exception
- 编译器在编译期间,会主动检查这种异常,如果发现异常则必须显示处理, 否则程序就会发生错误,无法通过编译
运行时异常 :
- RuntimeException 类及其子类,也称为unchecked exception
- 编译器在编译期间,不会检查这种异常,也不要求我们去处理,但是在运行期间,如果出现这种异常则自动抛出
1.4 异常传播
如果一个方法中出现了异常的情况,系统默认的处理方式是:自动创建异常对象,并将这个异常对象抛给当前方法的调用者,并一直向上抛出,最终传递给 JVM,JVM默认处理步骤有2步:
- 把异常的名称,错误原因及异常出现的位置等信息输出在了控制台
- 程序停止执行
案例展示:
public class Test_Default {
public static void main(String[] args) {
System.out.println("hello");
test1();
System.out.println("world");
}
public static void test1() {
test2();
}
public static void test2() {
test3();
}
public static void test3() {
//下面代码会抛出异常
int a = 1 / 0;
}
}
运行结果:
hello
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Test_Default.test3(Test_Default.java:18)
at Test_Default.test2(Test_Default.java:13)
at Test_Default.test1(Test_Default.java:9)
at Test_Default.main(Test_Default.java:4)
代码执行步骤解析:
- 因为 java.lang.ArithmeticException 是运行时异常,所以代码可以编译通过
- 程序运行时,先输出"hello",然后一层一层调用,最终执行test3方法
- 执行test3方法时,出现除数为0的情况,系统自动抛出异常
- java.lang.ArithmeticException 代码中没有对异常进行任何捕获处理,所以该异常往上传递给test2 --> test1 --> main --> JVM
- JVM虚拟机拿到异常后,输出异常相关信息,然后终止程序
二、异常抛出
2.1 自动抛出
Java代码中,出现了提前指定好的异常情况的时候,代码会自动创建异常对象, 并且将该异常对象抛出。
例如,上述案例中执行 int a = 1/0; 的时候,代码会自动创建并抛出 ArithmeticException 类型的异常对象,来表示当前的这种异常情况。(算术异常)
又如,代码中执行自动创建并抛出 String str = null; str.toString(); 的时候,代码会 NullPointerException 类型的异常对象,来表示当前这种异常情况。(空指针异常)
2.2 手动抛出
以上描述的异常情况,都是JVM中提前规定好的,我们不需要干预,JVM内部自己就会创建并抛出异常对象。
但是在其他的一些情况下,我们也可以手动的创建并抛出异常对象,抛出后系统也会按照默认的方式去处理。
手动抛出异常固定格式: throw 异常对象;
案例展示:
public class AgeValidator {
// 验证年龄的方法
public static void checkAge(int age) {
// 规定年龄必须在0-150之间,否则视为无效
if (age < 0 || age > 150) {
// 手动创建并抛出异常对象
throw new IllegalArgumentException("年龄必须在0到150之间,当前值:" + age);
}
System.out.println("年龄验证通过:" + age);
}
public static void main(String[] args) {
// 测试正常情况
checkAge(25); // 年龄有效,会输出验证通过信息
// 测试异常情况
checkAge(-5); // 年龄无效,会触发手动抛出的异常
}
}
三、异常处理
代码中出现了异常,除了默认的处理方式外,我们还可以手动处理异常:
- 声明继续抛出异常,借助throws关键字实现
- 捕获并处理异常,借助try、catch、finally关键字实现
3.1 throws
throws关键字用于在方法声明中指定该方法可能抛出的异常类型。
这个声明的目的,就是告诉方法的调用者,调用这个方法的时候要小心 ,方法在运行的时候可能会抛出指定类型的异常。
案例展示:
多个异常声明
import java.io.FileInputStream;
import java.io.IOException;
public class ThrowsExample {
// 声明该方法可能会抛出IOException和NullPointerException异常
public static void processFile(String filePath) throws IOException, NullPointerException {
if (filePath == null) {
throw new NullPointerException("文件路径不能为null");
}
FileInputStream fis = new FileInputStream(filePath);
// 其他文件处理逻辑
fis.close();
}
public static void main(String[] args) {
String filePath = null;
try {
processFile(filePath);
} catch (IOException | NullPointerException e) {
System.out.println("发生异常: " + e.getMessage());
}
}
}
我们将throw与throws进行一个对比:
对比维度 | throw 关键字 | throws 关键字 |
---|---|---|
作用 | 手动抛出一个具体的异常对象(触发异常) | 声明方法可能会抛出的异常类型(告知风险) |
使用位置 | 方法体内部(用于执行具体的异常抛出动作) | 方法签名末尾(用于声明异常,格式:方法名() throws 异常类型 ) |
抛出内容 | 必须是异常对象(如 throw new Exception() ) | 必须是异常类型(如 throws IOException ) |
数量限制 | 一次只能抛出一个异常对象 | 可以声明多个异常类型,用逗号分隔(如 throws AException, BException ) |
处理要求 | 抛出异常后,要么用 try-catch 处理,要么用 throws 声明抛给上层 | 声明异常后,调用者必须处理(try-catch )或继续用 throws 向上声明 |
适用场景 | 主动触发异常(如业务规则校验失败时) | 告知方法调用者 “此方法可能会引发这些异常,需注意处理” |
总结
throw
是 “主动扔出一个具体的异常”(执行动作);throws
是 “提前声明方法可能会扔出哪些异常”(告知风险)。
3.2 try-catch
try-catch 语句块,就是用来对指定代码,进行异常捕获处理,并且处理完成后,JVM不会停止运行,代码还可以正常的往下运行。
捕获异常语法:
try {
可能会出现异常的代码;
}catch(异常类型 引用名) {
//处理异常的代码,可以是简单的输出异常信息
//也可以使用日志进行了记录,也可以对数据进行修改纠正等操作
//一般输出异常信息
//e.printStackTrace();
}
try:该代码块中包含可能产生异常的代码
catch:用来进行某种类型异常的捕获,并对捕获到的异常进行处理
执行流程:
- 程序从 try 里面的代码开始执行
- 出现异常,就会跳转到对应的 catch块 里面去执行
- 执行完毕之后,程序出 catch块,继续往下执行
案例展示:
模拟用户输入数字并进行除法运算的场景,处理可能出现的输入格式错误和除零异常
import java.util.Scanner;
public class DivisionCalculator {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("===== 除法计算器 =====");
try {
// 尝试获取用户输入的被除数
System.out.print("请输入被除数(整数):");
String num1Str = scanner.nextLine();
int num1 = Integer.parseInt(num1Str); // 可能抛出NumberFormatException
// 尝试获取用户输入的除数
System.out.print("请输入除数(整数):");
String num2Str = scanner.nextLine();
int num2 = Integer.parseInt(num2Str); // 可能抛出NumberFormatException
// 尝试执行除法运算
int result = num1 / num2; // 可能抛出ArithmeticException(除数为0时)
// 如果以上步骤都没有异常,输出计算结果
System.out.println("计算结果:" + num1 + " ÷ " + num2 + " = " + result);
}
catch (NumberFormatException e) {
// 处理输入格式错误(用户输入的不是整数)
System.out.println("错误:请输入有效的整数!" + e.getMessage());
}
catch (ArithmeticException e) {
// 处理除法异常(除数为0)
System.out.println("错误:" + e.getMessage() + ",除数不能为0!");
}
// 无论是否发生异常,都会执行以下代码
System.out.println("\n程序执行结束,感谢使用!");
scanner.close();
}
}
上述案例中体现出,如果try语句块中的代码可能抛出多种异常,并且是不同类型的,则可以写多个 catch语句块,用来同时捕获多种类型异常。
注意事项:
这种异常处理方式,要求多个catch中的异常不能相同
如果catch中的多个异常类之间有子父类关系的话,那么子类异常必须写在父类异常上面的catch块中,父类异常必须写在下面的catch块中。 因为如果父类型异常再最上面的话,下面catch语句代码,永远不会被执行!
3.3 finally 语句
finally 关键字可以和 try、catch关键字一起使用,固定搭配为: try catch-finally ,它可以保证指定finally中的代码一定会执行,无论是否发生异常!
finally 块的主要作用:
- 资源释放:在 try 块中打开的资源(例如文件、数据库连接、网络连接等) 可以在 finally 块中关闭或释放,以确保资源的正确释放,即使在发生异常的情况下也能够执行释放操作。
- 清理操作: finally 块可以用于执行一些清理操作,例如关闭打开的流、释放锁、取消注册监听器等。
- 异常处理的补充:finally块可以用于在try块和catch块之后执行一些必要的操作,例如记录日志、发送通知等。
案例展示:
import java.io.FileInputStream;
import java.io.IOException;
public class FileReaderExample {
public static void main(String[] args) {
FileInputStream fis = null; // 声明文件输入流变量
try {
// 尝试打开文件并读取内容
fis = new FileInputStream("data.txt");
System.out.println("文件打开成功,准备读取...");
// 模拟读取过程中可能发生的异常(例如文件内容异常)
int data = fis.read();
if (data == -1) {
throw new IOException("文件内容为空");
}
System.out.println("文件读取完成,内容:" + (char) data);
} catch (IOException e) {
// 处理文件操作相关异常
System.out.println("文件操作出错:" + e.getMessage());
} finally {
// 无论是否发生异常,都必须关闭文件流(释放资源)
System.out.println("进入finally块,准备关闭文件流...");
if (fis != null) { // 确保流对象已初始化
try {
fis.close(); // 关闭流可能也会抛出IOException
System.out.println("文件流已成功关闭");
} catch (IOException e) {
System.out.println("关闭文件流时出错:" + e.getMessage());
}
}
}
System.out.println("程序执行结束");
}
}
四、自定义异常
如果要自定义一个编译时异常类型,就自定义一个类,并继承 Exceptionn
如果要自定义一个运行时异常类型,就自定义一个类,并继承 RuntimeException
自定义异常步骤:
无论哪种类型的自定义异常,定义步骤基本一致,核心是 “继承父类 + 提供构造方法”:
-
定义异常类
类名通常以Exception
结尾(如InvalidAgeException
、PasswordErrorException
),直观体现异常含义。 -
指定继承关系
- 编译时异常:
public class 自定义类名 extends Exception { ... }
- 运行时异常:
public class 自定义类名 extends RuntimeException { ... }
- 编译时异常:
-
提供构造方法
必须至少包含两种构造方法(通过super()
调用父类构造):- 空参构造:用于创建无详细信息的异常对象。
- 带参构造:接收一个
String
类型的异常信息(描述错误原因),传给父类保存(便于通过getMessage()
获取)。
案例展示:
// 自定义编译时异常
public class InvalidAgeException extends Exception {
// 空参构造
public InvalidAgeException() {
super(); // 调用父类Exception的空参构造
}
// 带异常信息的构造
public InvalidAgeException(String message) {
super(message); // 调用父类Exception的带参构造,保存异常信息
}
}
// 自定义运行时异常
public class EmptyNameException extends RuntimeException {
public EmptyNameException() {
super();
}
public EmptyNameException(String message) {
super(message);
}
}