在Java中,凡是可能抛出异常的语句,都可以用try...catch捕获。把可能发生异常的语句放在try{...}中,然后使用catch捕获对应的Exception及其子类。
可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。简单地说就是多个catch语句只有一个能被执行。例如:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e) {
System.out.println(e);
} catch (NumberFormatException e) {
System.out.println(e);
}
}
存在多个catch的时候,catch的顺序非常重要,子类必须写在前面,例如:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e) {
System.out.println("IO error");
} catch (UnsupportedEncodingException e) { // 永远捕获不到
System.out.println("Bad encoding");
}
}
对于上面的代码,UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类,当抛出UnsupportedEncodingException异常时,会被catch(IOException e){...}捕获并执行。因此,正确的做法应该是把子类一场放到前面:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
}
}
无论是否有异常发生,我们都希望执行一些语句,例如清理工作,可以把执行语句写若干遍:正常执行的放在try中,每个catch再写一遍,例如:
public static void main(String[] args) {
try {
process1();
process2();
process3();
System.out.println("中国抗击疫情必将胜利!!!"); //这行代码是我们想必须执行的
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
System.out.println("END");
} catch (IOException e) {
System.out.println("IO error");
System.out.println("END");
}
}
但是这行代码是我们想必须执行的System.out.println("中国抗击疫情必将胜利!!!"); 但是如果抛出前面调用方法如果产生异常的话,就不会执行这一行代码了,那我们可以利用finally语句块保证有无错误都会执行,可改写上述代码为:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
} finally {
System.out.println("中国抗击疫情必将胜利!!!"); //这行代码是我们想必须执行的
}
}
注意finally的几个特点:
1.finally语句不是必须的,可以写可以不写;
2.finally总是最后执行的;
如果没有没有发生异常,就正常执行try{...}语句块,然后执行finally。如果发生了异常,就中断执行try{...}语句块,然后跳转执行匹配的catch语句块,最后执行finally。可见finally是用来保证一些代码必须执行的。
某些情况下,我们也可以没有catch,只是用try...finally结构的,例如:
void process(String file) throws IOException {
try {
...
} finally {
System.out.println("中国抗击疫情必将胜利!!!"); //这行代码是我们想必须执行的
}
}
3.捕获多种异常:
如果某些异常的处理逻辑相同,但是异常本身之前又不存在继承关系,那么就需要编写多条catch语句,例如:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e) {
System.out.println("Bad input");
} catch (NumberFormatException e) {
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
因为处理IOException和NumberFormatException的代码时相同的,所以我们可以把她它两用 | 合并到一起,例如:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
使用try...catch...finally捕获异常小结:
1.多个catch语句的匹配顺序非常重要,子类必须放在前面。
2.finally语句保证了有无异常都会执行,它时可选的。
3.一个catch语句也可以匹配多个非继承关系的异常。
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try...catch被捕获为止:
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
process2();
}
static void process2() {
Integer.parseInt(null); // 会抛出NumberFormatException
}
}
通过printStackTrace()可以打印出方法的调用栈,例如:
java.lang.NumberFormatException: null
at java.base/java.lang.Integer.parseInt(Integer.java:614)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.process2(Main.java:16)
at Main.process1(Main.java:12)
at Main.main(Main.java:5)
printStackTrace()对于调试错误非常有用,上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层依次是:
1.main()调用process1();
2.process1()调用process2();
3.process2()调用Integer.parseInt(String);
4.Integer.parseInt(String)调用Integer.parseInt(String,int);
查看Integer.java源码可知,抛出异常方法的代码如下:
public static int parseInt(String s, int radix) throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("null");
}
...
}
当发生错误时,比如用户输入了 非法的字符,我们就可以抛出异常,如何抛出异常呢?参考上面Integer.parseInt()方法,抛出异常分两步:
1.创建某个Exception的实例;
2.使用throw语句抛出;
例如:
void process2(String s) {
if (s==null) {
NullPointerException e = new NullPointerException();
throw e;
//throw new NullPointerException();
}
}
实际上绝大部分抛出异常的代码会合成一行如上述注释掉的那样或者下述这样:
void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
如果一个方法捕获了某个异常后,又在catch自居中抛出新的异常,就相当于把抛出的异常类型转换了:
void process1(String s) {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}
void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
当process2()抛出NullPointerException后,被process1()捕获,然后抛出IllegalArgumentException()。如果在main()中捕获IllegalArgumentException,wine吧打印异常栈看看,如下:
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}
static void process2() {
throw new NullPointerException();
}
}
打印出的异常栈类似于:
java.lang.IllegalArgumentException
at Main.process1(Main.java:15)
at Main.main(Main.java:5)
这说明新的异常丢失了原始异常或者说根本异常的信息,我们已经看不到原始异常NullPointerException的信息了。为了能够追踪到完整的异常栈信息,在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始Exception信息了,对上述代码修改如下:
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
}
}
static void process2() {
throw new NullPointerException();
}
}
运行上述代码,打印出的异常栈类似于,如下:
java.lang.IllegalArgumentException: java.lang.NullPointerException
at Main.process1(Main.java:15)
at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
at Main.process2(Main.java:20)
at Main.process1(Main.java:13)
注意到:Caused by:Xxx,说明捕获的IllegalArgumentException并不是造成问题的根本原因,根本原因在于NullPointerException,时在Main.process2()方法中抛出的。在代码中获取原始异常信息可以使用Throwable.getCause()方法。如果返回null,则说明该异常已经是产生问题根本异常了。有了完整的异常栈信息,我们才可以快速的定位并修改代码解决问题。
捕获异常并再次抛出时,一定要保留原始异常,以便于更快的发现第一案发现场!!!
如果我们在try或者catch语句块中抛出异常,finally语句块是否还会执行呢?例如:
public class Main {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
System.out.println("catched");
throw new RuntimeException(e);
} finally {
System.out.println("finally");
}
}
}
上述代码执行结果如下:
catched
finally
Exception in thread "main" java.lang.RuntimeException: java.lang.NumberFormatException: For input string: "abc"
at Main.main(Main.java:8)
Caused by: java.lang.NumberFormatException: For input string: "abc"
at ...
第一行打印了catchd,说明进入了catch语句块,第二行打印了finally,说明执行了finally语句块,因此我们可以得知,在catch中抛出异常时,不会影响finally的执行,JVM会先执行finally,然后再抛出了异常。
如果在执行finally语句时抛出异常,那么catch语句块还能否继续抛出呢? 例如:
public class Main {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
System.out.println("catched");
throw new RuntimeException(e);
} finally {
System.out.println("finally");
throw new IllegalArgumentException();
}
}
}
执行上述代码发现异常信息如下:
catched
finally
Exception in thread "main" java.lang.IllegalArgumentException
at Main.main(Main.java:11)
控制台的输出说明finally抛出异常后,原来在catch中准备抛出的异常就"消失"了,因为只能抛出一个异常。没有被抛出的异常称之为"被屏蔽"的异常(Suppressed Exception)。在极少数情况下,我们需要获知所有的异常,那该如何保存所有的异常信息呢?方法是先用变量保存原始一场,然后调用Throwable.addSuppressed(),把原始一场添加进来,然后在finally中抛出,如下:
public class Main {
public static void main(String[] args) throws Exception {
Exception origin = null;
try {
System.out.println(Integer.parseInt("abc"));
} catch (Exception e) {
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
e.addSuppressed(origin);
}
throw e;
}
}
}
当catch和finally都抛出了异常时,虽然catch的异常被屏蔽了,但是finally抛出的异常仍然包含了它:
Exception in thread "main" java.lang.IllegalArgumentException
at Main.main(Main.java:11)
Suppressed: java.lang.NumberFormatException: For input string: "abc"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.main(Main.java:6)
通过Throwable.getSuppressed()可以获取所有的Suppressed Exception。绝大多数情况下,在finally中不要抛出异常。因此,我们通常不关心Suppressed Exception。
抛出异常小结:
1.调用printStackTrace()可以打印异常的传播栈,对于调试程序非常有效;
2.捕获异常并再次抛出新的异常时,应该持有原始异常信息;
3.通常不要在finally中抛出异常。如果在finally中抛出异常,应该将原始异常加入到哦原有异常中,调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception。
当我们在代码中需要抛出异常时,尽量使用JDK已经定义的异常。例如:参数类型不合法,我们应该抛出IllegalArgumentException:
static void process1(int age) {
if (age <= 0) {
throw new IllegalArgumentException();
}
}
当然在大型项目中,可以自定义新的异常类型,但是,保持一个合理的一场继承体系是非常重要的,通常的做法是自定义一个异常作为"根异常",然后,派生出各种业务类型的异常。根异常需要从一个适合的Exception派生,通常建议从RuntimeException派生:
public class BaseException extends RuntimeException {
}
其他异常类型都可以从根异常派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
...
自定义的根异常应该是需要提供多个构造方法的(最好是跟RuntimeException对应):
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
在上述构造方法参考RuntimeException原生实现。这样,抛出异常的时候,就可以选择合适的构造方法。
自定义异常小结:
1.抛出异常时,尽可能复用JDK已经定义好的异常类型;
2.自定义异常体系时,建议从RuntimeException类派生根异常,然后从根异常在派生出其他类型的异常。
3.自定义异常时,尽量模仿RuntimeException类,提供多种构造方法。