异常

在实际代码运行过程中可能会出现很多奇奇怪怪的错误,有的是不可控的内部原因:堆栈溢出、磁盘空间不足;有的是不可控的外部原因:比如网络连接故障,更多的是代码逻辑的问题。针对上述问题Java统一使用异常来处理这类问题。

初始异常

空指针异常

  1. String s = null;
  2. s.indexOf("a");
  3. System.out.println("end");

由于变量”s”没有初始化就调用了方法s.indexOf("a"),导致了程序出现异常,屏幕输出了NullPointerException,当程序运行到s.indexOf("a"),Java虚拟机发现变量s没有初始化就调用了实例方法,这时候就启用了”异常处理机制”,首先创建NullPointerException对象,然后查看当前代码所在的方法没没有进行异常处理,如果没有就往上查找……直到主函数main,由于没有代码能够处理该异常,Java虚拟机启用了默认异常处理机制,打印异常栈到屏幕并退出程序,因此System.out.println("end")并没有执行。

数字格式异常

  1. if(args.length < 1){
  2. print("请输入一个数字");
  3. }
  4. int num = Integer.parseInt(args[0]);
  5. System.out.println(num);

这段代码要求用户输入一个数值,比如”123”,然后将输入的字符转换为数值并输出,但如果用户输入的不是数值而是字符串”abc”,这时候就会出现转换异常,屏幕输出:NumberFormatException,我们查看对应代码发现它们通过trhow后面跟着一个异常对象进行了对应的处理,之前我们没看见trhow new NullPointerExceptoin可以认为是Java虚拟即抛出了该异常对象。

throw和return

我们知道return的作用是结束方法运行并返回对应的返回值,程序正常退出,而throw表示抛出一个异常,,这种情况会导致程序异常退出,对于return的位置是确定的,而throw后执行那行代码往往是不确定的,由异常处理机制决定,如果发生了溢出这时候Java虚拟机启用异常处理机制,首先查看当前代码所在方法有没有进行异常处理,如果没有就会依次向上查看……直到找到异常处理,否则Java虚拟机会启用默认异常处理机制,打印异常栈并退出程序。


异常类

前面我们接触过了NullPointerExceptionNumberFormatException,这两个类都有一个共同给的父类Throwable

Throwable

Throwable是所有异常基类的父类,有四个public修饰的构造方法。

  1. public Throwable(){}
  2. public Throwable(String message){}
  3. public Throwable(Throwable cause){}
  4. public Throwable(Stirng message, Throwable cuase){}

在这四个构造方法中主要有两个参数:message表示异常消息,cause表示触发该异常的其它异常,异常可以形成异常链,一层一层的传递,可以通过异常对象.getCause获取上一层异常。

Throwable有一个静态方法initCause用于初始化异常,由于某些异常类的构造方法没有cause这个参数,可以通过initCause来初始化,但是initCause只能调用一次。

异常类体系

Throwable主要有两个子类ErrorException

Error表示系统级别的错误,子类有VirtualMachineError虚拟机错误以及子类OutOfMemoryError内存溢出和StackOverflowError堆栈溢出,Error表示不可调节的错误,由Java虚拟机自动触发不应该在逻辑代码中抛出。

Exception表示异常,主要子类有IOException SQLException RuntimeExceptionRuntimeException从名字上可能点误解,表示运行时异常。但实际上它表示未受检异常Error以及Exception和它的子类表示受检异常

受检异常与未受检异常的区别在于:受检异常必须要求开发人员进行处理,否则编译器会报错。而未受检不需要强制处理。

自定义异常

自定义异常一般是继承RuntimeException或子类或者Exception。继承RuntimeExcption或子类代表未受检异常;而继承Exception代表受检异常。

  1. public class AppException extends Exception{
  2. public AppException(){
  3. super();
  4. }
  5. public AppExceptoin(String message){
  6. super(message);
  7. }
  8. public AppException(String message, Throwable cause){
  9. super(message, cause);
  10. }
  11. public AppException(Throwable cause){
  12. super(cause);
  13. }
  14. }

可以看出我们的自定义异常和其他异常类一样,并没有多余的属性和构造方法,在继承Exception之后自定义异常也只是多了几个构造方法,这些构造方法也只是调用了父类的构造方法。


异常处理

前面示例中我们代码抛出了异常,由于我们没有进行异常处理。导致了Java虚拟机启用了默认异常处理机制,即打印异常栈到标准错误流并退出程序,这对于用户是不友好的。因此需要我们进行异常处理,Java提供catch关键字用于捕获对应异常类。

catch匹配

  1. try{
  2. Integer.parseInt("abc");
  3. String s = null;
  4. s.indexOf("a");
  5. }catch(NumberFormatException e){
  6. print("数值格式不正确");
  7. }catch(RuntimeException e){
  8. pirnt("runtimeException:"+e.getMessage);
  9. }catch(Exception e){
  10. print(”exception“);
  11. }
  12. System.out.println("end");

try块里面包含了可能发生异常的代码块,当发生异常时程序终止try块异常点后续代码的执行,然后执行与catch块中包含的异常类型逐个比较,如果类型相等则执行catch中的内容,由于我们进行了异常处理,因此Java虚拟机没有启用默认异常处理机制。所以打印”end”。

需要注意的是如果抛出异常类是catch块类的子类,那么也会匹配。因此,需要将详细的异常类放在顶层,自顶向下从详细的异常类到异常基类。

重新抛出异常

除了在catch块中处理异常,我们也可以在catch中重新抛出异常。

  1. try{
  2. 5 / 0;
  3. }catch(ArithmeticException e){
  4. throw new AppException("算术异常",e);
  5. }
  1. 为什么要抛出异常?

    • 可能当前类处理不了该异常,因此抛出由上层处理
  2. 为什么要重新抛出异常

    • 可能当前异常类不太合适,或者描述不够准确,如果感兴趣可以通过异常类.getCause()或抛出该异常的原异常

finally

对于一些场景如数据库连接,IO。使用者在使用完之后一般需要释放连接。释放连接如果同时写在trycatch中可能比较麻烦,Java提供了finally关键字。

  1. 如果没有发生异常,那么在try执行结束后执行finally
  2. 如果发生异常且被捕获,那么执行catch块后执行finally
  3. 如果发生异常且没有被捕获,那么在异常抛出之前执行fially

关于finally还有一个小细节,如果trycatch中包含return,则return语句在finally语句执行结束之后执行。但fially不能改变返回值

被finally控制的语句一定会执行,除非JVM退出。

  1. int result = 0;
  2. try{
  3. return result;
  4. }finally{
  5. result = 2;
  6. }

最终函数返回结果为0,实际执行过程是:在执行return之前,会有一个临时变量存储result的值,然后执行finallyresult的值为2,然后返回临时变量的值0,result的结果为2。

  1. public class Application{
  2. public static int num = 10;
  3. public static int test(){
  4. try{
  5. Application app = new Application();
  6. return app.num;
  7. }finally{
  8. app.num = 10;
  9. }
  10. }
  11. }

这里的结果是10

不仅只有try-catch-finally还可以有try-finallytry-catch。如果finally中有return语句那么finally中的return将会覆盖掉trycatch中的return语句返回值。

  1. try{
  2. return 10;
  3. }catch(Exception e){
  4. return 20;
  5. }finally{
  6. return 30;
  7. }

如果finally中有异常抛出那么将会覆盖掉其他异常。

  1. try{
  2. 5 / 0;
  3. }catch(Exceptoin e){
  4. throw new AppException("");
  5. }finally{
  6. throw new RuntimeException("");
  7. }

真是因为finally块中的return和异常抛出会覆盖掉trycatch中的声明,因此,作为一个良好的实践,应该避免在finally块中使用return或者throw

try-with-resources

对于一些资源使用场景,经常在使用完之后需要释放连接,典型的写法是在finally中释放连接,针对这种情况Java7开始支持一种新语法try-with-resource,这种语法实现了java.lang.autoCloseable接口,该接口定义如下:

  1. public interface AutoCloseable{
  2. void close() throws Exception;
  3. }

没有使用try-with-resource,使用定义如下:

  1. public static void useResource(){
  2. AutoCloseable r = new FileInputStream("hello");
  3. try{
  4. }finally{
  5. r.close();
  6. }
  7. }

使用try-with-resource使用定义如下:

  1. public static void useResource(){
  2. try(Autocloseable r = new FileInputStream("hello")){
  3. }
  4. }

资源声明和初始化都方法在try语句内,在语句执行完之后,会自动调用close()方法。资源可以定义外面但必须是或者形式上是final(虽然没有声明为final但也没有改变引用)。

throws

trhows用于一个方法在执行过程中可能会抛出的异常。

  1. public static void test() throws AppException,IOException,NumberFormatException{
  2. }

throws写在方法声明后面,可以声明多个异常使用逗号隔开。这个声明的含义是这个方法在执行过程中可能会抛出的异常,且没有这些方法进行处理,调用者必须处理或者既继续向上抛出,最为一个良好的实践,应该使用注释中说明什么情况下会抛出异常。

对于未受检异常,可以不声明而抛出,而对于受检异常不可以抛出而不声明,否者编译器会报错。throws常用在

父类中,父类方法中可能没有定义需要抛出的异常,但子类重写后可能会抛出受检异常,但是同时父类又没有声明。因此,只能把需要抛出的异常都写在父类了。

如果一个方法调用了另一个声明抛出的受检方法,则必须处理这些异常,处理的方式可以是catch也可以是throws

  1. public void test() throws AppException{
  2. try{
  3. fn();
  4. }catch(SQLException e{
  5. e.printStackTrace();
  6. }
  7. }

对于fn()抛出的SQLException使用catch处理,而AppExcetion向上抛出。

未受检和受检异常

受检异常必须出现在throws里,调用者必须处理,Java编译器会强制要求这一点,而未受检异常则没有这个区别,为什么会有受检异常和未受检异常这个区分?我们自定义异常是使用受检异常还是未受检异常?未受检异常表示编程的逻辑错误,编程是应该避免这类问题,比如空指针异常,如果真出现了空指针异常,程序退出也是正常的,应该检查bug而不是想办法处理,对于受检异常表示程序本身没问题,但由于网络、IO、数据库等其他问题导致了异常,调用者应该适当处理。

其实编程错误也是应该处理的,尤其是Java广泛应用于服务端,不能因为一个逻辑错误而导致整个程序退出。Java受检异常和未受检异常的区分没有太大的意义,可以统一使用未受见异常来处理。这种观点的基本理由是:无论是受检异常还是未受检异常无论是否在throws中,都应该在合适的地方以适当的方式处理,而不是为了满足编译器的要求而盲目处理异常,既然都要处理异常,受检异常的强制声明就显得有点繁琐,尤其是在调用情况比较深的情况下。


如何使用异常

异常应该且仅用于异常情况

异常应该且仅用于异常情况,比如在循环迭代数组时候,应该检查索引是否有效。而不是等到抛出ArrayIndexOutOfBoundsException导致循环结束。对于一个引用变量,使用前应该检查其有效性,而不是让它抛出NPE

真正出现异常情况的时候,应该抛出异常,而不是返回特殊值。比如String类的subString方法实现,如下:

  1. public String subString(int beginIndex){
  2. if(beginIndex < 0){
  3. throw new StringIndexOutOfBoundsException(beginIndex);
  4. }
  5. int subLen = value.length - beginIndex;
  6. if(subLen < 0){
  7. throw new StringIndexOutOfBoundsException(subLen);
  8. }
  9. return beginIndex == 0 ? this : new Stirng(value, beginIndex, value);
  10. }

方法会判断beginIndex的有效性,如果无效会抛出StringIndexOutOfBoundsException异常。纯技术上的解决方法是不抛出异常而返回特殊值null,但beginIndex无效是异常情况,异常不能作为正常情况处理。

异常处理的目标

异常可以分为三个目标:用户,开发,第三发;对用户而言一般是输入错误;对程序而言一般是逻辑上的错误;而第三方泛指其他错误:如IO、数据库、第三方服务等。异常的处理可以分为报告和恢复;如果是输入错误对用户报告可以尽可能白话;如果是程序逻辑上的错误,可以联系客户或尝试重新连接。而对于开发或运维则需要详细的日志记录:异常链、异常栈等。恢复是指通过程序自动恢复。

异常处理的一般逻辑

  1. 如果知道如何处理异常,就自己处理
  2. 如果异常可以通过程序自动解决,就自动解决
  3. 如果知道异常如何解决,就不要向上报告而是解决异常
  4. 如果不能完全解决,应该向上报告
  5. 如果有额外的信息提供,就应该提供,可以以原异常做为cause重新抛出一个异常。

在没有异常机制的情况下,判断方法是不是异常情况就是通过返回值,方法根据是否异常返回不同的值。而调用者需要通过返回值进行判断是否为异常情况,并进行相对应的处理,每一层都需要对调用的方法的不同的返回值进行判断和处理,程序的正常逻辑和异常逻辑混在一起,代码往往难以阅读和维护。

有了异常之后,程序正常的逻辑和异常逻辑相分离,异常情况可以集中处理。异常还可以向上传递,不需要每层方法都进行处理,异常也不再可能被忽略。