Java 的异常处理机制 - 续、

本贴最后更新于 2674 天前,其中的信息可能已经沧海桑田

Java 的异常处理机制-续、

前言

上一篇中简单介绍了异常的概念和基本的使用方法。本篇将继续深入了解异常的使用原则。

使用 finally

在使用 finally 时有一些陷阱是需要注意的。我们知道无论是否发生异常 finally 代码块始终都会被执行。考虑一种情况:

try{
//可能产生异常的代码块
return 1;
...
}catch(MyException e){
//捕获异常后的动作
...
}finally{
//无论是否捕获异常这里都会执行,通常做一些清理资源的操作
return 0;
...
}


如果同时在 try 块和 finally 块中存在 return 语句究竟谁会被返回呢?直觉告诉我们好像 try 中的 return 执行后方法就结束了吧,应该返回 try 中的。但是实际上并不是这样,根据 finally 的用法,最终 finally 中的返回值将会覆盖 try 中的返回值(实际验证确实如此)。

根据上面的经验考虑这种情况:

try{
    throw new MyException();
}finally{
	return;
}

异常信息在没有得到处理的情况下丢失了。

异常链

常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常信息保存下来,这被称为 异常链

重新抛出异常

有的时候我们希望重新抛出异常:

try{
	throw new Exception(); 
}catch(Exception e){
	...
	...
	...
	throw e
}

如果想将异常抛出点更新为当前方法,可以调用异常的 fillInStackTrace() 方法,像这样:

try{
	throw new Exception(); 
}catch(Exception e){
	...
	...
	...
	throw (Exception)e.fillInStackTrace();
}


或者抛出新的异常

try{
	throw new Exception(); 
}catch(Exception e){
	...
	...
	...
	//抛出新的异常
	throw new MyException()
}

之前的异常信息丢失了。如果想把之前的异常信息保存下来(保存到新的异常对象中),所有的 Throwable 的子类在构造器中都可以接受一个 cause 对象作为参数(causeThrowable 类型的),这个 cause 就是用来表示原始异常,这样通过把原始异常传递给新的异常,使得我们可以追踪到异常最初发生的地方。

但是在 Throwable 子类中只有三种基本异常提供了带 cause 参数的构造器。ErrorJVM 报告系统错误),ExceptionRuntimeException。如果要把其他异常链接起来需要使用 initCause() 方法而不是构造器。下面是一个示例:

使用构造器:

try{
	throw new MyException();
}catch (MyException e){
	RuntimeException e1 = new RuntimeException(e);
	throw e1;
}

使用 initCause() 方法

try{
	throw new RuntimeException(); 
}catch(RuntimeException e){
	//抛出新的异常保存原来的异常信息
	MyException myException = new MyException();
	保存原来的异常信息
   myException.initCause(e);
   throw myException; //这个异常捕获或者抛出它
}


异常声明

throws 关键字

异常声明在不希望本层处理异常的时候把异常信息向上抛出,由上层代码处理(层层抛出则最终会抛到 JVM)。我们在设计抽象层或者类库的时候常常可以在方法后抛出需要处理的异常。这也是使用者需要遵守的约定之一。

异常声明本身不属于方法说明的一部分,方法类型是由方法的名字和参数类型组成的(实际上返回值类型也属于方法的组成,所谓方法签名,但是重载只根据方法名和参数列表)。所以不能基于异常声明来重载方法。

在继承体系中使用异常应该注意什么?

当覆盖方法或实现接口时只能抛出在基类方法(接口方法)的异常声明中列出的异常。为什么这样呢?我们要理解在方法声明中的异常信息是使用者(子类,实现类)需要遵守的约定之一,你必须处理(抛出)这些没有被处理的异常。所以在继承体系中,异常通常是越抛越少的(因为每层几乎都会处理掉一些)。与之形成对比的是:继承体系中类的方法和数据域往往是越来越丰富的。还有一点:这保证了子类的可替换性,是面向对象的特性。(如果没有这个限制,当替换子类或实现类时,代码将变的不可用,因为异常是不确定的)

构造器中抛出的异常

在继承体系中,派生类的构造器可以抛出任何异常而不受基类构造器异常的限制,但是必须包括基类抛出的异常。

pulbic class A{
	public A()throws MyException1{
	
	}
}

public class B extends A{
	public B() throws MyException1,MyException2{
	
	}
}

派生类构造器不能捕获基类构造器抛出的异常。(捕获异常需要用 try catch 语句包含,但是调用父类构造器的语句必须要在第一行,显然原则相互违背,编译不会通过)我们知道当类的对象被创建时会依次调用其所有基类的构造器。显然在派生类中捕获基类异常是没有意义的,因为对象创建已经在某个基类中失败了,每个构造器应该只关注自己的行为,如果子类构造器干涉了父类构造器抛出异常信息,那么就不能准确判断创建过程中发生的事了。

注意

不应该在异常没有处理的情况下再抛出新的异常,这样原来的异常信息就会丢失。

try{
	throw new MyException();
    try{

}finally{
	throw new MyException1();
}
}catch(Exception e){
	e.printStackTrace(System.out);
}

在异常发生时是不是所有的东西都可以被正确清理?像打开一个文件,获取一个数据库连接这样的操作,需要保证资源最终被释放。但是如果一开始就获取失败呢?考虑下面的这种情况:

try{
	InputFile in = new InputFile("test.txt");
}catch(Exception e){
   e.printStackTrace(System.out);
}finally{
	in.close();
}

如果 in 没有被创建,关闭操作将会失败。改变一下,需要一点点技巧

try{
	InputFile in = new InputFile("test.txt");
	try{
		...
		...
		...
	}catch(Exception e){
		e.printStackTrace(System.out);
	}finally{
	in.close();
}

}catch(Exception e){
	e.printStackTrace(System.out);
}

如果 in 没有被正确创建,则会进入外层的 catch,嵌套逻辑保证了当内层发生异常时,in 一定被创建了。

关于未检查异常

未检查异常(RuntimeException)是不受检查的异常,意思是编译器不会对它进行警告,它属于 运行时异常。这种异常实际上是一种错误(这里说的错误指的是应用运行层面的编程错误),比如 NullPointerException 空指针异常,它是继承自 RuntimeException,想象一下程序在运行过程中某个变量极有可能为空(原因可能是传入参数为空),而导致调用失败,这个时候就需要抛出异常来报告错误。但是在代码编写和编译阶段无法确定异常是否会发生,那么我们就要在每个可能的地方加上异常处理代码,这是相当可怕的。幸运的是 JVM 为我们处理了这类 运行时异常,使我们不必操心运行时的事情。当然你也可以主动抛出 RuntimeException 或者捕获它。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3454 回帖 • 189 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...