异常
在实际代码运行过程中可能会出现很多奇奇怪怪的错误,有的是不可控的内部原因:堆栈溢出、磁盘空间不足;有的是不可控的外部原因:比如网络连接故障,更多的是代码逻辑的问题。针对上述问题Java统一使用异常来处理这类问题。
初始异常
空指针异常
String s = null;s.indexOf("a");System.out.println("end");
由于变量”s”没有初始化就调用了方法s.indexOf("a"),导致了程序出现异常,屏幕输出了NullPointerException,当程序运行到s.indexOf("a"),Java虚拟机发现变量s没有初始化就调用了实例方法,这时候就启用了”异常处理机制”,首先创建NullPointerException对象,然后查看当前代码所在的方法没没有进行异常处理,如果没有就往上查找……直到主函数main,由于没有代码能够处理该异常,Java虚拟机启用了默认异常处理机制,打印异常栈到屏幕并退出程序,因此System.out.println("end")并没有执行。
数字格式异常
if(args.length < 1){print("请输入一个数字");}int num = Integer.parseInt(args[0]);System.out.println(num);
这段代码要求用户输入一个数值,比如”123”,然后将输入的字符转换为数值并输出,但如果用户输入的不是数值而是字符串”abc”,这时候就会出现转换异常,屏幕输出:NumberFormatException,我们查看对应代码发现它们通过trhow后面跟着一个异常对象进行了对应的处理,之前我们没看见trhow new NullPointerExceptoin可以认为是Java虚拟即抛出了该异常对象。
throw和return
我们知道return的作用是结束方法运行并返回对应的返回值,程序正常退出,而throw表示抛出一个异常,,这种情况会导致程序异常退出,对于return的位置是确定的,而throw后执行那行代码往往是不确定的,由异常处理机制决定,如果发生了溢出这时候Java虚拟机启用异常处理机制,首先查看当前代码所在方法有没有进行异常处理,如果没有就会依次向上查看……直到找到异常处理,否则Java虚拟机会启用默认异常处理机制,打印异常栈并退出程序。
异常类
前面我们接触过了NullPointerException和NumberFormatException,这两个类都有一个共同给的父类Throwable
Throwable
Throwable是所有异常基类的父类,有四个public修饰的构造方法。
public Throwable(){}public Throwable(String message){}public Throwable(Throwable cause){}public Throwable(Stirng message, Throwable cuase){}
在这四个构造方法中主要有两个参数:message表示异常消息,cause表示触发该异常的其它异常,异常可以形成异常链,一层一层的传递,可以通过异常对象.getCause获取上一层异常。
Throwable有一个静态方法initCause用于初始化异常,由于某些异常类的构造方法没有cause这个参数,可以通过initCause来初始化,但是initCause只能调用一次。
异常类体系
Throwable主要有两个子类Error和Exception。
Error表示系统级别的错误,子类有VirtualMachineError虚拟机错误以及子类OutOfMemoryError内存溢出和StackOverflowError堆栈溢出,Error表示不可调节的错误,由Java虚拟机自动触发不应该在逻辑代码中抛出。
Exception表示异常,主要子类有IOException SQLException RuntimeException。RuntimeException从名字上可能点误解,表示运行时异常。但实际上它表示未受检异常,Error以及Exception和它的子类表示受检异常。
受检异常与未受检异常的区别在于:受检异常必须要求开发人员进行处理,否则编译器会报错。而未受检不需要强制处理。
自定义异常
自定义异常一般是继承RuntimeException或子类或者Exception。继承RuntimeExcption或子类代表未受检异常;而继承Exception代表受检异常。
public class AppException extends Exception{public AppException(){super();}public AppExceptoin(String message){super(message);}public AppException(String message, Throwable cause){super(message, cause);}public AppException(Throwable cause){super(cause);}}
可以看出我们的自定义异常和其他异常类一样,并没有多余的属性和构造方法,在继承Exception之后自定义异常也只是多了几个构造方法,这些构造方法也只是调用了父类的构造方法。
异常处理
前面示例中我们代码抛出了异常,由于我们没有进行异常处理。导致了Java虚拟机启用了默认异常处理机制,即打印异常栈到标准错误流并退出程序,这对于用户是不友好的。因此需要我们进行异常处理,Java提供catch关键字用于捕获对应异常类。
catch匹配
try{Integer.parseInt("abc");String s = null;s.indexOf("a");}catch(NumberFormatException e){print("数值格式不正确");}catch(RuntimeException e){pirnt("runtimeException:"+e.getMessage);}catch(Exception e){print(”exception“);}System.out.println("end");
try块里面包含了可能发生异常的代码块,当发生异常时程序终止try块异常点后续代码的执行,然后执行与catch块中包含的异常类型逐个比较,如果类型相等则执行catch中的内容,由于我们进行了异常处理,因此Java虚拟机没有启用默认异常处理机制。所以打印”end”。
需要注意的是如果抛出异常类是catch块类的子类,那么也会匹配。因此,需要将详细的异常类放在顶层,自顶向下从详细的异常类到异常基类。
重新抛出异常
除了在catch块中处理异常,我们也可以在catch中重新抛出异常。
try{5 / 0;}catch(ArithmeticException e){throw new AppException("算术异常",e);}
为什么要抛出异常?
- 可能当前类处理不了该异常,因此抛出由上层处理
为什么要重新抛出异常
- 可能当前异常类不太合适,或者描述不够准确,如果感兴趣可以通过异常类
.getCause()或抛出该异常的原异常
- 可能当前异常类不太合适,或者描述不够准确,如果感兴趣可以通过异常类
finally
对于一些场景如数据库连接,IO。使用者在使用完之后一般需要释放连接。释放连接如果同时写在try和catch中可能比较麻烦,Java提供了finally关键字。
- 如果没有发生异常,那么在
try执行结束后执行finally。 - 如果发生异常且被捕获,那么执行
catch块后执行finally。 - 如果发生异常且没有被捕获,那么在异常抛出之前执行
fially。
关于finally还有一个小细节,如果try和catch中包含return,则return语句在finally语句执行结束之后执行。但fially不能改变返回值。
被finally控制的语句一定会执行,除非JVM退出。
int result = 0;try{return result;}finally{result = 2;}
最终函数返回结果为0,实际执行过程是:在执行return之前,会有一个临时变量存储result的值,然后执行finallyresult的值为2,然后返回临时变量的值0,result的结果为2。
public class Application{public static int num = 10;public static int test(){try{Application app = new Application();return app.num;}finally{app.num = 10;}}}
这里的结果是10。
不仅只有try-catch-finally还可以有try-finally、try-catch。如果finally中有return语句那么finally中的return将会覆盖掉try和catch中的return语句返回值。
try{return 10;}catch(Exception e){return 20;}finally{return 30;}
如果finally中有异常抛出那么将会覆盖掉其他异常。
try{5 / 0;}catch(Exceptoin e){throw new AppException("");}finally{throw new RuntimeException("");}
真是因为finally块中的return和异常抛出会覆盖掉try和catch中的声明,因此,作为一个良好的实践,应该避免在finally块中使用return或者throw。
try-with-resources
对于一些资源使用场景,经常在使用完之后需要释放连接,典型的写法是在finally中释放连接,针对这种情况Java7开始支持一种新语法try-with-resource,这种语法实现了java.lang.autoCloseable接口,该接口定义如下:
public interface AutoCloseable{void close() throws Exception;}
没有使用try-with-resource,使用定义如下:
public static void useResource(){AutoCloseable r = new FileInputStream("hello");try{}finally{r.close();}}
使用try-with-resource使用定义如下:
public static void useResource(){try(Autocloseable r = new FileInputStream("hello")){}}
资源声明和初始化都方法在try语句内,在语句执行完之后,会自动调用close()方法。资源可以定义外面但必须是或者形式上是final(虽然没有声明为final但也没有改变引用)。
throws
trhows用于一个方法在执行过程中可能会抛出的异常。
public static void test() throws AppException,IOException,NumberFormatException{}
throws写在方法声明后面,可以声明多个异常使用逗号隔开。这个声明的含义是这个方法在执行过程中可能会抛出的异常,且没有这些方法进行处理,调用者必须处理或者既继续向上抛出,最为一个良好的实践,应该使用注释中说明什么情况下会抛出异常。
对于未受检异常,可以不声明而抛出,而对于受检异常不可以抛出而不声明,否者编译器会报错。throws常用在
父类中,父类方法中可能没有定义需要抛出的异常,但子类重写后可能会抛出受检异常,但是同时父类又没有声明。因此,只能把需要抛出的异常都写在父类了。
如果一个方法调用了另一个声明抛出的受检方法,则必须处理这些异常,处理的方式可以是catch也可以是throws。
public void test() throws AppException{try{fn();}catch(SQLException e{e.printStackTrace();}}
对于fn()抛出的SQLException使用catch处理,而AppExcetion向上抛出。
未受检和受检异常
受检异常必须出现在throws里,调用者必须处理,Java编译器会强制要求这一点,而未受检异常则没有这个区别,为什么会有受检异常和未受检异常这个区分?我们自定义异常是使用受检异常还是未受检异常?未受检异常表示编程的逻辑错误,编程是应该避免这类问题,比如空指针异常,如果真出现了空指针异常,程序退出也是正常的,应该检查bug而不是想办法处理,对于受检异常表示程序本身没问题,但由于网络、IO、数据库等其他问题导致了异常,调用者应该适当处理。
其实编程错误也是应该处理的,尤其是Java广泛应用于服务端,不能因为一个逻辑错误而导致整个程序退出。Java受检异常和未受检异常的区分没有太大的意义,可以统一使用未受见异常来处理。这种观点的基本理由是:无论是受检异常还是未受检异常无论是否在throws中,都应该在合适的地方以适当的方式处理,而不是为了满足编译器的要求而盲目处理异常,既然都要处理异常,受检异常的强制声明就显得有点繁琐,尤其是在调用情况比较深的情况下。
如何使用异常
异常应该且仅用于异常情况
异常应该且仅用于异常情况,比如在循环迭代数组时候,应该检查索引是否有效。而不是等到抛出ArrayIndexOutOfBoundsException导致循环结束。对于一个引用变量,使用前应该检查其有效性,而不是让它抛出NPE。
真正出现异常情况的时候,应该抛出异常,而不是返回特殊值。比如String类的subString方法实现,如下:
public String subString(int beginIndex){if(beginIndex < 0){throw new StringIndexOutOfBoundsException(beginIndex);}int subLen = value.length - beginIndex;if(subLen < 0){throw new StringIndexOutOfBoundsException(subLen);}return beginIndex == 0 ? this : new Stirng(value, beginIndex, value);}
方法会判断beginIndex的有效性,如果无效会抛出StringIndexOutOfBoundsException异常。纯技术上的解决方法是不抛出异常而返回特殊值null,但beginIndex无效是异常情况,异常不能作为正常情况处理。
异常处理的目标
异常可以分为三个目标:用户,开发,第三发;对用户而言一般是输入错误;对程序而言一般是逻辑上的错误;而第三方泛指其他错误:如IO、数据库、第三方服务等。异常的处理可以分为报告和恢复;如果是输入错误对用户报告可以尽可能白话;如果是程序逻辑上的错误,可以联系客户或尝试重新连接。而对于开发或运维则需要详细的日志记录:异常链、异常栈等。恢复是指通过程序自动恢复。
异常处理的一般逻辑
- 如果知道如何处理异常,就自己处理
- 如果异常可以通过程序自动解决,就自动解决
- 如果知道异常如何解决,就不要向上报告而是解决异常
- 如果不能完全解决,应该向上报告
- 如果有额外的信息提供,就应该提供,可以以原异常做为
cause重新抛出一个异常。
在没有异常机制的情况下,判断方法是不是异常情况就是通过返回值,方法根据是否异常返回不同的值。而调用者需要通过返回值进行判断是否为异常情况,并进行相对应的处理,每一层都需要对调用的方法的不同的返回值进行判断和处理,程序的正常逻辑和异常逻辑混在一起,代码往往难以阅读和维护。
有了异常之后,程序正常的逻辑和异常逻辑相分离,异常情况可以集中处理。异常还可以向上传递,不需要每层方法都进行处理,异常也不再可能被忽略。
