error
Table of Contents

异常处理

获取调用失败

方法一:约定返回错误码。

例如,处理一个文件,如果返回0,表示成功,返回其他整数,表示约定的错误码:

int code = processFile("C:\\test.txt");
if (code == 0) {
    // ok:
} else {
    // error:
    switch (code) {
    case 1:
        // file not found:
    case 2:
        // no read permission:
    default:
        // unknown error:
    }
}

方法二:在语言层面上提供一个异常处理机制。

Java内置了一套异常处理机制,总是使用异常来表示错误。

异常是一种class,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了:

try {
    String s = processFile(C:\\test.txt);
    // ok:
} catch (FileNotFoundException e) {
    // file not found:
} catch (SecurityException e) {
    // no read permission:
} catch (IOException e) {
    // io error:
} catch (Exception e) {
    // other error:
}

继承关系

                     ┌───────────┐
                       Object   
                     └───────────┘
                           
                           
                     ┌───────────┐
                      Throwable 
                     └───────────┘
                           
                 ┌─────────┴─────────┐
                                    
           ┌───────────┐       ┌───────────┐
              Error           Exception 
           └───────────┘       └───────────┘
                                    
         ┌───────┘              ┌────┴──────────┐
                                              
┌─────────────────┐    ┌─────────────────┐┌───────────┐
OutOfMemoryError ... RuntimeException ││IOException...
└─────────────────┘    └─────────────────┘└───────────┘
                                
                    ┌───────────┴─────────────┐
                                             
         ┌─────────────────────┐ ┌─────────────────────────┐
         NullPointerException  IllegalArgumentException ...
         └─────────────────────┘ └─────────────────────────┘

Error

Error表示严重的错误,程序对此一般无能为力,例如:

Exception

Exception则是运行时的错误,它可以被捕获并处理。

某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:

还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:

Exception又分为两大类:

  1. RuntimeException以及它的子类;
  2. RuntimeException(包括IOExceptionReflectiveOperationException等等)

Java规定:

捕获异常

捕获异常使用try...catch语句,把可能发生异常的代码放到try {...}中,然后使用catch捕获对应的Exception及其子类

public class Main {
    public static void main(String[] args) {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) {
        try {
            // 用指定编码转换String为byte[]:
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
            System.out.println(e); // 打印异常信息
            return s.getBytes(); // 尝试使用用默认编码
        }
    }
}

>>>
[-42, -48, -50, -60]

多catch语句

可以使用多个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) { ... }捕获并执行

finally语句

无论是否有异常发生,如果我们都希望执行一些语句

Java的try ... catch机制还提供了finally语句,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("END");
    }
}

某些情况下,可以没有catch,只使用try ... finally结构。例如:

void process(String file) throws IOException {
    try {
        ...
    } finally {
        System.out.println("END");
    }
}

捕获多种异常

如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条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");
    }
}

因为处理IOExceptionNumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起:

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");
    }
}

printStackTrace() 异常调用栈

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
    }
}

>>>
java.lang.NumberFormatException: null
    at java.base/java.lang.Integer.parseInt(Integer.java:620)
    at java.base/java.lang.Integer.parseInt(Integer.java:776)
    at Main.process2(Main.java:15)
    at Main.process1(Main.java:11)
    at Main.main(Main.java:4)
  1. main()调用process1()
  2. process1()调用process2()
  3. process2()调用Integer.parseInt(String)
  4. Integer.parseInt(String)调用Integer.parseInt(String, int)

Throwable.getCause() 获取原始异常

在代码中获取原始异常可以使用Throwable.getCause()方法。如果返回null,说明已经是“根异常”了。

throw 抛出异常

throw语句抛出

void process2(String s) {
    if (s==null) {
        NullPointerException e = new NullPointerException();
        throw e;
    }
}

实际上,绝大部分抛出异常的代码都会合并写成一行:

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

异常屏蔽 (不常用)

finally抛出异常后,原来在catch中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)

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
Exception in thread "main" finally
java.lang.IllegalArgumentException
    at Main.main(Main.java:10)

绝大多数情况下,在finally中不要抛出异常。因此,我们通常不需要关心Suppressed Exception

获知所有的异常

在极少数的情况下,我们需要获知所有的异常

先用origin变量保存原始异常,然后调用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;
        }
    }
}

>>>
Exception in thread "main" java.lang.IllegalArgumentException
    at Main.main(Main.java:10)
    Suppressed: java.lang.NumberFormatException: For input string: "abc"
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)
        at java.base/java.lang.Integer.parseInt(Integer.java:658)
        at java.base/java.lang.Integer.parseInt(Integer.java:776)
        at Main.main(Main.java:5)

自定义异常

Java标准库定义的常用异常包括:

Exception

├─ RuntimeException
  
  ├─ NullPointerException
  
  ├─ IndexOutOfBoundsException
  
  ├─ SecurityException
  
  └─ IllegalArgumentException
     
     └─ NumberFormatException

├─ IOException
  
  ├─ UnsupportedCharsetException
  
  ├─ FileNotFoundException
  
  └─ SocketException

├─ ParseException

├─ GeneralSecurityException

├─ SQLException

└─ TimeoutException

在代码中需要抛出异常时,尽量使用JDK已定义的异常类型

static void process1(int age) {
    if (age <= 0) {
        throw new IllegalArgumentException();
    }
}

异常继承

自定义新的异常类型

一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。

BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:

public class BaseException extends RuntimeException {
}

其他业务类型的异常就可以从BaseException派生:

public class UserNotFoundException extends BaseException {
}

public class LoginFailedException extends BaseException {
}

...

自定义的BaseException应该提供多个构造方法:

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);
    }
}

断言 Assertion (不常用)

断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言

实际开发中,很少使用断言。更好的方法是编写单元测试

开启

JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行。

要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言

public static void main(String[] args) {
    double x = Math.abs(-123.45);
    assert x >= 0;
    System.out.println(x);
}

assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError

AssertionError

断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段

应用场景

对于可恢复的程序错误,不应该使用断言。例如:

void sort(int[] arr) {
    assert arr != null;
}

应该抛出异常并在上层捕获:

void sort(int[] arr) {
    if (x == null) {
        throw new IllegalArgumentException("array cannot be null");
    }
}