异常捕获
在 Java 中,凡是可能抛出异常的语句,都可以使用 try catch
捕获。
catch语句
catch
语句可以同时使用多个,每个 catch
语句分别捕获对应的 Exception
及其子类。
JVM 在捕获到异常后,会从上到下匹配 catch
语句,匹配到某个 catch
后,执行 catch
代码块,然后不再继续匹配。
注意
存在多个 catch
的时候,catch
的顺序非常重要,子类必须写在前面!
public static void main(String[] args) {
try {
Process1();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad Encoding");
} catch (IOException e) {
System.out.println("IO Exception");
}
}
上面代码中 UnsupportedEncodingException 是 IOException 的子类,因此 UnsupportedEncodingException 在前,IOException 在后。
finally语句
finally
语句用于 不论是否有异常发生,它里面的代码都会执行。因为 JVM 会先执行 finally
,再 catch
捕获异常。
public static void main(String[] args) {
try {
Integer.parseInt("abc"); // ❌
} catch (Exception e) {
System.out.println("catched exception");
throw new RuntimeException();
} finally {
System.out.println("finally");
}
}
可以看到控制台输出如下信息:
catched exception
finally
Exception in thread "main" java.lang.RuntimeException
at com.Exception.ExceptionTest.main(ExceptionTest.java:9)
在某些情况下,可以没有 catch
,只是用 try finally
结构:
try {
System.out.println("hello");
} finally {
System.out.println("world");
}
throws语句
throws
也是 Java 中处理异常的一种方式,但请注意它并不是真正在处理异常,而仅仅是将异常抛出。
注意
- 子类使用
throws
抛出异常时,父类调用的方法也需要抛出异常,并且父类抛出的异常必须是子类抛出异常的父类; - 当方法在
main()
方法调用时,就不要给main()
方法再throws
抛出异常了,这时候就应该使用 tryCatch 处理异常了;
public static void main(String[] args) {
try {
method();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void method() throws IOException {
method1();
}
public static void method1() throws FileNotFoundException {
File file = new File("D:\\test.txt");
FileInputStream fileInputStream = new FileInputStream(file);
}
异常合并
如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么可以将异常进行合并处理。
try {
Process1();
} catch (IOException | NumberFormatException e) {
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
异常传播
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直至遇到 try catch 为止。
public static void main(String[] args) {
try {
process1();
} catch (Exception e) { // 捕获 NumberFormatException 异常
e.printStackTrace();
}
}
private static void process1() {
process2();
}
private static void process2() {
Integer.parseInt(null); // 抛出 NumberFormatException 异常
}
上面 e.printStackTrace()
方法可以打印出方法的调用栈,可以看到详细的报错信息:
java.lang.NumberFormatException: Cannot parse null string
at java.base/java.lang.Integer.parseInt(Integer.java:550)
at java.base/java.lang.Integer.parseInt(Integer.java:685)
at com.Exception.ExceptionTest.process2(ExceptionTest.java:17)
at com.Exception.ExceptionTest.process1(ExceptionTest.java:13)
at com.Exception.ExceptionTest.main(ExceptionTest.java:6)
异常抛出
当发生错误时,例如用户输入了非法的字符,我们就可以抛出异常。
private static void process2(String s) {
if (s == null) {
throw new NullPointerException();
}
}
如果一个方法捕获到了某个异常后,又在 catch 子句中抛出新的异常,就相当于把抛出的异常类型 “转换” 了:
private static void process1() {
try {
process2(null);
} catch (NullPointerException e) {
throw new IllegalArgumentException(); //抛出了新的异常
}
}
private static void process2(String s) {
if (s == null) {
throw new NullPointerException(); //原始异常
}
}
上面的示例中,process2() 抛出 NullPointerException 异常后,被 process1() 捕获,然后又抛出了新的异常 IllegalArgumentException。
此时,执行代码可以看到下面的异常栈,它丢失了原始的异常信息:
java.lang.IllegalArgumentException
at com.Exception.ExceptionTest.process1(ExceptionTest.java:16)
at com.Exception.ExceptionTest.main(ExceptionTest.java:6)
为了能追踪到完整的异常栈,在构造异常的时候,需要在抛出新异常的实例中,将原始异常当作参数传递到新异常中:
private static void process1() {
try {
process2(null);
} catch (NullPointerException e) {
throw new IllegalArgumentException(e); //将原始异常 e 传递给新异常
}
}
private static void process2(String s) {
if (s == null) throw new NullPointerException();
}
此时的异常栈信息如下:
java.lang.IllegalArgumentException: java.lang.NullPointerException
at com.Exception.ExceptionTest.process1(ExceptionTest.java:16)
at com.Exception.ExceptionTest.main(ExceptionTest.java:6)
Caused by: java.lang.NullPointerException
at com.Exception.ExceptionTest.process2(ExceptionTest.java:21)
at com.Exception.ExceptionTest.process1(ExceptionTest.java:14)
... 1 more
注意 Caused by: xxx,说明 IllegalArgumentException 并不是造成异常的根源,根源在于 NullPointerException。
异常屏蔽
思考:如果在 finally 中抛出异常,那么 catch 中的异常还会继续抛出吗?
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
System.out.println("catched exception");
throw new RuntimeException(); //异常被屏蔽
} finally {
System.out.println("finally");
throw new IllegalArgumentException(); //抛出异常
}
}
运行代码,可以看到异常栈报错信息如下:
catched exception
finally
Exception in thread "main" java.lang.IllegalArgumentException
at com.Exception.ExceptionTest.main(ExceptionTest.java:12)
通过异常栈可以看出,在 finally 中抛出异常,原来在 catch 中准备抛出的异常就消失了。这种没有被抛出的异常称为 “被屏蔽” 的异常(Suppressed Exception)。
思考:假如我们就需要获取所有的异常信息,该如何做呢?
public static void main(String[] args) throws Exception {
Exception origin = null;
try {
Integer.parseInt("abc");
} catch (Exception e) {
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
e.addSuppressed(origin);
}
throw e;
}
}
可以看到此时的异常栈中,就包含了 catch 中的异常信息:
Exception in thread "main" java.lang.IllegalArgumentException
at com.Exception.ExceptionTest.main(ExceptionTest.java:12)
Suppressed: java.lang.NumberFormatException: For input string: "abc"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:588)
at java.base/java.lang.Integer.parseInt(Integer.java:685)
at com.Exception.ExceptionTest.main(ExceptionTest.java:7)
自定义异常
当我们在代码中需要抛出异常时,尽量使用 JDK 已定义的异常类型。
但是,我们也可以自定义异常,但是需要尽量保持一个合理的异常继承体系。
常见的做法是定义一个 BaseException
最为 “根异常”,并继承自 RuntimeException
类,然后派生出各种业务类型的异常。
INFO
BaseException
中可以提供多个构造方法,以适应不同的异常抛出。
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
}
public class UserNotFoundException extends BaseException {
}
public static void main(String[] args) {
try {
throw new Exception("原始异常");
} catch (Exception e) {
throw new BaseException("自定义异常消息", e);
}
}