Java——深入理解Java异常体系

该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。

前言:

Java的基本理念是“结构不佳的代码不能运行”。

“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理;你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在这里将作出正确的决定。

使用异常所带来的另一个相当明显的好处是,它往往能够降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。

异常概述:

现在我们需要编写一个五子棋程序,当用户输入下期坐标时,程序要判断用户输入是否合法,如果保证程序有较好的容错性,将会有如下的代码(伪代码):

if(用户输入包含除逗号之外的其他非数字字符)
{
alert 坐标只能是数值
goto retry
}
else if(用户输入不包含逗号) {
alert 应使用逗号分隔两个坐标值
goto retry
} else if(用户输入坐标值超出了有效范围) {
alert 用户输入坐标应位于棋盘坐标之内
goto retry
} else if(用户输入的坐标已有棋子)
{
alert 只能在没有棋子的地方下棋
goto retry
}
else {
.....
}

  

上面代码还未涉及任何有效处理,只是考虑了4种可能的错误,代码就已经急剧增加了。但实际上,上面考虑的4种情形还远未考虑到所有的可能情形(事实上,世界上的意外是不可穷举的),程序可能发生的异常情况总是大于程序员所能考虑的意外情况。

对于上面的错误处理机制,主要有以下两个缺点:

  • 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
  • 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。

我们希望有这样一种处理机制:

if(用户输入的数据不合法){
.....
}else{
处理逻辑
.....
}

  

上面伪码提供了一个非常强大的“if块”——程序不管输入错误的原因是什么,只要用户输入不满足要求,程序就一次处理所有的错误。这种处理方法的好处是,使得错误处理代码变得更有条理,只需在一个地方处理错误。

这就需要用到java异常了。

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException的异常。

异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

Java中的异常有以下三种类型:

检查异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

运行异常:运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。

错误:错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

异常的体系结构:

我们先来统观以下Java的异常体系结构图:

Java——深入理解Java异常体系

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

只有Java语言提供了Checked异常,其他语言都没有提供Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。

Throwable

Java异常的*类,所有的类都继承自Throwable

Error:

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

Exception:

Exception中异常主要分为两大类,运行时异常和检查异常。常见的运行时异常有ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、ClassNotFoundException(类型找不到),这些异常时非检查异常,程序可以选择处理,也可以不处理,编译器编译时期并不会报错。这些异常一般是由于程序逻辑错误引起的,所以建议程序员还是处理一下。除运行时异常外的所有异常我们都称为非运行时异常,也是必须处理的异常,如果不出来,程序编译会报错。

Error和Exception的区别:

ErrorException的区别:Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。

检查异常:必须处理的异常

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过。

非检查异常:可以不处理

包括RuntimeException及其子类和Error

不受检查异常为编译器不要求强制处理的异常,检查异常则是编译器要求必须处置的异常。

异常处理机制:

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

java异常关键字:

  • try – 用于监听。try后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码,当try语句块内发生异常时,异常就被抛出。【监控区域】
  • catch – 用于捕获异常。catch后对应异常类型和一个代码块,用于处理try块发生对应类型的异常。【异常处理程序】
  • finally – 用于清理资源,finally语句块总是会被执行。 多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。【使用finally进行清理】
  • throw – 用于抛出一个实际的异常。throw可以单独作为语句使用,抛出一个具体的异常对象。【抛出异常】
  • throws --用在方法签名中,用于声明该方法可能抛出的异常。【异常说明】

1、使用try...catch捕获异常:

语法格式:

try{
//业务实现代码
...
}catch(Exception e){
//异常处理代码
...
}

  

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

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

下面看几个简单的异常捕获的例子:

例1:

public class DivTest {

	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 e) {
System.out.println("数组越界,运行时参数不够");
}catch(NumberFormatException e) {
System.out.println("数字格式异常");
}catch(ArithmeticException e) {
System.out.println("算术异常");
}catch(Exception e) {
System.out.println("未知异常");
}
} }

  

  • 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常。
  • 如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常,Java运行时将调用NumberFormatException 对应的catch块处理该异常。
  • 如果运行该程序时输入的第二个参数是0,将发生除0异常,Java运行时将调用ArithmeticException对应的catch块处理该异常。
  • 如果程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,Java运行时将调用Exception对应的catch块处理该异常。

上面程序中的三种异常是我们在编程中经常遇见的,读者应该掌握这些异常。

例2:

import java.util.Date;

public class Test {

	public static void main(String[] args) {
Date d=null;
try {
System.out.println(d.after(new Date()));
}catch(NullPointerException e) {
System.out.println("空指针异常");
}catch(Exception e) {
System.out.println("未知异常");
}
} }

  

上面程序针对NullPointerException异常提供了专门的异常处理块。上面程序调用一个null对象的after0方法,这将引发NullPointerException异常(当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常),Java 运行时将会调用NullPointerException对应的catch块来处理该异常;如果程序遇到其他异常,Java运行时将会调用最后的catch块来处理异常。

catch块处理异常遵循着:先小后大,即先子类后父类。正如在前面程序所看到的,程序总是把对应Exception类的catch块放在最后,这是为什么呢?读者可能明白原因:如果把Exception类对应的catch块排在其他catch块的前面,Java运行时将直接进入该catch块(因为所有的异常对象都是Exception或其子类的实例),而排在它后面的catch块将永远也不会获得执行的机会。

多异常捕获:

在Java7之前,每个catch块只能捕获一个异常,Java7之后,每个catch块可以捕获多种类型的异常。

上面的例1可以改成如下代码实现:

public class Test {

	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 e) {
System.out.println("数组越界,数字格式异常,算术异常");
}catch(Exception e) {
System.out.println("未知异常");
}
} }

  

2、使用throws声明抛出异常:

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

import java.io.FileInputStream;
import java.io.FileNotFoundException; public class Test { public static void main(String[] args) throws FileNotFoundException {
FileInputStream fis=new FileInputStream("a.txt");
} }

  

上面程序声明不处理IOException异常,将该异常交给JVM处理,所以程序一旦遇到该异常,JVM就会打印该异常的跟踪栈信息,并结束程序。运行上面程序,会看到如下图所示的运行结果。

Java——深入理解Java异常体系

3、使用throw抛出异常:

public class Test {

	public static void main(String[] args) throws Exception {
try {
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
if(b==0) {
throw new Exception("除数不能为0");
}
System.out.println("您输入的两个数相除的结果是"+c);
}catch(Exception e) {
System.out.println("未知异常");
}
} }

  

上面程序中粗体字代码使用throw语句来自行抛出异常。当Java运行时接收到开发者自行抛出的异常时,同样会中止当前的执行流,跳到该异常对应的catch块,由该catch块来处理该异常。也就是说,不管是系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有任何差别。

4、访问异常信息:

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

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

getMessage():返回该异常信息的跟踪栈信息输出到标准错误输出

printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。

printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。

getStackTrace():返回该异常的跟踪栈信息。

5、使用finally回收资源:

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

在哪里回收这些物理资源呢?在try块里回收?还是在catch块中进行回收?假设程序在try块里进行资源回收,根据图10.1所示的异常捕获流程—一如果try块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。如果在catch块里进行资源回收,但catch块完全有可能得不到执行,这将导致不能及时回收这些物理资源。

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

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

  

例如:

public class Test {

	public static void main(String[] args) throws Exception {
try {
int a=10;
int b=0;
int c=a/b;
}catch(Exception e) {
System.out.println("未知异常");
}finally {
System.out.println("资源回收");
}
} }

  

结果:

未知异常
资源回收

  

总结:

Java——深入理解Java异常体系

上一篇:java(异常体系及权限修饰符)


下一篇:C语言库函数实现 【微软面试100题 第八十三题】