JavaSE丨异常处理详解,高效应对程序中的“意外”
一、异常
1.1 概述
程序在运行过程中,由于意外情况导致程序发生异常事件,默认情况下发生的异常会中断程序的运行。
在Java中,把常见的异常情况,都抽象成了对应的异常类型,那么每种异常类型都代表了一种特定的异常情况。
当程序中出现一种异常情况时,也会创建并抛出一个异常类型对象,这个对象就表示当前程序所出现的问题。
如图:
例如,程序中有一种异常情况是,当前使用下标从数组中取值的时候,这个下标值超过了数组下标的最大值,那么程序中就出现了异常情况,java中把这种异常情况抽象成了一个类: java.lang.ArrayIndexOutOfBoundsException ,表示程序中出现了数组下标超过边界的异常情况。
案例展示:
观察下面各种异常情况:
//如何理解异常:
// 程序不正常情况,统称为 异常
public class Test_Basic {public static void main(String[] args) {// ArithmeticExceptionint a = 10 / 0;String s = "abc";//: NumberFormatExceptionint n = Integer.parseInt(s);Object obj = new Object();//new String("hello");//类型转换异常:ClassCastExceptions = (String)obj;int[] arr = {1,2,3,4};arr = null;//空指针异常:NullPointerExceptionSystem.out.println(arr[0]);//数组索引越界 ArrayIndexOutOfBoundsExceptionSystem.out.println(arr[4]);}
}
运行结果:
Exception in thread "main" java.lang.ArithmeticException: / by zeroat 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 zeroat 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(); // 关闭流可能也会抛出IOExceptionSystem.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);}
}