Skip to content

异常捕获

在 Java 中,凡是可能抛出异常的语句,都可以使用 try catch 捕获。

catch语句

catch 语句可以同时使用多个,每个 catch 语句分别捕获对应的 Exception 及其子类。

JVM 在捕获到异常后,会从上到下匹配 catch 语句,匹配到某个 catch 后,执行 catch 代码块,然后不再继续匹配。

注意

存在多个 catch 的时候,catch的顺序非常重要,子类必须写在前面!

java
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 捕获异常。

java
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 结构:

java
try {
  System.out.println("hello");
} finally {
  System.out.println("world");
}

throws语句

throws 也是 Java 中处理异常的一种方式,但请注意它并不是真正在处理异常,而仅仅是将异常抛出。

注意

  1. 子类使用 throws 抛出异常时,父类调用的方法也需要抛出异常,并且父类抛出的异常必须是子类抛出异常的父类;
  2. 当方法在 main() 方法调用时,就不要给 main() 方法再 throws 抛出异常了,这时候就应该使用 tryCatch 处理异常了;
java
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);
}

异常合并

如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么可以将异常进行合并处理。

java
try {
  Process1();
} catch (IOException | NumberFormatException e) {
  System.out.println("Bad input");
} catch (Exception e) {
  System.out.println("Unknown error");
}

异常传播

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直至遇到 try catch 为止。

java
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)

异常抛出

当发生错误时,例如用户输入了非法的字符,我们就可以抛出异常。

java
private static void process2(String s) {
  if (s == null) {
    throw new NullPointerException();
  }
}

如果一个方法捕获到了某个异常后,又在 catch 子句中抛出新的异常,就相当于把抛出的异常类型 “转换” 了:

java
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)

为了能追踪到完整的异常栈,在构造异常的时候,需要在抛出新异常的实例中,将原始异常当作参数传递到新异常中:

java
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 中的异常还会继续抛出吗?

java
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)。

思考:假如我们就需要获取所有的异常信息,该如何做呢?

java
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 中可以提供多个构造方法,以适应不同的异常抛出。

java
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);
  }
}
java
public class UserNotFoundException extends BaseException {
}
java
public static void main(String[] args) {
  try {
    throw new Exception("原始异常");
  } catch (Exception e) {
    throw new BaseException("自定义异常消息", e);
  }
}

Released under the MIT License.