深刻理解Java异常

引言

说到异常,你们脑海中第一反应确定是try-catch-finally这样的固定的组合。的确,这是Java异常处理的基本范式,下面咱们就来好好聊聊Java异常机制,看看这个背后还有哪些咱们忽略的细节。java

Java异常介绍

异常时什么?就是指阻止当前方法或做用域继续执行的问题,当程序运行时出现异常时,系统就会自动生成一个Exception对象来通知程序进行相应的处理。Java异常的类型有不少种,下面咱们就使用一张图来看一下Java异常的继承层次结构:数组

图片中咱们看到Java异常的基类是Throwable类型,而后它有两个派生类Error和Exception类型,而后Exception类有分为受检查异常及RuntimeException(运行时异常)。下面咱们就来逐一介绍。

Java异常中的Error

Error通常表示编译时或者系统错误,例如:虚拟机相关的错误,系统崩溃(例如:咱们开发中有时会遇到的OutOfMemoryError)等。这种错误没法恢复或不可捕获,将致使应用程序中断,一般应用程序没法处理这些错误,所以也不该该试图用catch来进行捕获。安全

Java异常中的Exception

上面咱们有介绍,Java异常的中的Exception分为受检查异常和运行时异常(不受检查异常)。下面咱们展开介绍。bash

Java中的受检查异常

相信你们在写IO操做的代码的时候,必定有过这样的记忆,对File或者Stream进行操做的时候必定须要使用try-catch包起来,不然编译会失败,这是由于这些异常类型是受检查的异常类型。编译器在编译时,对于受检异常必须进行try...catch或throws处理,不然没法经过编译。常见的受检查异常包括:IO操做、ClassNotFoundException、线程操做等。网络

Java中的非受检查异常(运行时异常)

RuntimeException及其子类都统称为非受检查异常,例如:NullPointExecrption、NumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。ide

Java的异常处理

Java处理异常的通常格式是这样的:函数

try{
    ///可能会抛出异常的代码
}catch(Type1 id1){
    //处理Type1类型异常的代码
}catch(Type2 id2){
    //处理Type2类型异常的代码
}
复制代码

try块中放置可能会发生异常的代码(可是咱们不知道具体会发生哪一种异常)。若是异常发生了,try块抛出系统自动生成的异常对象,而后异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,而后进行catch语句执行(不会在向下查找)。若是咱们的catch语句没有匹配到,那么JVM虚拟机仍是会抛出异常的。ui

Java中的throws关键字

若是在当前方法不知道该如何处理该异常时,则可使用throws对异常进行抛出给调用者处理或者交给JVM。JVM对异常的处理方式是:打印异常的跟踪栈信息并终止程序运行。 throws在使用时应处于方法签名以后使用,能够抛出多种异常并用英文字符逗号’,’隔开。下面是一个例子:spa

public void f() throws ClassNotFoundException,IOException{}
复制代码

这样咱们调用f()方法的时候必需要catch-ClassNotFoundException和IOException这两个异常或者catch-Exception基类。
注意:
throws的这种使用方式只是Java编译期要求咱们这样作的,咱们彻底能够只在方法声明中throws相关异常,可是在方法里面却不抛出任何异常,这样也能经过编译,咱们经过这种方式间接的绕过了Java编译期的检查。这种方式有一个好处:为异常先占一个位置,之后就能够抛出这种异常而不须要修改已有的代码。在定义抽象类和接口的时候这种设计很重要,这样派生类或者接口实现就能够抛出这些预先声明的异常。线程

打印异常信息

异常类的基类Exception中提供了一组方法用来获取异常的一些信息.因此若是咱们得到了一个异常对象,那么咱们就能够打印出一些有用的信息,最经常使用的就是void printStackTrace()这个方法,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧.元素0是栈顶元素,而且是调用序列中的最后一个方法调用(这个异常被建立和抛出之处);他有几个不一样的重载版本,能够将信息输出到不一样的流中去.下面的代码显示了如何打印基本的异常信息:

public void f() throws IOException{
    System.out.println("Throws SimpleException from f()"); 
    throw new IOException("Crash");
 }
 public static void main(String[] agrs) {
    try {
    	new B().f();
    } catch (IOException e) {
    	System.out.println("Caught Exception");
        System.out.println("getMessage(): "+e.getMessage());
        System.out.println("getLocalizedMessage(): "+e.getLocalizedMessage());
        System.out.println("toString(): "+e.toString());
        System.out.println("printStackTrace(): ");
        e.printStackTrace(System.out);
    }
}
复制代码

咱们来看输出:

Throws SimpleException from f()
Caught  Exception
getMessage(): Crash
getLocalizedMessage(): Crash
toString(): java.io.IOException: Crash
printStackTrace(): 
java.io.IOException: Crash
	at com.learn.example.B.f(RunMain.java:19)
	at com.learn.example.RunMain.main(RunMain.java:26)
复制代码

使用finally进行清理

引入finally语句的缘由是咱们但愿一些代码老是能获得执行,不管try块中是否抛出异常.这样异常处理的基本格式变成了下面这样:

try{
    //可能会抛出异常的代码
}
catch(Type1 id1){
    //处理Type1类型异常的代码
}
catch(Type2 id2){
    //处理Type2类型异常的代码
}
finally{
    //老是会执行的代码
}
复制代码

在Java中但愿除内存之外的资源恢复到它们的初始状态的时候须要使用的finally语句。例如打开的文件或者网络链接,屏幕上的绘制的图像等。下面咱们来看一下案例:

public class FinallyException {
    static int count = 0;

    public static void main(String[] args) {
        while (true){
            try {
                if (count++ == 0){
                    throw new ThreeException();
                }
                System.out.println("no Exception");
            }catch (ThreeException e){
                System.out.println("ThreeException");
            }finally {
                System.out.println("in finally cause");
                if(count == 2)
                    break;
            }
        }
    }
}

class ThreeException extends Exception{}
复制代码

咱们来看输出:

ThreeException
in finally cause
no Exception
in finally cause
复制代码

若是咱们在try块或者catch块里面有return语句的话,那么finally语句还会执行吗?咱们看下面的例子:

public class MultipleReturns {
    public static void f(int i){
        System.out.println("start.......");
        try {
            System.out.println("1");
            if(i == 1)
                return;
            System.out.println("2");
            if (i == 2)
                return;
            System.out.println("3");
            if(i == 3)
                return;
            System.out.println("else");
            return;
        }finally {
            System.out.println("end");
        }
    }

    public static void main(String[] args) {
        for (int i = 1; i<4; i++){
            f(i);
        }
    }
}
复制代码

咱们来看运行结果:

start.......
1
end
start.......
1
2
end
start.......
1
2
3
end
复制代码

咱们看到即便咱们在try或者catch块中使用了return语句,finally子句仍是会执行。那么有什么状况finally子句不会执行呢?
有下面两种状况会致使Java异常的丢失

  • finally中重写抛出异常(finally中重写抛出另外一种异常会覆盖原来捕捉到的异常)
  • 在finally子句中返回(即return)

Java异常栈

前面稍微提到了点Java异常栈的相关内容,这一节咱们经过一个简单的例子来更加直观的了解异常栈的相关内容。咱们再看Exception异常的时候会发现,发生异常的方法会在最上层,main方法会在最下层,中间还有其余的调用层次。这实际上是栈的结构,先进后出的。下面咱们经过例子来看下:

public class WhoCalled {
    static void f() {
        try {
            throw new Exception();
        } catch (Exception e) {
            for (StackTraceElement ste : e.getStackTrace()){
                System.out.println(ste.getMethodName());
            }
        }
    }

    static void g(){
        f();
    }

    static void h(){
        g();
    }

    public static void main(String[] args) {
        f();
        System.out.println("---------------------------");
        g();
        System.out.println("---------------------------");
        h();
        System.out.println("---------------------------");

    }
}
复制代码

咱们来看输出结果:

f
main
---------------------------
f
g
main
---------------------------
f
g
h
main
---------------------------
复制代码

能够看到异常信息都是从内到外的,按个人理解查看异常的时候要从第一条异常信息看起,由于那是异常发生的源头。

从新抛出异常及异常链

咱们知道每遇到一个异常信息,咱们都须要进行try…catch,一个还好,若是出现多个异常呢?分类处理确定会比较麻烦,那就一个Exception解决全部的异常吧。这样确实是能够,可是这样处理势必会致使后面的维护难度增长。最好的办法就是将这些异常信息封装,而后捕获咱们的封装类便可。
咱们有两种方式处理异常,一是throws抛出交给上级处理,二是try…catch作具体处理。可是这个与上面有什么关联呢?try…catch的catch块咱们能够不须要作任何处理,仅仅只用throw这个关键字将咱们封装异常信息主动抛出来。而后在经过关键字throws继续抛出该方法异常。它的上层也能够作这样的处理,以此类推就会产生一条由异常构成的异常链。
经过使用异常链,咱们能够提升代码的可理解性、系统的可维护性和友好性。
咱们捕获异常之后通常会有两种操做

  • 捕获后抛出原来的异常,但愿保留最新的异常抛出点--fillStackTrace
  • 捕获后抛出新的异常,但愿抛出完整的异常链--initCause

捕获异常后从新抛出异常

在函数中捕获了异常,在catch模块中不作进一步的处理,而是向上一级进行传递 catch(Exception e){ throw e;},咱们经过例子来看一下:

public class ReThrow {
    public static void f()throws Exception{
        throw new Exception("Exception: f()");
    }

    public static void g() throws Exception{
        try{
            f();
        }catch(Exception e){
            System.out.println("inside g()");
            throw e;
        }
    }
    public static void main(String[] args){
        try{
            g();
        }
        catch(Exception e){
            System.out.println("inside main()");
            e.printStackTrace(System.out);
        }
    }
}
复制代码

咱们来看输出:

inside g()
inside main()
java.lang.Exception: Exception: f()
        //异常的抛出点仍是最初抛出异常的函数f()
	at com.learn.example.ReThrow.f(RunMain.java:5)
	at com.learn.example.ReThrow.g(RunMain.java:10)
	at com.learn.example.RunMain.main(RunMain.java:21)
复制代码

fillStackTrace——覆盖前边的异常抛出点(获取最新的异常抛出点)

在此抛出异常的时候进行设置 catch(Exception e){ (Exception)e.fillInStackTrace();} 咱们经过例子看一下:(仍是刚才的例子)

public void g() throws Exception{
    try{
        f();
    }catch(Exception e){
    	System.out.println("inside g()");
        throw (Exception)e.fillInStackTrace();
    }
}
复制代码

运行结果以下:

inside g()
inside main()
java.lang.Exception: Exception: f()
        //显示的就是最新的抛出点
	at com.learn.example.ReThrow.g(RunMain.java:13)
	at com.learn.example.RunMain.main(RunMain.java:21)
复制代码

捕获异常后抛出新的异常(保留原来的异常信息,区别于捕获异常以后从新抛出)

若是咱们在抛出异常的时候须要保留原来的异常信息,那么有两种方式

  • 方式1:Exception e=new Exception(); e.initCause(ex);
  • 方式2:Exception e =new Exception(ex);
class ReThrow {
    public void f(){
        try{
             g(); 
         }catch(NullPointerException ex){
             //方式1
             Exception e=new Exception();
             //将原始的异常信息保留下来
             e.initCause(ex);
             //方式2
             //Exception e=new Exception(ex);
             try {
    		    throw e;
    		} catch (Exception e1) {
    		    e1.printStackTrace();
    		}
         }
    }

    public void g() throws NullPointerException{
    	System.out.println("inside g()");
        throw new NullPointerException();
    }
}

public class RunMain {
    public static void main(String[] agrs) {
    	try{
            new ReThrow().f();
        }
        catch(Exception e){
            System.out.println("inside main()");
            e.printStackTrace(System.out);
        }
    }
}
复制代码

在这个例子里面,咱们先捕获NullPointerException异常,而后在抛出Exception异常,这时候若是咱们不使用initCause方法将原始异常(NullPointerException)保存下来的话,就会丢失NullPointerException。只会显示Eception异常。下面咱们来看结果:

//没有调用initCause方法的输出
inside g()
java.lang.Exception
	at com.learn.example.ReThrow.f(RunMain.java:9)
	at com.learn.example.RunMain.main(RunMain.java:31)
//调用initCasue方法保存原始异常信息的输出
inside g()
java.lang.Exception
	at com.learn.example.ReThrow.f(RunMain.java:9)
	at com.learn.example.RunMain.main(RunMain.java:31)
Caused by: java.lang.NullPointerException
	at com.learn.example.ReThrow.g(RunMain.java:24)
	at com.learn.example.ReThrow.f(RunMain.java:6)
	... 1 more
复制代码

咱们看到咱们使用initCause方法保存后,原始的异常信息会以Caused by的形式输出。

Java异常的限制

当Java异常遇到继承或者接口的时候是存在限制的,下面咱们来看看有哪些限制。

  • 规则一:子类在重写父类抛出异常的方法时,要么不抛出异常,要么抛出与父类方法相同的异常或该异常的子类。若是被重写的父类方法只抛出受检异常,则子类重写的方法能够抛出非受检异常。例如,父类方法抛出了一个受检异常IOException,重写该方法时不能抛出Exception,对于受检异常而言,只能抛出IOException及其子类异常,也能够抛出非受检异常。 咱们经过例子来看下:
class A {  
    public void fun() throws Exception {}  
}  
class B extends A {  
    public void fun() throws IOException, RuntimeException {}  
}
复制代码

父类抛出的异常包含全部异常,上面的写法正确。

class A {  
    public void fun() throws RuntimeException {}  
}  
class B extends A {  
    public void fun() throws IOException, RuntimeException {}  
}
复制代码

子类IOException超出了父类的异常范畴,上面的写法错误。

class A {  
    public void fun() throws IOException {}  
}  
class B extends A {  
    public void fun() throws IOException, RuntimeException, ArithmeticException{}
}
复制代码

RuntimeException不属于IO的范畴,而且超出了父类的异常范畴。可是RuntimeException和ArithmeticException属于运行时异常,子类重写的方法能够抛出任何运行时异常。因此上面的写法正确。

  • 规则儿:子类在重写父类抛出异常的方法时,若是实现了有相同方法签名的接口且接口中的该方法也有异常声明,则子类重写的方法要么不抛出异常,要么抛出父类中被重写方法声明异常与接口中被实现方法声明异常的交集。
class Test {
    public Test() throws IOException {}
    void test() throws IOException {}
}

interface I1{
    void test() throw Exception;
}

class SubTest extends Test implements I1 {
    public SubTest() throws Exception,NullPointerException, NoSuchMethodException {}
    void test() throws IOException {}
}
复制代码

在SubTest类中,test方法要么不抛出异常,要么抛出IOException或其子类(例如,InterruptedIOException)。

Java异常与构造器

若是一个构造器中就发生异常了,那咱们如何处理才能正确的清呢?也许你会说使用finally啊,它不是必定会执行的吗?这可不必定,若是构造器在其执行过程当中遇到了异常,这时候对象的某些部分尚未正确的初始化,而这时候却会在finally中对其进行清理,显然这样会出问题的。
原则:
对于在构造器阶段可能会抛出异常,而且要求清理的类,最安全的方式是使用嵌套的try子句。

try {
    InputFile in=new InpputFile("Cleanup.java");
    try {
    	String string;
    	int i=1;
    	while ((string=in.getLine())!=null) {}
    }catch (Exception e) {
    	System.out.println("Cause Exception in main");
    	e.printStackTrace(System.out);
    }finally {
    	in.dispose();
    }
}catch (Exception e) {
    System.out.println("InputFile construction failed");
}
复制代码

咱们来仔细看一下这里面的逻辑,对InputFile的构造在第一个try块中是有效的,若是构造器失败,抛出异常,那么会被最外层的catch捕获到,这时候InputFile对象的dispose方法是不须要执行的。若是构形成功,那么进入第二层try块,这时候finally块确定是须要被调用的(对象须要dispose)。

异常的使用指南(下列状况下使用异常)

  • 在恰当的级别处理异常(在知道如何处理的状况下才捕获异常)
  • 努力解决问题而且从新调用产生异常的方法
  • 进行少量修补,而后绕过异常的地方从新执行
  • 把当前运行环境下能作的事情尽可能作完,而后把相同的异常重抛到更高层
  • 把当前运行环境下能作的事情尽可能作完,而后把不相同的异常重抛到更高层
  • 努力让类库和程序更安全
相关文章
相关标签/搜索