ZB-025-02-异常的类型体系

throw/throws

throw 丢出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
b();
}

private static void b(){
c();
}

private static void c(){
throw new RuntimeException();
}
}

所有人都不处理这个异常会怎么样

  • 答案是: 终止这个线程,如果都没人抓这个异常,异常就会把当前的线程杀掉,这是默认的处理方式

throws 用于声明方法将来可能会丢出什么样的东西

1
2
3
4
5
private static void b() throws  Exception{
if(true){
throw new Exception();
}
}

如果一个方法抛出了异常,那么调用它的地方都要 主动抛出异常 或者 try/catch

throw/throws区别

  • throw 是一个语句,可以声明自己要抛出的异常,不管这个异常怎样被处理,只管往外抛
  • throws 只是声明当前方法 有可能 会抛出 XXException,代价就是调用该方法的地方都要 throws XXException 或者 try/catch

让你诧异的地方

throws RuntimeException 时候 a 不报错

1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws RuntimeException{

}

throws Exception 时候 a 竟然报错了

1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws Exception{

}

Java的异常体系

1
2
3
4
5
6
7
8
Throwable 
所有 错误 和 异常的父类 ,任何的 throwable 都可以被丢出来
它是有毒的
-|Exception (checked exception) 受检异常,有毒
-|RuntionException 运行时异常 无毒
_|NullPointerException

-|Error 错误 无毒

什么是 有毒 什么是 无毒

”有毒“学名叫做 受检异常

  • 抛出了Throwable 或者 Throwable 的子类 都是有毒的,就是你必须处理它 要么你也 throws 要么 try/catch
1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws Throwable{

}

“无毒” 学名叫做 不受检异常

  • 不用去处理,你可以随便抛
    • RuntimeException
    • NullPointerException
    • Error
1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws RuntimeException{

}

java异常体系的最初的设计想法

1994年 ,假如显示的抛出异常,这样别人就必须处理,是不是能让软件更加安全呢?

  • 当时的想法:是的
  • 后来发现:实在太麻烦了,于是大家纷纷讨厌了这种设计思路。

Exception 代表我预期,可能会发生的情况的一种状态

  • 预期之内的, 如文件找不到,断网了
  • 所以当时的思路是,任何调用throws Exception 的方法的人都该对预期之内的情况进行处理 ,这就是最初设计的目的

RuntimeException 代表什么呢?

  • 意料之外的异常,因此不需要声明,所以你可以随便丢
  • 发生了就是一个bug

Error 是什么

  • 异常是可以恢复的,而 Error 是不可以恢复的异常
  • 意思就是 “错误”
  • 大多数情况 代表 不正常 的情况

你可以丢出不同的异常,因为它们都是 Exception 的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void b(){
try {
c();
} catch (FileNotFoundException e) {
e.printStackTrace();
System.out.println("文件没找到");
} catch (EOFException e) {
e.printStackTrace();
System.out.println("文件末尾");
} catch (Exception e) {
e.printStackTrace();
}
}


private static void c () throws Exception {
if(fileNotFound()){
throw new FileNotFoundException();
}else if(readFileEnd()){
throw new EOFException();
}else{
throw new IOException();
}
}

如果一个方法抛出多个异常,你应该按照 从小到大 的顺序捕获异常

  • 为什么? 因为 Exception 是其他异常的父类,优先被匹配了,这样就不会走进其他 catch 里了
1
2
3
4
5
6
7
8
9
10
11
12
13
try {
c();
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("文件没找到");
} catch (RuntimeException e) {
e.printStackTrace();
System.out.println("文件末尾");
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable t){

}

比如一个方法抛出了多个异常,其中有两个处理都是 打印日志,你可以 折叠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void b(){
try {
c();
} catch (NullPointerException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Throwable t){

}
}

// 折叠它 对处理方式一样的异常 来合并 catch 块

private static void b(){
try {
c();
} catch (NullPointerException | IllegalAccessException e) {
e.printStackTrace();
} catch (Throwable t){

}
}

异常的栈轨迹 Stacktrace

  • 排查问题最重要的信息,没有之一
  • 排查问题最重要的信息,没有之一
  • 排查问题最重要的信息,没有之一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.io.demo;

public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
b();
}

private static void b(){
c();
}

private static void c () {
d();
}

private static void d () {
if(true){
throw new RuntimeException();
}
}
}

栈轨迹如下

1
2
3
4
5
6
7
8
9
Exception in thread "main" java.lang.RuntimeException
at com.io.demo.Main.d(Main.java:23)
at com.io.demo.Main.c(Main.java:18)
at com.io.demo.Main.b(Main.java:14)
at com.io.demo.Main.a(Main.java:10)
at com.io.demo.Main.main(Main.java:6)

沿着栈轨迹 我们知道方法的执行过程,从哪里报错了
这是排查错误最重要的信息

为异常添加额外信息,以便排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.io.demo;

import java.sql.SQLException;

public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
processUserData();
}

private static class UserAlreadyExistException extends RuntimeException{
public UserAlreadyExistException(String msg, Throwable cause) {
super(msg,cause);
}
}

private static void processUserData () {
Integer userId = 1;
try {
insertIntoDatabase();
} catch (SQLException e) {
// 额外包一层 用于方便排查信息
throw new UserAlreadyExistException("插入id值为:" + userId + "的数据发生了异常",e);
}
}

private static void insertIntoDatabase () throws SQLException {
throw new SQLException("重复的键值");
}
}

// 栈轨迹如下

Exception in thread "main" com.io.demo.Main$UserAlreadyExistException: 插入id值为:1的数据发生了异常
at com.io.demo.Main.processUserData(Main.java:27)
at com.io.demo.Main.a(Main.java:12)
at com.io.demo.Main.main(Main.java:8)
Caused by: java.sql.SQLException: 重复的键值
at com.io.demo.Main.insertIntoDatabase(Main.java:32)
at com.io.demo.Main.processUserData(Main.java:24)
... 2 more

异常链 Caused by

异常抛出的原则

  • 能⽤if/else处理的,不要使⽤异常

    • https://github.com/hcsp/http-login-and-use-cookie/pull/7
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      a(){
      try{
      return xxx.length();
      }catch(NullPointerException e){
      return 0;
      }
      }

      a(){
      if(xxx != null){
      return xxx.length();
      }else{
      return 0;
      }
      }

      public int getNameLength() {
      // Fix the NullPointerException thrown in this method
      // 在本方法中,修复抛出的空指针异常(NullPointerException)
      - return name.length();
      + try {
      + return name.length();
      + }catch(NullPointerException e) {
      如果可以使用“正常”的流程完成,尽量不要使用try/catch。
      原因有二:
      1. 无法保证catch到的异常一定是你想要抓住的。你可以错误的catch到了更深处的异常。比如说,这里你怎么保证catch到的一定是name为null抛出来的,而不是name.length()方法里面抛出来的?(当然这个例子可能有点极端)
      2. 相比正常的if判断,异常的创建是非常,非常昂贵的操作
  • 尽早抛出异常

    • 当你碰到一个异常时?
    • 首先问自己 能处理这个异常吗?
    • 如果不能处理就立刻抛出来
  • 异常要准确、带有详细信息
    • 一个写错的异常,比没有异常更可怕,你可以定义很多异常
    • IOException 是很宽泛的异常,你该抛出更具体的如 FileNotFoundException / EOFException等更准确的异常
    • 尽可能带有更多的信息,如上面我们自己定义的UserAlreadyExistException
  • 抛出异常也⽐悄悄地执⾏错误的逻辑强的多
    • 抓住了异常,就直接处理不要继续往下走流程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      a(){
      String result = "";
      try {
      result = xxx;
      } catch (IOException e){
      e.printStackTrace();
      // 这样要么抛出异常 要么return
      }
      // 上面抓到了异常,此时就不该继续返回 result
      return result;
      }

      b(){
      try {
      result = xxx;
      } catch (IOException e){
      // e.printStackTrace();
      不打印栈轨迹,你抓住了,但是你把异常吃掉了,很难排查问题
      }
      }

异常的处理原则

  • 本⽅法是否有责任处理这个异常?
    • 不要处理不归⾃⼰管的异常
  • 本⽅法是否有能⼒处理这个异常?
    • 如果⾃⼰⽆法处理,就抛出
  • 如⾮万分必要,不要忽略异常
    1
    2
    3
    4
    try {
    result = xxx;
    } catch (IOException e){}
    这里就是把异常吃了,如果出错,没人发现的了这个异常

了解和使⽤JDK内置的异常

  • NullPointerException
  • ClassNotFoundException/NoClassDefFoundError
  • IllegalStateException
  • IllegalArgumentException
  • IllegalAccessException
  • ClassCastException