Java异常机制

一. Java异常机制

程序是人写的,是人就避免不了犯错,即使对于资深技术专家也是如此,更不用说初学者了。试想一下,如果Java没有提供异常机制,发生以下情况会发生什么现象,又将如何进行处理。
1.调用不在当前作用域的null对象。
2.访问越界的数组元素。
以正常编程习惯来说,我们不会写一行代码就运行一次,通常都会实现一个功能后在进行调试。如果一个功能含有很多行代码,而每行代码都会有各种各样的问题发生,一旦运行程序,我们根本不知道问题发生在哪里,更不要提如何解决了。即使是这样,你还会说:对于每行代码,都进行问题检测并进行相应处理,不就行了。这样的话,“正常代码”和“错误检测与修复代码”就会混在一起,代码将会变得难以阅读,维护一个小的功能都不容易,更不用说一个庞大的系统了。而且,我们也不可能预料到所有可能的情况,例如:网络断连,堆栈溢出,数据库异常等等一系列问题。所以对于一种语言来说,提供一种错误处理模式,是非常必要的。
为此,Java也提供了自己的一套错误处理模式——Java异常机制。

Java异常机制是一种错误检测报告与错误恢复机制,该机制保证程序在错误发生时,中断当前程序的执行并进行错误报告,并能够在代码的任意地方将问题捕获并进行处理。并且,只需在一个地方处理该异常,即所谓的“异常处理程序”中。通过该机制,可以 把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离,这样程序员就很容易专注于业务代码的编写,代码也更易阅读和维护。

二. 异常的抛出与捕获

1)异常的抛出

异常最重要的作用之一就是当问题发生时,它们将不允许程序沿着正常的路径继续走下去。

如何保证这一点。

通过关键字throw将异常对象从当前环境中抛出到更高级别的环境中,当前环境下的程序在异常未解决之前不能继续执行,直到异常被解决。

例如:
  NullPointException是Java中最常见的异常,由于对象引用在调用之前未被正确的初始化,导致对象引用为null。现假设有一对象引用obj,在传给你时可能未被正确初始化,所以在调用之前会检查该对象引用是否为空,如果该对象引用的确未被正确赋值,将抛出NullPointException。

if(obj == null) 
	throw new NullPointException();

异常抛出后,将发生什么?
  首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象,然后,当前的执行路径被终止,并且从当前环境弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序,这个恰当的地方就是异常处理程序。它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

2)异常的捕获

  我们知道当问题发生时,throw会抛出异常,并交由异常处理机制处理,异常处理机制会找到异常处理程序,由它将错误恢复。但如何检测异常的发生并进行捕获,这是一个问题。Java由此设计能够检测异常的方法——监控区域,它是一段可能发生异常的代码,其后跟着异常处理程序。也就是所谓的try、catch块。

try块
  它是由关键字try修饰的普通代码块。里面包含可能(注意:可能两个字,未发生异常的代码也可以放在它里面)发生异常的代码。

try {
		//可能发生异常的代码
}

catch块
  抛出的异常会在某个地点进行处理,这个地点就是异常处理程序。异常处理程序紧跟在try块之后,以关键字catch表示:

try {
		//可能发生异常的代码
} catch (Exception e) {
		// 异常处理程序
}

  由此可以看到,异常检测和异常处理的代码分别放在了try块和catch块中,和“正常执行”的代码相分离,这样程序员可以更关注于“正常执行”的代码,降低了错误处理的复杂度。

异常捕获规则
  每个catch都有一个特定异常类型的参数,当异常抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个异常处理程序(匹配规则是:异常类型 instanceof 参数类型)。然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则异常处理程序的查找过程结束。所以当catch的参数为Exception、Throwable等父类异常类型时,尽量将其放在异常处理程序列表的末尾,以防他抢在其他异常 处理程序之前将异常捕获。

3)重抛异常

  有时,当前环境下的异常处理程序没有足够的信息处理该异常,或出于业务的考虑要将刚捕获的异常抛出到更高一级的环境中。既然已经得到了对当前异常对象的引用,可以直接把它重新抛出:

catch (Exception e) {
            System.out.println("重新抛出一个异常");
            throw e;
}

  重抛异常会把异常抛给更高一级的异常处理程序,同一个try块的后续catch子句将被忽略。此外,异常对象的所有信息都得以保持,所以更高一级环境中捕获此异常的处理程序可以从这个异常对象得到所有异常信息。

三. 异常说明

try-catch块能进行异常的检测与捕获,但没有任何提示说明哪些代码将会产生异常。我们不能将方法内所有的代码用try进行修饰,因为那会破坏异常机制使"正常执行"的代码和"异常处理"的代码相分离的特性。而通过查找调用方法的throw语句也是不可行的,通常一个方法的调用栈很深,而将过多的精力放在查找throw上是一种愚蠢的行为。

为解决这一问题,Java提供了相应的语法(并强制使用此语法),告诉客户端程序员某个方法可能抛出的异常类型,然后客户端程序员就可以进行异常检测并完成相应的处理。这就是异常说明。异常说明属于方法的一部分,紧跟在形式参数列表之后。

Java通过throws关键字实现异常说明,其后跟随潜在的异常类型列表。所以,看起来会像下面这样:

public void method() throws OneException,TowException,ThreeException {}

但如果要这样写:就表示此方法不会抛出任何异常(RuntimeException类型异常除外)

public void method() {}

代码必须与异常说明保持一致:
如果方法里的代码产生了异常却没有抛出,编译器会发现这个问题并提醒你,要么在方法内处理这个异常,要么就在异常说明中表示此方法将产生异常。这种在编译时被强制检查的异常称为“编译时异常"。

四. 异常分类

Throwable是异常结构中的超类,它有两个直接派生类,分别是Error和Exception。

Error
表示系统错误,在运行中是不可恢复的,它会终止程序的运行,属于JVM层次的严重错误,必须解决。一般该错误产生的原因是由于开发者的逻辑问题导致的,所以不应该在程序中捕获,必须立即进行处理。例如:一个系统中生成的对象过多,GC不能及时回收,导致系统内存不足,抛出 java.lang.OutOfMemoryError ,或者递归太深,导致堆栈溢出,抛出java.lang.StackOverflowError,它们都是 java.lang.VirtualMachineError的子类,属于JVM层次的错误。这些错误都是由于开发者对技术的掌握和对软件开发系统不够了解造成的,属于软件设计问题的逻辑错误。

Exception
这类错误都是可恢复的,由开发人员自身的疏忽或编程错误导致的。例如:使用I/O流读取文件,未指定正确的文件,抛出 java.io.FileNotFoundException ;在调用数组元素前,未了解数组的容量,试图获取超过数组长度的元素,抛出 java.lang.ArrayIndexOutOfBoundsException。根据是否在编译时被编译器强制检查,分为编译时异常和运行时异常。

编译时异常:
在编译时被编译器强制检查,抛出编译时异常的方法必须附有异常说明,且调用声明有编译时异常的方法必须进行异常检测,要么处理该异常,要么重新抛出到更高一级的环境中。
常见的编译时异常有:
java.lang.ClassNotFoundException      没有找到具有指定名称的类
java.sql.SQLException            提供有关数据库访问错误或其他错误信息的异常
java.io.IOException             I/O失败或中断产生的异常

运行时异常:
在运行时产生,不受编译器检查的异常,也被称为不受检查异常。这类异常属于编程错误,它们会被JVM自动捕获并重新抛出给调用者,所以不必进行异常说明。
常见的运行时异常有:
java.lang.ClassCastException       强制转型为不属于该实例子类类型的对象
java.lang.IllegalArgumentException     方法中使用非法参数
java.lang.NullPointerException       调用为null的对象引用

五. 创建自定义异常

Java提供的异常体系不可能预见所有错误,所以可以自己定义异常类来表示程序中可能会遇到的特定的异常。
创建自定义异常类非常简单,只需继承父类即可,最好继承意义相同的异常,但这样的异常类并不好找。所以直接继承Exception即可。

public class UserDefinedException extends Exception { }
public class Test {

    public static void f() throws UserDefinedException {
        System.out.println("抛出自定义异常");
        throw new UserDefinedException();
    }

    public static void main(String[] args) {
        try {
            f();
        } catch (Exception e) {
            System.out.println("异常捕获:" + e);
        }
    }
}

UserDefinedException除了简单的定义并没有实体内容。对于异常类来说,类名就是最重要的内容。因为异常的基本概念是用名称代表所发生的问题,并且异常的名称可以望文知意。

六. finally

对于有一些代码,我们希望无论try块中的异常是否抛出,它们都可以执行。

例如:已经开启的I/O流,我们希望无论是否发生异常,都可以将它关闭,这时finally就派上用场了。
finally保证其内部的代码块一定会执行,无论发生什么情况,即使在return中使用finally也是一样的(由于篇幅实在太长,我就不给大家证明了)。

finally一般放在catch块的后面。所以,最基本的结构是这样的:

try {
	// 可能发生异常的代码块
}catch (Exception e) {
	// 异常处理程序
}finally {
	// 无论是否发生异常,finally内的代码一定都会执行
}

七. 最后

本篇博客部分内容摘抄自<<Java编程思想 第4版>>,加上个人对于Java异常机制的理解编写而成。个人十分推荐<<Java编程思想>>这本书,不愧称为Java领域的"圣经"。

最后,如果本篇有任何错误和不足之处,希望大家在下方评论中给出,我会及时与大家讨论,与君共勉、共同进步。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值