1、背景介绍
为什么要进行异常处理?
对于计算机程序而言,没有人能保证它在运行时不会出错,出错来源主要有以下几个:
代码错误
用户非法输入
设备错误及物理限制:磁盘满了、内存溢出、硬件出现问题、网络中断……
程序出现错误,那么该如何解决呢?在Java语言中,它提供了异常处理机制来帮助我们处理这个问题。
异常机制可以使程序中的异常处理代码和正常业务代码分离开,保证程序代码更加优雅,并可以提高程序的健壮性。
Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字。
try关键字后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码;catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块;多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行;throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。
简单例子如下
try { // args表示传入的形参,args[0]表示传入的第一个形参,args[1]表示传入的第二个形参 int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[1]); int c = a / b; System.out.println("您输入的两个数相除的结果是:"+c); } catch (IndexOutOfBoundsException ie){ System.out.println("数组越界:运行程序时输入的参数个数不够"); } catch (NumberFormatException ne){ System.out.println("数字格式异常:程序只能接收整数参数"); } catch (ArithmeticException ae){ // 除0异常 System.out.println("算术异常"); } catch (Exception e){ System.out.println("未知异常"); }
2、知识剖析
2.1 异常处理机制
2.1.1 使用try...catch捕获异常
try{
// 业务实现代码
} catch (Exception e){
// 异常处理代码
}
执行流程(或者说逻辑):
如果try块里代码能够顺利运行,那就“一切正常”;如果执行try块里的业务逻辑代码时出现异常,系统会自动生成一个异常对象,该异常对象被提交给Java运行时环境 ,这个过程被称为抛出(throw)异常。当Java运行时环境收到异常对象后,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。
注:不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就此退出。
2.1.2 异常类的继承体系
当Java运行时环境接收到异常对象后,会一次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常,否则再次拿该异常对象和下一个catch块里的异常类进行比较。
Java异常捕获流程
从图中可以看出,try块后可以有多个catch块,这是为了针对不同的异常类提供不同的异常处理方式。当系统发生不同的意外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。
在通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行。除非在循环中使用了continue开始下一次循环,下一次循环又重新运行了try块,这才可能导致多个catch块被执行。
Java常见的异常类之间的继承关系
Java把所有的非正常情况分为两种:异常和错误,它们都继承Throwable父类。
Error错误,一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败、资源耗尽 等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象,也无须声明可能抛出Error及其任何子类对象。
常见异常:
IndexOutOfBoundsException:数组越界异常,原因在于运行程序时输入的参数个数不够
NumberFormatException:数字格式异常,原因在于运行程序时输入的参数不是数字,而是字母
ArithmeticException:除0异常
Exception:当发生未知异常时,该异常对象总是Exception类或其子类的实例,可用Exception对应的catch块处理该异常
NullPointerException:空指针异常,当试图调用一个null对象的实例或者实例变量时,会引发此异常
注:进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面,即先捕获小异常,再捕获大异常,否则将出现编译错误。(若父类在前,则排在它后面的子类的catch块将永远不会获得执行的机会,因为检索catch块是从上到下依次执行的)
2.1.3 Java7提供多异常捕获
在Java7之前,每个catch块只能捕获一种类型的异常,但从Java7开始,一个catch块可以捕获多种类型的异常。
使用一个catch块捕获多种类型的异常时需要注意如下两个地方:
捕获多种类型的异常时,多种异常类型之间用 | 隔开。
捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
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 e){ System.out.println("程序发生了数组越界、数字格式异常、算术异常之一"); // 捕获多异常时,异常变量默认有final修饰 // 所以下面代码编译报错 // e = new ArithmeticException("text"); } catch (Exception e){ System.out.println("未知错误"); // 捕获一种类型的异常时,异常变量没有final修饰 // 所有下面代码完全正确 e = new RuntimeException("test"); }
2.1.4 访问异常信息
如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch后面括号中的异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有的异常对象都包含了如下几个常用方法:
getMessage():返回该异常的详细描述字符串。
printStackTrace():将该异常的跟踪栈信息输出到标准错误流。
printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
getStackTrace():返回该异常的跟踪栈信息。
2.1.5 使用finally
有时,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显示回收。
注:Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。
为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块是否被执行,甚至在try块或者catch块中执行了return语句,finally块总会被执行。
完整的Java异常处理语法结构:
try{
// 业务实现代码
} catch (Exception1 e){
// 异常处理块1
} catch (Exception2 e){
// 异常处理块2
} finally {
// 资源回收块
}
异常处理语法结构中只有try块是必需的,如果没有try块,则不能有后面的catch块和finally块;catch块和finally块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;但不能只有try块,没有catch块,也没有finally块;多个catch块必须位于try块之后,finally块必须位于所有catch块之后。
注:除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。
在通常情况下,尽量避免在finally块中使用return或throw等导致方法终止的语句,否则可能出现一些很奇怪的情况。
2.1.6 异常处理的嵌套
在try块、catch块或finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套。
异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程既可放在try块里,也可放在catch块里,还可放在finally块里。
异常处理嵌套的深度没有很明显的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深一是没必要,而是导致程序可读性降低。
2.1.7 Java7的自动关闭资源的try语句
Java7增强了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显示关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。
注:为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法。(Closeable是AutoCloseable的子接口;Closeable接口里的close()方法声明抛出了IOException,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类;AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明抛出任何异常)
自动关闭资源的try语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没catch块,也没有finally块。如果程序需要,自动关闭资源的try语句后也可以带多个catch块和一个finally块。
Java7几乎把所有的“资源类”(包括文件的IO的各种类、JDBC编程的Connection、Statement等接口)进行了改写,改写后的资源类都实现了AutoCloseable或Closeable接口。
2.2 Checked异常和Runtime异常体系
Java的异常被分为两大类:Checked异常和Runtime异常(运行时异常,有的也称未检查异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。(在使用时要分辨是什么类型异常,只需看声明的异常类是啥就知道了)
只有Java语言提供了Checked异常,其他语言都没有提供Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显示处理Checked异常,如果程序没有处理Checked异常,该程序在编译时会发生错误,无法通过编译。
对于Checked异常的处理方式有以下两种:
当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常,然后再对应的catch块中修复该异常。
当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。
Runtime异常则更加灵活,Runtime异常无须显示声明抛出,如果程序需要捕获Runtime异常,也可以使用try...catch块来实现。
注:Java核心技术
在Exception层次结构中,有两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。划分这两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为未(不受?)检查异常(unchecked),所有其他的异常称为已检查异常(checked)。(在编译期间进行检查,编译器将检查是否为所有的已检查异常提供了异常处理器) ??????????? 有待讨论
2.3 使用throws抛出异常
使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是平时我们的程序在遇到异常后自动结束的原因。
throws声明抛出只能在方法签名中使用,throws可以声明多个异常类,多个异常类之间使用逗号隔开。
一旦使用throws语句声明抛出异常,那么就无需使用try...catch块来捕获该异常了。
使用throws声明抛出异常时有一个限制,就是方法重写时“两下”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。(即设置了上限)
使用Checked异常至少存在如下两大不便之处:
对于程序中的Checked异常,Java要求必须显示捕获并处理该异常,或者显示声明抛出该异常,这增加了编程复杂度。
如果在方法中显示声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。
在大部分时候推荐使用Runtime异常,而不使用Checked异常,尤其当程序需要自行抛出异常时,使用Runtime异常将更加简洁。
当使用Runtime异常时,程序无需在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。对于Runtime异常,Java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。
使用Runtime异常省事,但Checked异常也有其优势,Checked异常能在编译时提醒程序员代码可能存在的问题,提醒程序员必须注意处理该异常,或者声明该异常有方法的调用者来处理,从而避免程序员因为粗心而忘记处理该异常的错误。
2.4 使用throw抛出异常
当程序出现错误时,系统会自动抛出异常,除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成。
2.5 自定义异常类
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(即异常对象的getMessage()方法的返回值)。
package AuctionException; // 自定义异常都要继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。 // 自定义异常类需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串作为 // 该异常对象的描述信息(即异常对象的getMessage()方法的返回值) public class AuctionException extends Exception{ //1、无参数的构造器 public AuctionException(){ } //2、带一个字符串参数的构造器 public AuctionException(String msg){ // 通过super调用父类的构造器 super(msg); } }
2.6 catch和throw同时使用
在实际应用中,当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可以完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。
这种catch和throw结合使用使用的情况在大型企业级应用中非常常用,企业级应用对异常的处理通常分成两个部分:一是后台需要通过日志来记录异常发生的详细情况;二是应用还需要根据异常向应用使用者传达某种提示。
package CatchAndThrow; import AuctionException.AuctionException; public class CatchAndThrow { private double initPrice = 30.0; // 因为该方法中显示抛出了AuctionException异常 // 所以此处需要声明抛出AuctionException异常 public void bid(String bidPrice) throws AuctionException{ double d = 0.0; 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){ CatchAndThrow catchAndThrow = new CatchAndThrow(); try{ catchAndThrow.bid("df"); } catch (AuctionException ae){ // 再次捕获到bid()方法中的异常,并对该异常进行处理,此处是将异常的详细描述信息输出到标准错误(err)输出 System.err.println(ae.getMessage()); } } }
2.7 异常跟踪栈
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
3、常见问题
3.1 为什么在finally块中不能访问try块中声明的变量?
答:try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块及finally块中不能访问该变量。
3.2 为什么最好不要使用catch all语句?
答:所谓catch all语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。例如使用Exception或者Throwable类捕获所有异常,虽然这种方式能够处理异常,但是它不能精确描述引发异常的原因,我们很难进行准确的排查。
3.3 有没有关于注解方式的异常处理?
答:有的,比如spring有基于注解的全局异常处理方式(使用@ExceptionHandler)
4、参考文献
摘自《疯狂Java讲义 》第三版,文中代码模仿书中例子所敲。
今天的分享就到这了,希望大家多多指正,互相学习~
相关推荐:
Atas ialah kandungan terperinci 为什么要进行异常处理?详解java异常处理机制. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!