目录
异常简介
java的异常机制用于处理程序可能出现的异常代码,让异常处理和正常业务逻辑分离。异常机制通过5个关键字try、catch、 finally、throw和throws完成。
异常的体系结构
所有的异常都继承自Throwable父类。
Throwable有两个子类:Exception和Error。
Error表示系统级别的错误,是严重的、不可恢复的错误,Error错误必然会导致程序中断。所以,没有必要捕获Error对象。
Exception是指在程序正常运行过程中,可以预料的意外情况,可以事先捕获并且处理。
Exception类有2大类:Runtime异常和Checked异常。
所有RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例被称为Checked异常。
异常的两种类型
java的异常种类很多,但按照处理方式不同可以分为两种:Runtime异常和Checked异常(也称为编译时异常)。非Runtime异常就是Checked异常。
Checked异常必须明确处理,也就是必须选择try-catch块或者throws语句两种方式处理异常。这样做增加了程序的健壮性,但是降低了代码的开发和执行效率。
而Runtime异常如果不用try-catch块直接处理,就相当于隐式的带有throws声明,程序会自动把异常抛给上一级调用方。
所以,相比于Checked异常,Runtime异常可以不显式处理,如果没显示处理,系统就通过throws语句抛出异常。
Checked异常的意义
Runtime异常的出现很多时候是无法预料的,因为使用者很多时候不会按照程序员预期的方式操作程序。程序员很难提前通过try-catch的方式提供足够完备的异常处理。最好的办法就是当异常出现后直接抛给上一级。所以,java默认Runtime异常带有隐式的throws声明。
而Checked异常java认为是应该被处理(或修复)的,所以java要求程序员必须显式的处理Checked异常。比如直接修改源代码防止Checked异常的产生,或者即使没办法完全避免Checked异常的产生,也要做到完全清楚程序可能产生哪些Checked异常,并且将这些Checked异常通过try-catch或者throws语句清楚的处理。
这样做会让代码更繁琐,但是常常可以帮助程序员在编译阶段就避免Checked异常的产生。
2种产生异常的方式
java的异常本质是一个个的对象,不同类型的异常本质是不同的异常类,代码出现问题时,会生成对应类型的异常对象。
java的异常对象可以通过2种方式产生:
-
JVM自行生成异常对象
如果出现异常的代码,JVM会根据代码的错误类型自动生成对应的异常对象。比如数组下标越界访问的话,会自动生成ArrayIndexOutOfBoundsException异常对象。 -
程序员主动通过throw语句生成异常对象
有的异常属于不符合业务逻辑,但代码本身不会自动抛异常,比如age的值小于0,这种情况需要程序员主动通过throw语句生成异常对象。最好是根据异常的类型自己定义一个异常类,让这个异常类继承Exception类或者最好是继承RuntimeException类。
异常的两种处理方式
java一共有2种处理异常的方式:try-catch块和throws。
当异常对象在某个方法的执行过程中生成后,方法有2种方式处理该异常对象:
1.如果用try-catch块包住可能出现异常的代码,就是主动处理异常,异常对象会被传入对应的catch块,按照catch块中设定的逻辑处理该异常。
2.如果不想主动处理该异常,可以通过throws语句把异常对象抛给方法的调用者,让方法的调用者处理。如果方法的调用者是JVM,JVM就会直接打印异常信息并终止程序。注意:如果异常类型是RuntimeException类及其子类,如果不try-catch处理,系统会自动调用throws语句向上抛出,程序员可以不主动写throws语句。
也就是说,异常被抛出后,除了用try-catch块主动处理,就只能抛给上一级方法调用者。每个方法都有这2个选择,如果一直不用try-catch块主动处理,异常对象就会被抛给JVM处理。
try catch finally
try-catch块用于捕获和处理异常。
try块部分包含可能抛出异常的代码,如果出现了异常,会生成对应类型的异常对象(可以系统自动生成或者程序员主动通过throw语句生成)。这个过程称为抛出异常。
catch块用于捕获并处理这些异常,系统生成的异常对象实例会去和catch块的形参类型匹配,如果匹配上,该实例会通过JVM赋值给catch块的形参,之后catch块就拥有了这个异常对象,可以使用这个异常对象进行相应的处理。这个过程称为捕获异常。
一个try块可以包含多个catch块,因为try块部分的代码可能在不同的情况下,会产生不同类型的异常对象。在运行程序时,假如产生了第一个异常对象,那这个异常对象会尝试和对应catch块的形参匹配,进入适合该异常的处理逻辑代码部分。
如果JVM没有找到可以匹配上的catch块,程序会直接终止。
注意:只要代码会产生异常(不管有没有try-catch块,甚至在catch块内部还可能产生异常),系统就会自动生成相应的异常对象,与try catch无关。只不过如果没有try-catch块,系统找不到能捕获并处理异常的代码逻辑,就会把异常“向上抛出”。
try-catch块用于处理代码中可能产生的异常,只要异常对象传入对应的catch块,程序就会执行相应的逻辑,程序就不会中断,会继续正常执行。
try-catch块的运行逻辑
程序先运行try块的代码,这部分代码可能会产生异常。
假如程序正常运行,没有产生异常对象。则程序会直接跳过所有的catch块代码,往下继续执行(如果有finally块就进入finally块)。
假如程序产生一个异常,程序会停止执行try块剩余代码,直接跳转到对应的catch块中执行,catch块结束后,整个try-catch块就会结束执行(如果有finally块就进入finally块)。如果没有找到匹配的catch块,程序只能通过throws语句把异常向上抛出(对于运行时异常,throws语句可以省略)。
注意:try块后的{}不可以省略,即使try块里只有一行代码。catch块后的{}也不可以省略。try块内部的变量是代码块局部变量,其作用范围和生存周期都在{}里面。catch块无法访问try块里定义的变量。
catch块的匹配规则
catch块的形参类型会和生成的异常对象类型进行匹配。异常对象如果是catch块形参的相同类型或者子类,都算匹配成功。
注意:父类异常的catch块要排在子类异常catch块的后面,也就是先处理小异常,再处理大异常,否则会出现编译错误。
这很好理解,如果父类异常放在前面,那么子类异常的catch块将永远不会被执行。
finally块
finally块用于回收try块中打开的物理资源。
try块或者catch块的代码都不是一定会执行。所以,假如try块中打开了某些物理资源,必须通过finally块来关闭这些物理资源。
try-catch-finally块中,try块是必须的,但是catch和finally二者至少出现一个,可以同时出现。catch块必须放在try块之后,而finally块必须放在try和catch块之后,也就是整个块的最后。
finally块中的代码一定会被执行。即使在try块或者catch块中执行了return语句,程序也会先跳转到finally块执行完,再执行最后一条return语句。
(特殊情况:System.exit(1); 这行语句可以强制退出虚拟机。只有在try块或者catch块中调用了这个方法,finally块才不会得到执行。)
因此,尽量避免在finally块中使用return或throw等导致方法终止的语句,否则可能会导致try块或者catch块的return语句或者throw语句失效。
try-finally块
在java源码中,可能会使用try-finally块,也就是不包含catch块。
这是因为程序希望无论是否有异常,都要关闭某些物理资源。所以需要使用finally块保证物理资源的关闭。
如果程序出现异常,try块会直接向上抛出异常,方法后续代码不会继续运行。但有finally块的话,可以保证finally中的关闭物理资源的代码一定被执行。
多异常捕获
从java7开始,一个catch块可以捕获多种类型的异常。
语法为:catch (异常类型1 | 异常类型2 | … 异常类型n 形参名) {}
注意:捕获多种类型异常时,异常的形参变量有隐式的final修饰(很好理解,毕竟它的类型有多种情况,重新赋值自然不允许),不允许对异常的形参变量重新赋值。
public class ExceptionTest {
public static void main(String[] args) {
try {
System.out.println(10 / 0);
int[] arr = new int[3];
arr[3] = 3;
} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
System.out.println("除数不能为0或者数组索引越界");
// 不能对多异常类型的变量重新赋值
// e = new RuntimeException();
}
}
}
嵌套异常
异常处理流程代码(也就是try-catch-finally块)可以放在任何能放可执行性代码的地方。
如果放在try块、catch块或者finally块内部,就形成了异常的嵌套。
最好嵌套层数不要超过2层。否则会影响可读性。
自动关闭资源的try语句
之前关闭物理资源必须依靠finally语句,会让程序显得臃肿。
Java7增强了try语句的功能:允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个需要关闭的资源资源。
注意:这些资源实现类必须实现AutoCloseable或Closeable接口,这样就必须实现里面的close()方法。实现了close方法的资源类对象才可以被try语句关闭资源。
Closeable是AutoCloseable的子接口,它们略有不同:
Closeable的close抽象方法抛出了IOException。所以它的实现类重写的close方法只能声明抛出IOException或其子类。
AutoCloseable接口的close方法声明抛出了Exception异常,所以它的实现类实现close方法时可以声明抛出任何类型的异常。
举例:
public class AutoCloseTest {
public static void main(String[] args) throws IOException {
try (
var br = new BufferedReader(
new FileReader("AutoCloseTest.java"));
var ps = new PrintStream(new FileOutputStream("a.txt"))) {
System.out.println(br.readLine());
ps.println("打印一段话");
}
}
}
BufferedReader和PrintStream都实现了Closeable接口,而且它们都在try语句
上面程序中粗体字代码分别声明、初始化了两个IO流,由于它们声明和初始化在try语句的圆括号中,所以try语句会在结束时自动关闭它们。
所以这种增强的try语句可以单独出现,相当于隐式的包含了finally块。
Java 9再次增强了这种try语句,只需要自动关闭的资源有final修饰或者即使没被final修饰但没有被重新赋值过(effectively final),java9允许只要把这些资源变量放到try后的圆括号内,try语句就会自动在结束时关闭这些资源。
举例:
public class AutoCloseTest {
public static void main(String[] args) throws IOException {
// 只需要保证br变量是final类型
final var br = new BufferedReader(
new FileReader("AutoCloseTest.java"));
// 或者即使没声明为final,但是没有重新赋值过
var ps = new PrintStream(new FileOutputStream("a.txt"));
// 要关闭资源的变量之间用分号间隔
try (br; ps) {
System.out.println(br.readLine());
ps.println("打印一段话");
}
}
}
注意:br和ps两个资源的引用变量的声明和初始化在try块外部,只需要在圆括号内包含这些变量名,并且用分号间隔即可。
throws声明
很多时候当前方法并不知道如何明确处理可能抛出的异常代码,就只能把可能出现的异常抛给上一级调用者。
此时需要通过throws声明该方法可能会抛出的异常,如果异常对象产生,就把异常抛给上一级。
throws语法格式
throws语法格式如下:
throws ExceptionClass1, ExceptionClass2…
throws可以声明多个可能抛出的异常类型。throws只能出现在方法签名中,表示如果该方法产生对应的异常对象,就直接抛出给上一级调用者。
如果上一级调用者是JVM,JVM就直接打印异常的跟踪栈信息,并中止程序运行。
throws与方法重写
方法重写有两同两小一大原则,其中两小有一条是:子类重写方法throws声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或者相同,子类方法声明抛出的异常类型不允许比父类方法声明抛出的异常多。
这其实很好理解,也就是重写的子类方法不能超过父类的能力范围。
throw语句
有的异常属于程序的数据、执行和既定的业务需求不符合。这种异常系统没办法为你抛出,只能程序员自行定义。
自行抛出异常需要使用throw语句。注意throw是程序员自己抛出异常,throws是声明方法可能会抛出的异常,两者的应用场合完全不一样。
throw语句的语法格式是:throw 异常对象;
比如throw new XXXException(“提供的异常相关提示信息”);
throw语句抛出的异常更常见是RuntimeException异常,因为这样不用显式的处理该异常,代码看起来更简洁。
如果throw语句抛出的是Exception异常,虽然Exception异常的子类也包括RuntimeException异常,但系统默认抛出的是Checked异常,需要显式处理。所以这种方式不推荐!
自定义异常类
程序员可以自己定义异常类。自定义的异常类应该继承Exception类,如果想定义运行时异常,应该继承RuntimeException类(更推荐定义运行时异常)。
通常需要提供2个构造器:无参构造器和带字符串参数的构造器。
给构造器传入的字符串就是异常对象的描述信息,也就是getMessage方法的返回值。
举例:
class AgeOutOfBoundsException extends RuntimeException {
public AgeOutOfBoundsException() {}
public AgeOutOfBoundsException(String msg) {
super(msg);
}
}
自定义异常最重要的是异常类的名字,异常名字要做到见名知意。一般当程序出现不符合业务逻辑,但程序本身可以通过的时候,就需要使用throw语句抛出自定义异常,异常的名字要和错误的业务逻辑相关,比如:AgeOutOfBoundsException就表示年龄超出了合理的范围。
常见的Runtime异常
常见的运行时异常有:
- NullPointerException 空指针异常
- ArithmeticException 算术异常
- ArrayIndexOutOfBoundsException 数组下标越界异常
- ClassCastException 类强制转换异常
- NumberFormatException 数字格式异常
- IllegalArgumentException 非法参数异常
NullPointerException异常
在需要使用对象的地方,使用null代替,就会抛出该异常。
比如:
- 调用null对象的实例方法。
- 访问或者修改null对象的实例变量。
- 把null作为数组使用。
ArithmeticException异常
当出现异常的运算条件时,抛出此异常,比如除以零。
ArrayIndexOutOfBoundsException异常
访问数组的索引越界,索引为负或者大于等于数组大小。
ClassCastException异常
把对象强制转换为不匹配的引用变量的子类类型时,就会抛出该异常。
比如把Integer对象转换为String类型,虽然String和Integer都是Object的子类,但是它们两个类型并不匹配。
举例:
public class ClassCastException_ {
public static void main(String[] args) {
Object obj = Integer.valueOf("10");
System.out.println((String)obj);
}
}
NumberFormatException异常
如果把字符串转换为一种数值类型,但字符串的值本身无法成功转换时,就会抛出该异常。
常见的Checked异常
常见的编译异常有:
- SQLException 数据库异常
- IOException IO异常
- ClassNotFoundException 类无法找到异常
IOException中常见的有:
- EOFException 到达文件尾异常
- FileNotFoundException 文件无法找到异常
所有异常对象的常用方法
捕获到异常对象后,可以通过异常对象的方法获取对应异常的相关信息。
- public String getMessage(); 返回异常对象相关信息的字符串,这个字符串是通过参数为String类型的构造器传入的。
public class ExceptionTest {
public static void main(String[] args) {
try {
Scanner sc = new Scanner(System.in);
System.out.print("请输入年龄:");
int age = sc.nextInt();
// 如果年龄不合法,就抛出自定义的AgeException异常
if (age < 0 || age >= 120) {
// 这里传入的字符串"年龄不合法"会被记录下来,之后getMessage返回的信息就是这里传入的信息
throw new AgeException("年龄不合法!");
}
} catch (AgeException ae) {
System.out.println(ae.getMessage());
}
}
}
class AgeException extends RuntimeException {
public AgeException() {
}
public AgeException(String message) {
super(message);
}
}
如果程序员想通过throw语句抛出自己创建的异常对象,则可以通过构造器设置异常对象相关的提示信息,并且可以在其他地方通过getMessage方法得到该提示信息。
如果是虚拟机自动创建的异常对象,则提示信息是虚拟机自行设置的,比如如果除数为0产生的ArithmeticException的提示信息就是:/ by zero。
public void printStackTrace(); 直接把异常的原因和出错位置信息(也称为异常的跟踪栈信息)打印到控制台。
注意:打印的信息显示为红色字体,而且打印后并不会终止程序,而是继续执行后面的代码。
public void printStackTrace(PrintStream s); 把异常的跟踪栈信息输出到指定的输出流。和上一个方法的区别是,上一个方法直接把信息输出到控制台(标准错误输出流。
public StackTraceElement[] getStackTrace(); 返回异常的跟踪栈信息。
public String toString(); 返回异常的简短描述(包括异常类的名字和异常的相关信息)。返回的信息包括getLocalizedMessage()方法返回的信息,getLocalizedMessage()方法返回的是本地的错误信息描述,默认和getMessage()方法返回的值一样。getLocalizedMessage()方法只有被子类异常重写后才有区别。