异常处理

概念

异常机制可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。

Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无须处理。

异常处理机制

Java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时额,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

使用try/catch捕获异常

下面是Java异常处理机制的语法结构

        try {
            //业务实现代码
        }catch (Exception e){
            //错误处理代码
        }

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。

当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此退出。

异常类的继承体系

当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常;否则再次拿该异常对象和下一个catch块里的异常类进行比较。

Java异常捕获流程示意图

在这里插入图片描述
当程序进入负责异常处理的catch块时,系统生成的异常对象ex将会传给catch后的异常处理,从而允许catch块通过该对象来获得异常的详细信息。

try块后可以有多个catch块,这是为了针对不同的异常类提供不同的异常处理方式。当系统发生不同的意外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。

Java把所有非正常情况分为两种:异常(Exception)和错误(Error),它们都继承Throwable父类。

Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

Java7提供的多异常捕获

在Java7以前,每个catch块只能捕获一种类型的异常;但从Java7开始,一个catch块可以捕获多种类型的异常。

使用一个catch块捕获多种类型的异常时需要注意如下两个地方

  • 捕获多种类型的异常时,多种异常类型之间用竖线 | 隔开

  • 捕获多种类型的异常时,异常变量有隐式地final修饰,因此程序不能对异常变量重新赋值

代码演示
public class MultiExceptionTest {

    public static void main(String[] args) {

        try {

            int a = Integer.parseInt(args[0]);
            int b = Integer.parseInt(args[1]);
            int c = a / b;
            System.out.println("您输入的两个数相除的结果是:" + c);
        }catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie){
            System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");

            //捕获多异常时,异常变量默认有final修饰,所以下面代码会报错
            //ie = new ArithmeticException("test");

        }catch (Exception e){
            System.out.println("未知异常");
            //捕获一种类型的异常时,异常类型没有final修饰
            e = new RuntimeException("test");
        }


    }

}
访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常参数来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。

所有的异常对象都包含了如下几个常用方法

返回该异常的详细描述字符串

    public String getMessage() {
        return detailMessage;
    }

将该异常的跟踪栈信息输出到标准错误输出

    public void printStackTrace() {
        printStackTrace(System.err);
    }

返回该异常的跟踪栈信息

    public StackTraceElement[] getStackTrace() {
        return getOurStackTrace().clone();
    }

代码演示

public class AccessExceptionMsg {

    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("a.txt");
        }catch (IOException ioe){
            System.out.println(ioe.getMessage());
            ioe.printStackTrace();
        }
    }

}

输出结果

a.txt (系统找不到指定的文件。)
java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at com.sunrise.eHealth.day0615.AccessExceptionMsg.main(AccessExceptionMsg.java:10)

上面程序调用了Exception对象的getMessage()方法来得到异常对象的详细信息,也使用了printStackTrace()方法来打印该异常的跟踪信息。

使用finally回收资源

有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源必须显式回收。

Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。

为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。

完整的Java异常处理语法结构如下

        try {
            //业务实现代码
            
        }catch (ex1){
            //异常处理块1
        }catch (ex2){
            //异常处理块2
        }
        ...
        finally {
            //资源回收块
        }

异常处理块语法结构中只有try块是必须的,也就是说,如果没有try块,则不能有后面的catch块和finally块;catch块和finally块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;但不能只有try块,既没有catch块,也没有finally块;多个catch块必须位于try块之后,finally块必须位于所有的catch块之后。

代码演示

public class FinallyTest {

    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("a.txt");
        }catch (IOException ioe){
            System.out.println(ioe.getMessage());
            //return语句强制方法返回
            return;
            //使用exit退出虚拟机
            //System.exit(1);
        }finally {
            //关闭资源文件,回收资源
            if (fis != null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("执行finally块里的资源回收!");

        }
    }

}

上面程序的try块后增加了finally块,用于回收在try块中打开的物理资源。注意程序的catch块中有一条return语句,该语句强制方法返回。在通常情况下,一旦在方法里执行到return语句的地方,程序将立即结束该方法;现在不会了,虽然return语句也强制方法结束,但一定会执行finally块里的代码。

输出结果

a.txt (系统找不到指定的文件。)
执行finally块里的资源回收!

将return语句注释掉,取消System.exit(1);代码注释,执行结果输出如下

a.txt (系统找不到指定的文件。)

上面执行结果表明finally块没有被执行。如果在异常处理代码中使用System.exit(1)语句来退出虚拟机,则finally块将失去执行的机会。

除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行

在通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,一旦在finally中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效。

代码演示

public class FinallyFlowTest {

    public static void main(String[] args) {
        boolean a = test();
        System.out.println(a);
    }

    public static boolean test(){
        try {
            //以为finally块中包含了return语句
            //所以下面的return语句失去作用
            return true;

        }finally {
            return false;
        }
    }

}

输出结果

false

当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里任何代码。

尽量避免在finally块里使用return或throw等导致方法终止的语句,否则可能出现一些很奇怪的情况。

异常处理的嵌套

异常处理流程代码可以放在任何能放可执行代码的地方,因此完整的异常处理流程既可放在try块里,也可放在catch块里,还可放在finally块里。

异常处理嵌套的深度没有明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。

Java7的自动关闭资源的try语句

在前面程序中看到,当程序使用finally块关闭资源时,程序显得异常臃肿。Java7增强了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显示关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。

需要指出的是,为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法。

代码演示

public class AutoCloseTest {

    public static void main(String[] args) throws Exception{
        try (
                //声明、初始化两个可关闭的资源
                //try语句会自动关闭这两个资源
                BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
                PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))

                ){
            //使用两个资源
            System.out.println(br.readLine());
            ps.println("庄生晓梦迷蝴蝶");

        }
    }

}

由于BufferedReader、PrintStream都实现了Closeable接口,而且它们放在try语句中声明、初始化,所以try语句会自动关闭它们。

自动关闭资源的try语句相当于包含了隐式地finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。

Java7几乎把所有的资源类(包括文件IO的各种类、JDBC编程的Connection、Statement等接口)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口

如果程序需要,自动关闭资源的try语句后也可以带多个catch块和一个finally块

Checked异常和Runtime异常体系

Java的异常被分为两大类:Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。

对于Checked异常的处理方式有如下两种。

  • 当前方法明确知道如何处理该异常,程序应该使用try/catch块来捕获该异常,然后在对应的catch块中修改该异常。
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常

Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try/catch块来实现。

只有Java语言提供了Checked异常,Checked异常体现了Java的严谨性,它要求程序员必须注意该异常,要么显式声明抛出,要么显式捕获并处理它。这是一种非常严谨的设计哲学,可以增加程序的健壮性。问题是:大部分的方法总是不能明确地知道如何处理异常,因此只能声明抛出该异常,而这种情况又是如此普遍,所以Checked异常降低了程序开发的生产率和代码的执行效率。关于Checked异常的优劣,在Java领域是一个备受争论的问题。

使用throws声明抛出异常

使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是程序在遇到异常后自动结束的原因。

throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开。

throws IOException,RuntimeException

如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中。

代码演示

public class ThrowsTest2 {
    
    public static void test() throws FileNotFoundException {
        
        //因为FileInputStream的构造器声明抛出IOException异常
        //所以调用FileInputStream的代码要么处于try/catch块中
        //要么处于另一个带throws声明抛出的方法中
        FileInputStream fis = new FileInputStream("a.txt");
    }


    public static void main(String[] args) throws FileNotFoundException {
        test();
    }
    
    
}

使用throws声明抛出异常时有一个限制:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

很简单,因为调用子类方法的地方都能调用父类(类的多态性),如果子类的抛出的异常要比父类多,则程序无法处理,因为此时代码中知道的是父类的声明异常,所以子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,才不会出现问题

使用throw抛出异常

当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成

抛出异常

异常是一种很“主观”的说法,以下雨为例,假设大家约好明天去爬山郊游,如果第二天下雨了,这种情况会打破既定计划,就属于一种异常;但对于正在期盼天降甘霖的农名而言,如果第二天下雨了,他们正好随雨追肥,这就完全正常。

很多时候,系统是否要抛出异常,可能需要根据应用的业务需求来决定,如果程序中的数据、执行与既定的业务需求不符,这就是一种异常。由于与业务需求不符而产生的异常,必须由程序员来决定抛出,系统无法抛出这种异常。

throw语句的语法格式
throw new RuntimeException();

不管是系统自行抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有任何差别

如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throws声明抛出的方法中;程序既可以显式使用try/catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。

代码演示

public class ThrowTest {

    public static void main(String[] args) {


        try {
            //调用声明抛出Checked异常的方法,要么显式捕获该异常
            //要么在main方法中再次声明抛出
            throwChecked(-3);
        } catch (Exception e) {
            e.printStackTrace();
        }


        //调用声明抛出RuntimeException异常的方法既可以显式捕获该异常
        //也可不理会该异常
        throwRuntime(3);
        
    }
    
    public static void throwChecked(int a) throws Exception {
        if (a > 0){
            //自行抛出Exception异常
            //该代码必须处于try块里,或处于带throws声明的方法中
            throw new Exception("a的值大于0,不符合要求");
        }
    }
    
    public static void throwRuntime(int a){
        if (a > 0){
            //自行抛出RuntimeException异常,既可以显式捕获该异常
            //也可完全不理会该异常,把该异常交给该方法调用者处理
            throw new RuntimeException("a的大于0,不符合要求");
        }
    }
    
    
}

通过上面程序可以看出,自行抛出Runtime异常比自行抛出Checked异常的灵活性更好。同样,抛出Checked异常则可以让编译器提醒程序员必须处理该异常。

自定义异常类

自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。

代码演示

public class AuctionException extends Exception {
    //无参数构造器
    public AuctionException(){}

    //带一个字符串参数的构造器
    public AuctionException(String msg){
        super(msg);
    }

}

super(msg);调用可以将此字符串参数传给异常对象的message属性,该message属性就是该异常对象的详细描述信息

如果需要自定义Runtime异常,只需要将AuctionException.java程序中的Exception基类改为RuntimeException基类,其它地方无须修改。

在大部分情况下,创建自定义异常都可采用与AuctionException.java相似的代码完成,只需改变AuctionException异常的类名即可,让该异常类的类名可以准确描述该异常

catch和throw同时使用

前面介绍的异常处理方式有如下两种

  1. 在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常
  2. 该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理

在实际应用中往往需要更复杂的处理方式,当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成。所以应该再次抛出异常,让该方法的调用者也能捕获到异常。

为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。

代码演示

public class AuctionTest {
    private double initPrice = 30.0;

    public void bid(String bidPrice) throws AuctionException {

        double d;
        try {
            d = Double.parseDouble(bidPrice);
        }catch (Exception e){
            //此处完成本方法中可以对异常执行的修复处理
            //此处仅仅是在控制台打印异常的跟踪信息
            e.printStackTrace();
            throw new AuctionException("竞拍价必须是整数值,不能包含其他字符");
        }

        if (initPrice > d){
            throw new AuctionException("竞拍价比起拍价低,不允许竞拍");
        }

        initPrice = d;

    }

    public static void main(String[] args) {
        AuctionTest at = new AuctionTest();

        try {
            at.bid("df");
        } catch (AuctionException e) {
            //再次捕获到bid()方法中的异常,并对该异常进行处理
            System.out.println(e.getMessage());
        }

    }



}

Java7增强的throw语句

public class ThrowTest2 {

    public static void main(String[] args) throws FileNotFoundException {
        try {
            new FileInputStream("a.txt");
        }catch (Exception ex){
            ex.printStackTrace();
            throw ex;
        }
    }

}

从Java7开始,Java编译器会执行更细致的检查,Java编译器会检查throw语句抛出异常的实际类型,这样编译器知道 throw ex; 实际上只可能抛出 FileNotFoundException异常,因此在main方法签名中只要声明抛出FileNotFoundException 即可。而在Java7之前main方法会声明Exception。

异常链

public class ThrowTest2 {

    public static void main(String[] args) throws Exception {
        try {
            new FileInputStream("a.txt");
        }catch (FileNotFoundException ex){
            ex.printStackTrace();
            //将异常重新封装起来抛出
            throw new Exception(ex);
        }
    }

}

从JDK1.4以后,Exceptoin基类已有了一个可以接收Throwable参数的构造器,所以上面程序可以将ex异常对象重新封装起来,从而实现对异常的链式处理

Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程

代码演示

public class PrintStackTraceTest {

    public static void main(String[] args) {
        firstMethod();
    }

    public static void firstMethod(){
        secondMethod();
    }

    public static void secondMethod(){
        thirdMethod();
    }
    public static void thirdMethod(){
        throw new SelfException("自定义异常信息");
    }

}

class SelfException extends RuntimeException{
    SelfException(){}

    SelfException(String msg){
        super(msg);
    }

}

输出结果

Exception in thread "main" com.sunrise.eHealth.day0615.SelfException: 自定义异常信息
	at com.sunrise.eHealth.day0615.PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:17)
	at com.sunrise.eHealth.day0615.PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:14)
	at com.sunrise.eHealth.day0615.PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:10)
	at com.sunrise.eHealth.day0615.PrintStackTraceTest.main(PrintStackTraceTest.java:6)

从上面程序可以看出,异常从thirdMethod方法开始触发,传到secondMethod方法,再传到firstMethod方法,最后传到main方法,在main方法终止,这个过程就是Java的异常跟踪栈。

面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者…直至最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。

多线程中发生异常的情形

public class ThreadExceptionTest implements Runnable {
    @Override
    public void run() {
        firstMethod();
    }

    public void firstMethod(){
        secondMethod();
    }
    public void secondMethod(){
        int a = 5;
        int b = 0;
        int c = a / b;
    }


    public static void main(String[] args) {
        new Thread(new ThreadExceptionTest()).start();
    }


}
异常信息
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
	at com.sunrise.eHealth.day0615.ThreadExceptionTest.secondMethod(ThreadExceptionTest.java:15)
	at com.sunrise.eHealth.day0615.ThreadExceptionTest.firstMethod(ThreadExceptionTest.java:10)
	at com.sunrise.eHealth.day0615.ThreadExceptionTest.run(ThreadExceptionTest.java:6)
	at java.lang.Thread.run(Thread.java:748)

由上面程序可以看出系统默认处理异常的方式是调用Throwable类的printStackTrace()方法打印该异常的跟踪栈信息

虽然printStackTrace()方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它;而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来

异常处理规则

成功地异常处理应该实现如下4个目标

  1. 使程序代码混乱最小化
  2. 捕获并保留诊断信息
  3. 通知合适的人员
  4. 采用合适的方式结束异常活动
不要过度使用异常

过度使用异常主要有两个方面

  1. 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理
  2. 使用异常处理来代替流程控制

对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性;对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有外部的、不能确定和预知的运行时错误才使用异常。

必须指出:异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值