异常
在实际代码运行过程中可能会出现很多奇奇怪怪的错误,有的是不可控的内部原因:堆栈溢出、磁盘空间不足;有的是不可控的外部原因:比如网络连接故障,更多的是代码逻辑的问题。针对上述问题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
的值,然后执行finally
result的值为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
重新抛出一个异常。
在没有异常机制的情况下,判断方法是不是异常情况就是通过返回值,方法根据是否异常返回不同的值。而调用者需要通过返回值进行判断是否为异常情况,并进行相对应的处理,每一层都需要对调用的方法的不同的返回值进行判断和处理,程序的正常逻辑和异常逻辑混在一起,代码往往难以阅读和维护。
有了异常之后,程序正常的逻辑和异常逻辑相分离,异常情况可以集中处理。异常还可以向上传递,不需要每层方法都进行处理,异常也不再可能被忽略。