错误恢复在我们所编写的每一个程序中都是基本的要素,并且在 Java 中它显得格外重要,因为 Java 的主要目标之一就是创建供他人使用的程序构件。

发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前。然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。

Java 使用异常来提供一致的错误报告模型,使得构件能够与客户端代码可靠地沟通问题。

Java 中的异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加确信:你的应用中没有未处理的错误。

异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器强制执行。本章将教你如何编写正确的异常处理程序,以及当方法出问题的时候,如何产生自定义的异常。

一、异常的概念

C 以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。这些模式通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。然而,随着时间的推移,人们发现,程序员们在使用程序库的时候更倾向于认为:“对,错误也许会发生,但那是别人造成的,不关我的事”。所以,程序员不去检查错误情形也就不足为奇了(何况对某些错误情形的检查确实很无聊)。如果的确在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读。正是由于程序员还仍然用这些方式拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。

解决的办法是,使用用强制规定的形式来进行错误处理。这种做法由来已久,对异常处理的实现可以追溯到 20 世纪 60 年代的操作系统。 C++的异常处理机制基于 Ada,Java 中的异常处理机制则建立在 C++ 的基础之上(尽管看上去更像 Object Pascal)。

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

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

二、异常的基本介绍

异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的情况。将异常情形与普通问题相区分是很重要的,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。而对于异常情形,因为在当前环境下无法获得必要的信息来解决问题,所以程序就不能继续执行下去了。我们所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。

除法就是一个简单的例子。除数有可能为 0,所以先进行检查很有必要。但除数为 0 代表的究竟是什么意思呢?通过当前正在解决的问题所在的环境,或许我们能知道该如何处理除数为 0 的情况。但如果这是一个意料之外的值,我们也不清楚该如何处理,那就要抛出异常,而不是顺着原来的路径继续执行下去。

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

举一个抛出异常的简单例子。对于对象引用 t,传给你的时候可能尚未被初始化。所以在使用这个对象引用调用其方法之前,会先对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中“抛出”,这样就把错误信息传播到了“更大”的环境中。这被称为抛出一个异常,看起来像这样:

if(t == null)throw new NullPointerException();

这就抛出了异常,于是在当前环境下就不必再为这个问题操心了,它将在别的地方得到处理。

2.1 异常的参数

与使用 Java 中的其他对象一样,我们总是用 new 在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器:一个是无参构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:

throw new NullPointerException("t = null");

我们在后面可以看到,要将这个字符串的内容提取出来可以有多种不同的方式。

在使用 new 创建了异常对象之后,此对象的引用将传给 throw。尽管异常对象的类型通常与方法定义时的返回类型不同,但从效果上看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,当然这种对比并不准确。另外还能用抛出异常的方式从当前的作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。

注意异常返回的“地点”与普通方法调用返回的“地点”完全不同。(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层级。)

此外,我们能够抛出任意类型的 Throwable 对象,它是异常类型的根类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。(通常,唯一的信息只有异常的类型名,而在异常对象内部没有任何有意义的信息。)

三、异常的捕获

要明白异常是如何被捕获的,必须首先理解监控区域(guarded region)的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

3.1 try语句块

如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为在这个块里“尝试”各种(可能产生异常的)方法调用,所以称为 try 块。它是跟在 try 关键字之后的普通程序块:

try {// Code that might generate exceptions
}

对于不支持异常处理的程序语言,要想仔细检查错误,就得在每个方法调用的前后加上设置和错误检查的代码,甚至在每次调用同一方法时也得这么做。有了异常处理机制,可以把所有动作都放在 try 块里,然后只需在一个地方就可以捕获所有异常,这意味着你的代码将更容易编写和阅读。

3.2 异常处理程序

当然,抛出的异常必须在某处得到处理。这个“地点”就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在 try 块之后,以关键字 catch 表示:

try {// Code that might generate exceptions
} catch(Type1 id1) {// Handle exceptions of Type1
} catch(Type2 id2) {// Handle exceptions of Type2
} catch(Type3 id3) {// Handle exceptions of Type3
}
// etc

每个 catch 子句(异常处理程序)看起来就像是接收且仅接收一个特殊类型的参数的方法。可以在处理程序的内部使用标识符(id1,id2 等等),这与方法参数的使用很相似。有时可能用不到标识符,因为异常的类型已经给了你足够的信息来对异常进行处理,但标识符并不可以省略。

异常处理程序必须紧跟在 try 块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。

注意在 try 块的内部,许多不同的方法调用可能会产生类型相同的异常,而你只需要提供一个针对此类型的异常处理程序。

3.3 终止与恢复

异常处理理论上有两种基本模型。Java 支持终止模型(它是 Java 和 C++所支持的模型)。在这种模型中,将假设错误非常严重,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。

另一种称为恢复模型。意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。如果想要用 Java 实现类似恢复的行为,那么在遇见错误时就不能抛出异常,而是调用方法来修正该错误。或者,把 try 块放在 while 循环里,这样就不断地进入 try 块,直到得到满意的结果。

在过去,使用支持恢复模型异常处理的操作系统的程序员们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。

四、自定义异常

Java异常体系不可能预见你将报告的所有错误,所以你可以创建自己的异常类,来表示你的程序中可能遇到的问题。

要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生无参构造器,这几乎不用写多少代码:

// exceptions/InheritingExceptions.java
// Creating your own exceptions
class SimpleException extends Exception {}public class InheritingExceptions {public void f() throws SimpleException {System.out.println("Throw SimpleException from f()");throw new SimpleException();}public static void main(String[] args) {InheritingExceptions sed =new InheritingExceptions();try {sed.f();} catch(SimpleException e) {System.out.println("Caught it!");}}
}

输出:

Throw SimpleException from f()
Caught it!

编译器创建了无参构造器,它将自动调用基类的无参构造器。其实对异常来说,最重要的部分就是类名,所以本例中建立的异常类在大多数情况下已经够用了。

本例的结果被显示在控制台。你也可以通过写入 System.err 而将错误发送给标准错误流。通常这比把错误信息输出到 System.out 要好,因为 System.out 也许会被重定向。如果把结果送到 System.err,它就不会随 System.out 一起被重定向,所以用户就更容易注意到它。

你也可以为异常类创建一个接受字符串参数的构造器:

// exceptions/FullConstructors.java
class MyException extends Exception {MyException() {}MyException(String msg) { super(msg); }
}
public class FullConstructors {public static void f() throws MyException {System.out.println("Throwing MyException from f()");throw new MyException();}public static void g() throws MyException {System.out.println("Throwing MyException from g()");throw new MyException("Originated in g()");}public static void main(String[] args) {try {f();} catch(MyException e) {e.printStackTrace(System.out);}try {g();} catch(MyException e) {e.printStackTrace(System.out);}}
}

输出为:

Throwing MyException from f()
MyExceptionat FullConstructors.f(FullConstructors.java:11)at FullConstructors.main(FullConstructors.java:19)
Throwing MyException from g()
MyException: Originated in g()at FullConstructors.g(FullConstructors.java:15)at FullConstructors.main(FullConstructors.java:24)

新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键字明确调用了其基类构造器,它接受一个字符串作为参数。

在异常处理程序中,调用了在 Throwable 类声明(Exception 即从此类继承)的 printStackTrace() 方法。就像从输出中看到的,它将打印“从方法调用处直到异常抛出处”的方法调用序列。这里,信息被发送到了 System.out,并自动地被捕获和显示在输出中。但是,如果调用默认版本:

e.printStackTrace();

信息就会被输出到标准错误流。

4.1 异常与记录日志

你可能还想使用 java.util.logging 工具将输出记录到日志中。基本的日志记录功能还是相当简单易懂的:

// exceptions/LoggingExceptions.java
// An exception that reports through a Logger
// {ErrorOutputExpected}
import java.util.logging.*;
import java.io.*;
class LoggingException extends Exception {private static Logger logger =Logger.getLogger("LoggingException");LoggingException() {StringWriter trace = new StringWriter();printStackTrace(new PrintWriter(trace));logger.severe(trace.toString());}
}
public class LoggingExceptions {public static void main(String[] args) {try {throw new LoggingException();} catch(LoggingException e) {System.err.println("Caught " + e);}try {throw new LoggingException();} catch(LoggingException e) {System.err.println("Caught " + e);}}
}

输出为:

___[ Error Output ]___
May 09, 2017 6:07:17 AM LoggingException <init>
SEVERE: LoggingException
at
LoggingExceptions.main(LoggingExceptions.java:20)
Caught LoggingException
May 09, 2017 6:07:17 AM LoggingException <init>
SEVERE: LoggingException
at
LoggingExceptions.main(LoggingExceptions.java:25)
Caught LoggingException

静态的 Logger.getLogger() 方法创建了一个 String 参数相关联的 Logger 对象(通常与错误相关的包名和类名),这个 Logger 对象会将其输出发送到 System.err。向 Logger 写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是 severe()。为了产生日志记录消息,我们欲获取异常抛出处的栈轨迹,但是 printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一个 java.io.PrintWriter 对象作为参数(PrintWriter 会在 附录:I/O 流 一章详细介绍)。如果我们将一个 java.io.StringWriter 对象传递给这个 PrintWriter 的构造器,那么通过调用 toString() 方法,就可以将输出抽取为一个 String。

尽管由于 LoggingException 将所有记录日志的基础设施都构建在异常自身中,使得它所使用的方式非常方便,并因此不需要客户端程序员的干预就可以自动运行,但是更常见的情形是我们需要捕获和记录其他人编写的异常,因此我们必须在异常处理程序中生成日志消息;

// exceptions/LoggingExceptions2.java
// Logging caught exceptions
// {ErrorOutputExpected}
import java.util.logging.*;
import java.io.*;
public class LoggingExceptions2 {private static Logger logger =Logger.getLogger("LoggingExceptions2");static void logException(Exception e) {StringWriter trace = new StringWriter();e.printStackTrace(new PrintWriter(trace));logger.severe(trace.toString());}public static void main(String[] args) {try {throw new NullPointerException();} catch(NullPointerException e) {logException(e);}}
}

输出结果为:

___[ Error Output ]___
May 09, 2017 6:07:17 AM LoggingExceptions2 logException
SEVERE: java.lang.NullPointerException
at
LoggingExceptions2.main(LoggingExceptions2.java:17)

还可以更进一步自定义异常,比如加入额外的构造器和成员:

// exceptions/ExtraFeatures.java
// Further embellishment of exception classes
class MyException2 extends Exception {private int x;MyException2() {}MyException2(String msg) { super(msg); }MyException2(String msg, int x) {super(msg);this.x = x;}public int val() { return x; }@Overridepublic String getMessage() {return "Detail Message: "+ x+ " "+ super.getMessage();}
}
public class ExtraFeatures {public static void f() throws MyException2 {System.out.println("Throwing MyException2 from f()");throw new MyException2();}public static void g() throws MyException2 {System.out.println("Throwing MyException2 from g()");throw new MyException2("Originated in g()");}public static void h() throws MyException2 {System.out.println("Throwing MyException2 from h()");throw new MyException2("Originated in h()", 47);}public static void main(String[] args) {try {f();} catch(MyException2 e) {e.printStackTrace(System.out);}try {g();} catch(MyException2 e) {e.printStackTrace(System.out);}try {h();} catch(MyException2 e) {e.printStackTrace(System.out);System.out.println("e.val() = " + e.val());}}
}

输出为:

Throwing MyException2 from f()
MyException2: Detail Message: 0 null
at ExtraFeatures.f(ExtraFeatures.java:24)
at ExtraFeatures.main(ExtraFeatures.java:38)
Throwing MyException2 from g()
MyException2: Detail Message: 0 Originated in g()
at ExtraFeatures.g(ExtraFeatures.java:29)
at ExtraFeatures.main(ExtraFeatures.java:43)
Throwing MyException2 from h()
MyException2: Detail Message: 47 Originated in h()
at ExtraFeatures.h(ExtraFeatures.java:34)
at ExtraFeatures.main(ExtraFeatures.java:48)
e.val() = 47

新的异常添加了字段 x 以及设定 x 值的构造器和读取数据的方法。此外,还覆盖了 Throwable. getMessage() 方法,以产生更详细的信息。对于异常类来说,getMessage() 方法有点类似于 toString() 方法。

既然异常也是对象的一种,所以可以继续修改这个异常类,以得到更强的功能。但要记住,使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其他的就不管了(大多数 Java 库里的异常都是这么用的),所以对异常所添加的其他功能也许根本用不上。

五、异常声明

Java 鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。当然,如果提供了源代码,客户端程序员可以在源代码中查找 throw 语句来获知相关信息,然而程序库通常并不与源代码一起发布。为了预防这样的问题,Java 提供了相应的语法(并强制使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。

异常说明使用了附加的关键字 throws,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:

void f() throws TooBig, TooSmall, DivZero { // ...

但是,要是这样写:

void f() { // ...

就表示此方法不会抛出任何异常(除了从 RuntimeException 继承的异常,它们可以在没有异常说明的情况下被抛出,这些将在后面进行讨论)。

代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java 在编译时就可以保证一定水平的异常正确性。

不过还是有个能“作弊”的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。

这种在编译时被强制检查的异常称为被检查的异常。

15.1 异常(异常的基本概念+自定义异常)相关推荐

  1. c++中的异常--1(基本概念, c语言中处理异常,c++中处理异常,异常的基本使用,栈解旋)

    异常基本概念 异常处理就是处理程序中的错误,所谓错误是指在程序运行的过程中发生的一些异常事件(如:除0退出,数组下标越界,所要读取的文件不存在,空指针,内存不足等等) c语言中处理异常 两种方法: 使 ...

  2. python学习-异常(异常类型,异常处理、自定义异常)

    文章目录 python标准异常 异常处理 自定义异常 python标准异常 异常就是影响程序正常执行的一个事件. 某一行代码发生异常,程序就退出了,后续代码都不会执行. 异常名称 描述 BaseExc ...

  3. java异常——异常分类+声明已检查异常+如何抛出异常+自定义异常类

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java异常--异常分类+声明已检查异常+如何抛出异常+自定义异常类 的相关知识: 0.2)异 ...

  4. -1-6 java 异常简单介绍 java异常 异常体系 Throwable 分类 throws和throw 异常处理 自定义异常...

    异常 异常:异常就是Java程序在运行过程中出现的错误. 异常由来:问题也是现实生活中一个具体事务,也可以通过java 的类的形式进行描述,并封装成对象. 其实就是Java对不正常情况进行描述后的对象 ...

  5. 15.笔记go语言——Web编程概念

    15.笔记go语言--Web编程概念 Go目前已经拥有了成熟的Http处理包,这使得编写能做任何事情的动态Web程序易如反掌. Web原理 浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去 ...

  6. python自定义异常类时、可以继承的类是_Python异常类型及处理、自定义异常类型、断言...

    异常的概念.识别报错信息 异常处理 断言的基本使用 异常类型(异常就是报错) 常见异常 NameError:名称错误 SyntaxError:语法错误 TypeError:类型错误 错误回溯 查看报错 ...

  7. 22.Java之异常处理(异常介绍,异常体系图一览,运行时异常,编译异常,try-catch方式处理异常,throws异常处理,自定义异常,throws 和 throw 的区别)

    22.1.异常介绍 Java语言中,将程序执行中发生的不正常情况称为 "异常" (开发过程中的语法错误和逻辑错误不是异常) 执行过程中所发生的异常事件分为两大类: Error:Ja ...

  8. python异常模块raise的概念以及基本用法

    当程序出现错误,python会自动引发异常,也可以通过raise显示地引发异常.一旦执行了raise语句,raise后面的语句将不能执行.        raise():  用raise语句来引发一个 ...

  9. 第3集丨Java中的异常、处理方式及自定义异常汇总

    目录 一.异常的分类 1.1 常见的运行时异常 1.2 常见的检查异常 1.3 继承关系 二.异常处理机制 三.try-catch-finally 四.声明抛出异常 五.人工抛出异常 六.自定义异常 ...

  10. 学习java的第15天之 异常

    异常产生的原因 因为硬件缺失,程序不够严谨,必须的内容缺失导致的程序问题称为异常 类似与人生病 异常的分类 Throwable Error: 证明硬件出现问题,不处理 常见的错误: OutOfMemo ...

最新文章

  1. 如何快速分辨一个男人是不是程序员
  2. 【数据挖掘知识点二】概率基础
  3. shell脚本mysql_Shell脚本中执行sql语句操作MySQL数据库的几个方法
  4. html导航栏代码跳转,微信小程序自定义底部导航栏tabBar(含跳转页面wx.navigateTo)...
  5. 浅谈shell中的clear命令实现
  6. 开挂的人生: 本科生发Nature和 Science
  7. Open vSwitch 使用
  8. form和ajax同时提交吗,form表单提交与ajax消息传递
  9. 2、数字,字符串,列表,字典,集合
  10. css3中的@font-face的用法(定义多个规则)
  11. EXCEL IFS函数的使用
  12. dlib 怎么安装vs2017_VS2017+DLib_19.17详细配置教程
  13. [蓝桥杯] 扩散 (Python 实现)
  14. 怀疑chinaitlab的一个Linux模块被黑了!!
  15. 医学生建议计算机系学生植发,00后都开始“秃”了?皮肤科专家:不想成为“地中海”,这件事情要记牢...
  16. 项目怎么加入城市服务器,服务器奔溃逼疯玩家 模拟城市5或可加入单机模式
  17. Swift中方法的多面性
  18. [ERROR:0] global /build/opencv/modules/videoio/src/cap_ffmpeg_impl.hpp (2791) open VIDEOIO/FFMPEG: F
  19. 小程序 wx.showModal
  20. python 统计文件夹下的文件夹/某类型文件的数目

热门文章

  1. 内存泄漏分析小工具分享(基于UMDH)
  2. 实习踩坑之路:URL传参错误,导致后台查不到数据,但是不报错
  3. 算法笔记-----归并排序
  4. html页面发送post请求中文乱码,用XMLHTTP Post/Get HTML页面时的中文乱码问题之完全Script解决方案...
  5. Apache Flink CDC 批流融合技术原理分析
  6. Demo 示例:如何原生的在 K8s 上运行 Flink?
  7. 有这些信号,你可能需要跳槽了
  8. mysql char最大长度_MySQL中的CHAR和VARCHAR到底支持多长?
  9. oracle存档模式,Oracle学习系列之如何开启归档模式
  10. cad注释比例和打印比例不一样_CAD中输出不同比例图纸效率不高,原因在这里