在程序执行过程中,可能有各种出错的情况,有的是不可控的内部原因,比如内存不够了、磁盘满了,有的是不可控的外部原因,比如网络连接有问题,更多的可能是程序的编写错误,比如引用变量未初始化就直接调用实例方法。这些非正常情况在Java中都被认为是异常。
异常简介
异常是相对于 return 的一种退出机制,可以由系统触发,也可以由程序通过 throw 语句触发,异常可以通过 try/catch 语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息。
当发生异常时,Java 会启用异常处理机制,首先创建一个异常对象,然后异常处理机制会从当前方法开始查找看谁 “捕获” 了这个异常,当前方法没有就查看上一层,直到 main 方法,如果也没有,就使用默认机制,输出异常栈信息并退出。
异常栈信息包括从异常发生点到最上层调用者的轨迹,还包括行号,是分析异常最为重要的信息。
对于异常栈信息,普通用户无法理解,也不知道该怎么办,我们需要给用户一个更为友好的信息,又或者我们不希望程序在出现异常后就退出程序,而是抛出异常然后继续执行,为此需要使用 try/catch 捕获异常。
try 后面的花括号 {}
内包含可能抛出异常的代码,catch 语句包含能捕获的异常和处理代码,捕获异常后,程序就不会异常退出了,但 try 语句内异常点之后的代码不会执行,执行完 catch 内的语句后,程序会继续执行 try/catch 之后的代码。
public class TestUtil {
public static void main(String[] args) {
excepTest1();
excepTest2();
System.out.println("main 方法没有捕获异常,不会继续执行");
}
public static void excepTest1(){
/**
* 发生异常时,会启用异常处理机制,首先创建一个异常对象
* 异常处理机制会从当前方法开始查找看谁 “捕获” 了这个异常,
* 当前方法没有就查看上一层,直到 main 方法,
* 如果也没有,就使用默认机制,输出异常栈信息并退出
*/
String str = "abc";
try {
int num = Integer.parseInt(str);
System.out.println("如果异常,try 语句内,异常点后的代码不会执行");
System.out.println(num);
} catch (NumberFormatException e1){
System.err.println("参数:"+ str +",不是有效数字");
e1.getMessage();
e1.getCause();
e1.printStackTrace();
} catch (Exception e2){
/**
* 异常处理机制将根据抛出的异常类型找第一个匹配的 catch 块,
* (Java 7 开始,多个异常之间可以用“|”操作符,但注意异常之间不能有父子关系)
* 找到后,执行catch块内的代码,不再执行其他 catch 块,
* 如果没有找到,会继续到上层方法中查找。
* 需要注意的是,如果抛出的异常类型是 catch 中声明异常的子类也算匹配(多态)
*/
System.err.println("异常类型不是 NumberFormatException 才会执行");
throw e2;
} finally {
System.out.println("不管有没有异常,一定执行");
}
System.out.println("当前有捕获异常,方法会继续执行");
}
public static void excepTest2(){
String str = "abc";
int num = Integer.parseInt(str);
System.out.println("当前没有捕获异常,方法不会继续执行,向上级方法查找异常捕获");
System.out.println(num);
}
/**
* throws 跟在方法的括号后面,可以声明多个异常,以逗号分隔
* 主要用于在父类方法中声明可能(包括子类)抛出的异常
* @throws Exception
*/
public static void excepTest3() throws Exception {
try {
} catch (Exception e){
e = new Exception(); // 注释该行,可以去掉方法后的 throws
throw e;
}
}
}
异常体系
[图片上传失败…(image-3fc672-1677849073641)]
Throwable
java.lang.Throwable 是所有异常的基类,它有两个子类:Error 和 Exception。
Error 用来指示运行时环境发生的错误,一般发生在严重故障时,Java 程序通常不捕获错误,比如 内存溢出错误(OutOfMemory-Error)和栈溢出错误(StackOverflowError)。
Exception 表示应用程序错误,它有很多子类,应用程序也可以通过继承 Exception 或其子类创建自定义异常。
RuntimeException 是未受检异常(unchecked exception),IOException、SQLException 和 Exception 自身则是受检异常(checked exception)。对于受检异常,Java 会强制要求程序员进行处理,否则会有编译错误,而对于未受检异常则没有这个要求。
大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。定义这么多不同的异常类主要是为了名字不同,异常类的名字本身就代表了异常的关键信息。
- 构造方法
public Throwable()
public Throwable(String message)
public Throwable(String message, Throwable cause)
public Throwable(Throwable cause)
message,表示异常消息;cause,表示触发该异常的其他异常。
异常可以形成一个异常链,上层的异常由底层异常触发,cause表示底层异常。
- 设置 cause
这个方法最多只能被调用一次
Throwable initCause(Throwable cause)
- 保存异常栈信息
用当前的调用栈层次填充 Throwable 对象栈层次,添加到栈层次信息中。
public Throwable fillInStackTrace()
- 获取异常信息
public String getMessage()
public Throwable getCause()
- 打印异常栈信息
public void printStackTrace()
自定义异常
一般是继承 Exception 或者它的某个子类。如果父类是 RuntimeException 或它的某个子类,则自定义异常也是未受检异常;如果是 Exception 或 RuntimeException 以外的其他子类,则自定义异常是受检异常。
// 自定义异常类,继承Exception类
public class InsufficientFundsException extends Exception {
// 此处的amount用来储存当出现异常(取出钱多于余额时)所缺乏的钱
private double amount;
public InsufficientFundsException(double amount) {
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
异常处理
try catch
使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方,try/catch 中的代码称为保护代码。
// 自定义异常类,继承Exception类
public class InsufficientFundsException extends Exception {
// 此处的amount用来储存当出现异常(取出钱多于余额时)所缺乏的钱
private double amount;
public InsufficientFundsException(double amount) {
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。
try {
file = new FileInputStream(fileName);
x = (byte) file.read();
} catch(FileNotFoundException f) { // Not valid!
f.printStackTrace();
return -1;
} catch(IOException i) {
i.printStackTrace();
return -1;
}
如果保护代码中发生异常,异常被抛给第一个 catch 块。如果不匹配,它会被传递给第二个 catch 块,直到异常被捕获或者通过所有的 catch 块。
需要注意的是,抛出的异常类型是 catch 中声明异常的子类也算匹配,所以需要将最具体的子类放在前面。
这种写法比较烦琐,Java 7开始支持一种新的语法,多个异常之间可以用 “|” 操作符。
try {
//可能抛出 ExceptionA和ExceptionB
} catch (ExceptionA | ExceptionB e) {
e.printStackTrace();
}
throw
在 catch 块内处理完后,可以重新抛出异常,异常可以是原来的,也可以是新建的。
如果一个方法抛出检查性异常,那么该方法必须使用 throws 关键字来声明抛出的异常,否则不能抛出。
throws 跟在方法的括号后面,可以声明多个异常,以逗号分隔。
声明的含义是,这个方法内可能抛出这些异常,且没有没有处理完这些异常,调用者必须进行处理。
public void test() throws Exception {
try{
// 可能触发异常的代码
}catch(NumberFormatException e){
System.out.println("not valid number");
throw new AppException("输入格式不正确", e);
}catch(Exception e){
e.printStackTrace();
throw e;
}
}
finally
finally 内的代码不管有无异常发生,都会执行。一般用于释放资源,如数据库连接、文件流等。
- 如果没有异常发生,在 try 内的代码执行结束后执行
- 如果有异常发生且被 catch 捕获,在 catch 内的代码执行结束后执行
- 如果有异常发生但没被 catch 捕获,则在异常 throw 给上层之前执行
- 如果在 try 或者 catch 语句内有 return 语句,则 return 语句在 finally 语句执行结束后才执行,对于基本数据类型,finally 中并不能改变返回值(可以理解为返回值存储在返回值存储器中,finally 的操作不会改变返回值存储器中的值),对于对象类型,可以改变其中的值(返回值存储器存储的是对象地址)。
- 如果 finally 中有 return,不仅会覆盖 try 和 catch 内的返回值,还会掩盖 try 和 catch 内的异常,就像异常没有发生一样,应该避免在 finally 中使用 return 语句或者抛出异常
try {
// 可能抛出异常
} catch (Exception e){
// 捕获异常
} finally {
// 不管有无异常都执行
}
try-with-resources
对于一些使用资源的场景,比如文件和数据库连接,典型的使用流程是首先打开资源,最后在 finally 语句中关闭资源。
Java 7 开始支持一种新的语法 try-with-resources,针对实现了 java.lang.AutoCloseable 接口的对象。
资源的声明和初始化放在 try 语句内,不用再调用 finally,在语句执行完 try 语句后,会自动调用资源的 close() 方法。
try-with-resources 语句中可以声明多个资源,使用分号 ;
分隔各个资源。
Java 9 之前,资源必须声明和初始化在try语句块内,Java 9 去除了这个限制,资源可以在 try 语句外被声明和初始化,但必须是 final 的或者是事实上 final 的(即虽然没有声明为 final 但也没有被重新赋值)。
try(AutoCloseable autoClose = new FileInputStream("hello")){
//
}
异常处理的目标
异常大概可以分为三种来源:用户、程序员、第三方。
用户是指用户的输入有问题;程序员是指编程错误;第三方泛指其他情况,如 I/O 错误、网络、数据库、第三方服务等。
处理的目标可以分为恢复和报告。恢复是指通过程序自动解决问题。报告的最终对象可能是用户,即程序使用者,也可能是系统运维人员或程序员。报告的目的也是为了恢复,但这个恢复经常需要人的参与。
对用户,如果用户输入不对,可以提示用户具体哪里输入不对,如果是编程错误,可以提示用户系统错误、建议联系客服,如果是第三方连接问题,可以提示用户稍后重试。
异常处理的一般逻辑
如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。
如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为 cause 重新抛出一个异常。
总有一层代码需要为异常负责,可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是主程序。如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。