Java基础篇笔记(五):Java中的异常处理机制

一、引入异常

异常机制已经成为判断一门编程语言是否成熟的标准,异常机制可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。
Java的异常机制主要依赖于try、catch、finally、throw、throws五个关键字,其中try关键字后紧跟一个花括号括起来的代码块(花括号不可以省略),简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源。 throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。

二、异常处理机制

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

try{
    //业务实现代码
    ...
}
catch(Exception e){
    alert 输入不合法
    goto retry
}

如果执行try块的业务逻辑代码时出现异常,系统将自动生成一个异常对象,该异常悐被提交给Java运行时环境,这个过程被称为抛出(throw)异常。当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

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

当Java运行时环境接收到异常对象时,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常;否则再次拿该异常对象和下一个catch块里的异常类进行比较。当程序进入负责处理的catch块时,系统生成的异常对象ex将会传给catch块后的异常形参,从而允许catch块通过该对象来获得异常的详细信息(当进入catch块后就不会再向下执行了)。try块后可以有多个catch块,这是为了针对不同的异常类提供不同的异常处理方式。当系统发生不同的以外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。

在通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,不可能有多个catch块被执行。除非在循环中使用了continue开始下一次循环,下一次循环重写运行了try块,这才导致多个catch块被执行。try块里声明的变量时代码块内局部变量,它只在try块内有效,在catch块中不能访问该变量。

三、异常与错误

Java把所有的非正常的情况分成两种:异常(Exception)和错误(Error),它们都继承Throwable父类。Error错误一般指虚拟机相关的问题,如系统奔溃、虚拟机错误、动态链接失败等,这些错误无法恢复或不可能捕获,将导致程序中断。通常应用程序无法处理这些错误,因此程序不应该试图用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

四、异常的捕获与访问

程序应该总是把Exception类的catch块放在最后,因为捕捉异常是按照顺序进行的,如果把Exception类的catch块放在最前,Java运行时将直接进入该catch块(因为所有的异常对象都是Exception或其子类的实例),排在他后面的catch块将永远不会获得执行机会。实际上,所有的父类异常的catch块都应该排在子类异常catch块的后面(即先处理小异常,再处理大异常)。
Java7开始,一个catch块可以捕获多种类型的异常,使用一个catch块捕获多种类型的异常时需要注意:多种异常类型之间用竖线(丨)隔开;异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。

public class MultiExceptionTest {
    public static void main(String[] args){
        try{
            //可以直接定义a,b为常量
            int a=Integer.parseInt(args[0]);
            int b=Integer.parseInt(args[1]);
            int c=a/b;
            System.out.println("两个数相除的结果是:"+c);
        }
        catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException e){
            System.out.println("发生数组越界、数字格式异常、算术异常之一");
            //捕获多异常时,异常变量默认有final修饰,以下代码出错
            e=new ArithmeticException("test")
        }
        catch (Exception e){
            System.out.println("未知异常");
            //捕获一种类型的异常时,异常变量没有final修饰,以下代码正确
            e=new RuntimeException("test");
        }
    }
}

如果程序需要在catch块访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有的异常对象都包含了一下几个常用方法:

getMessage() 返回该异常的详细描述字符串。
printStackTrace() 将该异常的跟踪栈信息输出到标准错误输出。
printStackTrace(PrintStream s) 将该异常的跟踪栈信息输出到指定输出流。
getStackTrace() 返回该异常的跟踪栈信息。

下面的程序实例了如何访问异常信息:

public class AccessExceptionMsg {
    public static void main(String[] args){
        try{
            FileInputStream fis=new FileInputStream("QAQ.txt");
        }
        catch(IOException ie){
            //获得异常对象的详细信息
            System.out.println(ie.getMessage());
            //使用printStackTrace()方法来打印该异常的跟踪信息
            ie.printStackTrace();
        }
    }
}
输出结果:
QAQ.txt (系统找不到指定的文件。)
java.io.FileNotFoundException: QAQ.txt (系统找不到指定的文件。)
	at java.base/java.io.FileInputStream.open0(Native Method)
	at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
	at AccessExceptionMsg.main(AccessExceptionMsg.java:7)

可以看到,“QAQ.txt(系统找不到指定的文件。)”,为getMessa()方法返回的字符串,下面更详细的时该异常的跟踪栈信息。

五、对于finally

有时候,程序在try块里打开了一些物理资源(例如数据库连接,网络连接和磁盘文件等),这些物理资源就必须显示回收。为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块,即完整的Java异常处理语法结构如下:

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

异常处理语法结构中,只有try块是必须的,如果没有try块,就不可能有后面的catch块和finally块;catch块和finally块都是可选的,但是必须出现之一,也可以同时出现。finally块必须位于所有catch块后面。

一般情况下不要在finally块中使用return或throw等导致方法终止的语句,一旦使用,将会导致try块、catch块中的return、throw语句失效,或者一些很奇怪的现象。

使用finally块关闭资源时,程序显得有些臃肿,Java7改变了这种局面,它允许在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("D:\\复习QAQ\\src\\AutoCloseTest.java"));
                PrintStream ps=new PrintStream(new FileOutputStream("a.txt"));
                )
        {
            System.out.println(br.readLine());
            ps.println("能以精诚致魂魄");
        }
    }
}

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

finally块不是总会执行的
1.在前面的代码块中return导致执行不到try块,那么finally就不会被执行。
2.在前面的代码块中调用了.exit()等退出虚拟机的方法。

六、Checked异常和Runtime异常体系

Java的异常被分为两大类,即Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。只有Java提供了Checked异常,因为它认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显示处理Checked异常。如果没有处理,该程序编译时就会发生错误,无法通过编译。
对于Checked异常的处理方式有如下两种:

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

而对于Runtime异常,它无须显示声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。

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

public class ThrowsTest {
    public static void main(String[] args) throws IOException{
    //只声明不处理,将会打印该异常的跟踪栈信息,并结束程序。
        FileInputStream fis=new FileInputStream("qwq.txt");
    }
}
输出结果:
Exception in thread "main" java.io.FileNotFoundException: qwq.txt (系统找不到指定的文件。)
	at java.base/java.io.FileInputStream.open0(Native Method)
	at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
	at ThrowsTest.main(ThrowsTest.java:6)

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

使用throw抛出异常
当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成。throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。语法格式如下:

throw ExceptionInstance;

当Java运行时接收到开发者自行抛出的异常时,同样会中止当前的执行流,调到该异常对应的catch块,由该catch块来处理异常。也就是说,不管是系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有差别。
如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里,显示捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throws声明抛出的方法中,程序既可以显示的使用try…catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。
在实际应用中,往往需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完全处理该异常,必须有几个方法写作才可完全处理该异常。即在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。
这种catch和throw结合使用的情况在大型企业级应用非常常用。企业级应用对异常的处理通常分成两个部分:1.应用后台需要通过日志来记录异常发生的详细情况;2.应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。

综上,使用Checked异常至少存在两个不便之处:

  • 对于程序中的Checked异常,Java要求必须显示捕获并处理该异常,或者显式声明抛出异常。这样增加了编程的复杂度。
  • 如果在方法中显式声明抛出了Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。

在大部分时候推荐使用Runtime异常,这样会更加简洁,并且这样程序无须在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。
使用Runtime异常是比较省事的方式,使用这种方式既可以享受“正常代码和错误处理代码分离”,“保证程序具有较好的健壮性”的优势,又可以避免因为使用Checked一场带来的编程烦琐性。但是使用Checked异常能在编译时提醒程序员代码可能存在的问题,提醒程序员必须注意处理该异常,或者声明该异常由该方法调用者来处理,从而避免程序员因为粗心而忘记处理该异常的错误。

七、异常处理规则

成功的异常处理应该试下如下四个目标:

  • 使程序代码混乱最小化。
  • 捕获并保留诊断信息。
  • 通知合适的人员。
  • 采用合适的方式结束异常活动。

滥用异常机制也会带来一些负面影响:

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

异常处理的初衷时将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。并且要指出的是,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。即对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。
不要忽略捕获到的异常,用catch块做一些有用的事情——处理并修复这个错误:

  • **处理异常。**对异常进行合适的修复,然后绕过异常发生的地方继续执行;或者用别的数据进行计算,以代替期望的方法返回值…等等,总之对于Checked异常,程序应该尽量修复
  • **重新抛出新异常。**把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
  • **在合适的层处理异常。**如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层的调用者来负责处理该异常。

猜你喜欢

转载自blog.csdn.net/laobanhuanghe/article/details/98311130