目录
概念
基本的异常
捕获异常
try块
异常处理程序(catch)
创建自己的异常
异常说明(及检查型异常)
捕获任何异常
多重捕捉
栈轨迹
重新抛出异常
异常链
本笔记参考自: 《On Java 中文版》
Java的基本哲学之一:写得不好的代码无法运行。
错误恢复机制对Java尤为重要,因为Java的一个主要目标就是创建程序组件供他人使用。捕获错误的理想时机是在编译时,也就是在程序员试图运行程序之前。但不是所有错误都能在编译时被发现。其他的问题需要在运行时通过其他方式解决。
Java使用异常提供了一个一致的错误报告模型。从而使组件可以将问题可靠地传达给客户代码。
在Java中,异常处理的目标是减少当前的代码量(但在之后会了解到,异常处理也会变相增加一些代码)。
概念
C语言,乃至一些早期的编程语言往往有多种错误处理机制,但这类机制通常是通过约定建立的,而不是作为编程语言的一部分。这些机制会要求程序员检测错误条件。但我们发现,若每次调用一个方法,都要去彻底检查错误,我们的代码就会变得难以阅读。
Java沿用了C++的解决方案:结束原本自由散漫的错误处理方式,并强制实施正规手段。
当“异常”(exception)出现时,若当前的上下文没有提供足够的信息来解决这个问题,我们就需要停止我们的动作,并将问题交给更上层的上下文进行决策。
异常降低了错误处理代码的复杂性。它使得我们不再需要在方法调用的地方检测错误,因为异常会保证有人会捕获它。
在理想的情况下,我们只在一个地方处理问题,就是在所谓的异常处理程序中。
基本的异常
||| 异常情形:是指阻止当前方法或作用域继续执行的问题。这些问题在当前上下文章没有必要的信息进行处理。
与异常情形相对的是普通问题,即指在当前上下文中有足够信息,我们能够以某种方式解决的问题。
当抛出一个异常时,会发生几件事情:
- 异常对象被创建(使用new创建,并放在堆上)。
- 当前执行路径停止,指向这个异常对象的引用被从上下文中抛出。
- 异常处理机制接管控制,并开始寻找可以继续执行这个程序的适当位置。
在这里,适当位置指的就是“异常处理程序”,这一程序的作用是从问题中恢复,使程序能够尝试另一条路径,或是继续执行。
抛出异常:通过创建一个表示信息的对象,并将其“抛”出当前上下文,我们可以将关于这个错误的信息发送给一个更大的上下文。这就是抛出异常。例如,下面的语句将会检测引用,若其为空,则抛出异常:
if(t == null)
throw new NullPointerException();
这个被抛出的异常会在其他地方被处理掉。
我们可以把异常看作是一个内置的“撤销”系统。因为通过异常,我们可以在程序中小心地设置各种恢复点。若发生了不好的事情,异常不允许程序沿正常的路径继续执行。
异常参数
异常和Java中的其他任何对象一样,都是通过new在堆上创建异常(分配空间,调用构造器)。所有标准异常类都有两个构造器:①无参构造器;②接受一个String参数的构造器,这一参数用于在异常中放置相关信息。
throw new NullPointerException("t == null");
throw关键字有一些特别之处。上述的这个异常对象实际上是从方法中“返回”的,尽管它的类型通常不会是我们设计让这个方法返回的。通过抛出异常,我们可以退出当前的方法或是作用域(并且获取一个异常对象)。
但是,和正常的方法返回不同,从异常中返回所到达的地方将会是一个适当的异常处理程序,这与异常被抛出的位置可能距离很远(若调用堆栈,会发现中间叠加了许多层)。
所有的异常类型存在一个根类:Throwable。
我们通常会为每一种不同类型的错误抛出一个不同的异常类。关于错误的信息即包含在异常对象中,也隐含在异常对象的名字中(但通常,异常对象仅有的信息就是异常类的名字)。
捕获异常
||| 被守护区域:一段可能会产生异常的代码,其后会跟着处理这些异常的代码。
try块
若我们正处于一个方法中,这个方法或其调用的方法抛出了异常,那么该方法就会在抛出异常的过程中退出。
若不希望退出当前方法,就需要使用一个特殊的块捕获这个异常。这个块被称为try块,它是跟在try关键字后的普通作用域:
try {
// 内部放置可能产生异常的代码
}
使用异常处理,我们可以把所有内容放置在一个try块中,并且在这里捕获所有异常。这种做法在一定程度上化简了代码,保证代码的正常执行不会因为过于复杂的错误检测而受干扰。
异常处理程序(catch)
被抛出的异常会在异常处理程序这被处理(每个异常都可以有自己的异常处理程序)。这一程序紧跟在try块之后,使用catch关键字表示:
try {
// 可能引发异常的代码
} catch(Type1 id1) {
// 处理Type1类型的异常
} catch(Type2 id2) {
// 处理Type2类型的异常
} catch(Type3 id3) {
// 处理Type3类型的异常
}
// ...
每个catch子句(异常处理程序)都只接受一个特定类型的参数。在这里,标识符(如id1等)可以在处理程序中进行使用(尽管有时我们并不使用标识符,但它们必须存在)。
异常处理程序必须紧跟在try块后面。若异常被抛出,异常处理机制会去查找参数与异常类型相匹配的第一个处理程序(只有匹配的catch子句会执行,一旦子句完成,对处理程序的搜索就会停止)。
在try块中,不同的方法调用可能产生同样的异常,但我们只需要一个针对这一异常的处理程序。
终止与恢复
在异常处理理论中,存在着两种基本模型。Java支持的是终止模型,我们会假设错误过于严重,以至于无法返回(也不会想返回)异常发生的地方。
另一种是恢复模型。它表示通过异常处理程序,我们可以修正一些情况,然后再重新尝试出现问题的方法,并假定第二次可以成功。
恢复模型的问题在于其所导致的耦合:处理程序需要知道异常的抛出位置(并且要包含特定于抛出位置的非通用代码)。这会使代码难以编写和维护。
若想在java中应用恢复模型,可以选择在遇到错误时不抛出异常,而是调用某个能处理这个问题的程序。或是把try块放入一个循环中,不断进入循环,知道出现一个令人满意的结果。
创建自己的异常
Java的异常体系无法预测我们可能遇见的所有错误,所以我们可以创建自己的异常,以此来表示自己的代码可能遇到的特殊问题。
要创建异常类,可以继承现有的异常类,最好的继承对象是与我们定义的新异常含义接近的(但这不太可能)。Java提供的异常通过具有两个构造器:无参的和带有一个String参数的。使用无参构造器,那么最简单的自定义异常类几乎不需要任何代码:
【例子:创建最简单的异常类】
class SimpleException extends Exception {
// 编译器自动创建无参构造器,并且会自动调用基类的无参构造器
}
public class InheritingExceptions {
public void f() throws SimpleException {
System.out.println("从f()中抛出异常SimpleException");
throw new SimpleException();
}
public static void main(String[] args) {
InheritingExceptions sed =
new InheritingExceptions();
try {
sed.f();
} catch (SimpleException e) {
System.out.println("捕获异常SimpleException");
}
}
}
程序执行的结果是:
在上述代码中,编译器创建了一个(异常类的)无参构造器,并且自动(隐式地)调用了其基类的无参构造器。从中可以看出,对一个异常类而言,最重要的就是其的类名。
上述示例将结果打印到了控制台上。除此之外,还可以使用System.err将错误发生到标准错误流。这通常会是个更好的选择,因为System.out可以被重定向。
【例子:带有String参数的异常类构造器】
class MyException extends Exception {
MyException() {
}
MyException(String msg) {
super(msg); // 显式调用了基类的带有String参数的构造器
}
}
public class FullConstructors {
public static void f() throws MyException {
System.out.println("从f()中抛出异常MyException");
throw new MyException();
}
public static void g() throws MyException {
System.out.println("从g()中抛出异常MyException");
throw new MyException("来自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);
}
}
}
程序执行的结果是:
上述例子的处理程序调用了Throwable(Exception类就是从它继承而来的)的一个方法:printStackTrace()。这一方法会输出到达异常发生点的方法调用序列的信息。例子中将这些信息发生给了System.out,并自动被捕获和打印在了输出中。若调用printStackTrace()的默认版本:
e.printStackTrace();
那么这些信息将会进入标准错误流。
异常与日志记录
可以使用java.util.logging工具将输出记录到日志中:
【例子:将异常纪录到日志中】
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
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("捕获异常" + e);
}
}
}
程序执行的结果是:
通过语句
static Logger.getLogger("LoggingException");
我们可以创建一个与String参数“LoggingException”(这一错误通常是错误相关的包和类的名字)关联的Logger对象,这个对象会被发生给System.err。
printStackTrace()的默认版本不会生成String,这里通过StringWriter和PrintWriter的组合,将printStackTrace()的结果通过toString调用出来(作为一个String)。
LoggingException可以做到在异常中自动工作。但更常见的状况是其被用于捕获他人的异常,并记入到日志中。因此,我们必须在异常处理程序中生成日志信息:
【例子:在异常处理程序这生成日志信息】
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
public class LogggingException2 {
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);
}
}
}
程序执行的结果是:
若是创建自己的异常,还可以添加更多的构造器和成员。
【例子:进一步创建异常】
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;
}
@Override
public String getMessage() {
return "详细信息:" + x + " " + super.getMessage();
}
}
public class ExtraFeatures extends Exception {
public static void f() throws MyException2 {
System.out.println("从f()抛出异常MyException2");
throw new MyException2();
}
public static void g() throws MyException2 {
System.out.println("从g()抛出异常MyException2");
throw new MyException2("来自g()");
}
public static void h() throws MyException2 {
System.out.println("从h()抛出异常MyException2");
throw new MyException2("来自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);
}
}
}
程序执行的结果是:
异常只是另一种对象。尽管异常类可以像一个普通类一样被装饰,但使用这些包的客户程序员可能只会想要寻找被抛出的异常,因此大多装饰会失去意义。
异常说明(及检查型异常)
Java鼓励人们将其方法中可能抛出的异常告知调用该方法的客户程序员(因为客户程序员不一定能够接触到库的源代码),这样客户程序员就可以解决它们。这就是异常说明,它是方法声明的组成部分,出现在参数列表之后。
这种说明会使用额外的关键字throws,其后跟着可能被抛出的异常的列表:
void f() throws FirstException, SecondException, ThirdException{ // ...
若不使用这一异常说明:
void f() { // ...
则意味着这个方法不会抛出异常(除了从RuntimeException继承来的异常,这种异常可以从任何地方抛出,而不需要异常说明)。
异常说明必须与实际情况匹配,若方法引发了异常(且方法本身没有处理该异常),此时编译器会给我们两个选择:①处理这个异常;②使用异常说明,指出这个异常可以从该方法中抛出。
在实际操作中,我们可以声明会抛出某个异常,而实际上不这么做。编译器认为这是合法的,并会要求使用该方法的用户按照方法会抛出这个异常的情况进行处理。通过这种方式,可以为预订的异常占个位置。
这种在编译时被检测并强制实施的异常叫做检查型异常。
捕获任何异常
异常类型有一个基类Exception:
可以对Exception进行捕获,这样就可以创建一个可以捕获任何异常的处理程序。
catch(Exception e) {
System.out.println("捕获一个异常");
}
也存在其他的基本异常,不过Exception与编程活动更为相关。
因为这种捕获对所有的异常有效,因此需要把它放在处理程序列表的最后,以避免它在其他处理程序前面捕获了异常。
Exception是对程序员而言很重要的异常类的基类,因此它不会带有过多的具体信息,但这一点可以通过调用其基类Throwable的方法进行弥补。
String getMessage() // 获取详细信息
String getLocalizeMessage() // 获取针对特定区域调整过的信息
另外,toString()方法会包含Throwable的简短描述和一些详细的信息(如果有)。
下列展示的三个方法会展示Throwable及其调用栈轨迹。调用栈会显示把我们带到异常抛出电的方法调用序列。
// 打印到标准错误流
void printStackTrace()
// 打印到我们选择的流
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)
还有一个方法fillInStackTrace(),这个方法可以记录当前的Throwable对象的栈帧的当前状态信息,多用于重新抛出错误或异常。
此为,Object类提供了一些有用的方法:getClass()会返回表示这个对象的类的Class对象。也可以查询这个Class对象的名字,getName()获得的结果包含了包信息,而getSimpleName()获得的结果只包含了类名。
【例子:Exception的基本方法】
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("一个异常");
} catch (Exception e) {
System.out.println("捕获一个异常");
System.out.println(
"getMessage(): " + e.getMessage());
System.out.println(
"getLocalizeMessage(): " + e.getLocalizedMessage());
System.out.println("toString(): " + e.toString());
System.out.println("printStackTrace():");
e.printStackTrace(System.out);
}
}
}
程序执行的结果是:
上述的每个方法都提供了比前一个方法更多的信息。实际上,每一个方法都是前一个方法的超集。
多重捕捉
若想以同样的方式处理一组异常,且这组异常有一个共同的基类,那么捕获这个基类即可。但若它们没有共同的基类,在Java 7之前,必须为每一个异常写一个catch子句。
Java 7提供了多重捕捉处理程序,允许我们在一个catch子句中使用“|”操作符连接不同类型的异常:
【例子:多重捕捉】
class EBase1 extends Exception {}
class Except1 extends EBase1 { }
class EBase2 extends Exception { }
class Except2 extends EBase1 { }
class EBase3 extends Exception { }
class Except3 extends EBase1 { }
class EBase4 extends Exception { }
class Except4 extends EBase1 { }
public class MultiCatch {
void x() throws Except1, Except2, Except3, Except4 {}
void process() {}
void f() {
try {
x();
} catch (Except1 | Except2 | Except3 | Except4 e) {
process();
}
}
}
通过这种多重捕捉的机制,就可以将需要集中处理的异常集合起来,减少代码的重复,使代码更加清晰。
栈轨迹
printStackTrace()提供信息,而getStackTrace()可以直接访问这些信息。
如上图所示,getStackTrace()的返回值是一个由栈轨迹元素组成的数组,其中的每个元素都表示一个栈帧。下标为0的元素是栈顶,其中存储的是序列中的最后一个方法调用(就是这个Throwable被创建和抛出的位置)。
【例子:访问栈轨迹】
public class WhoCalled {
static void f() {
// 生成一个异常,用来填充栈轨迹
try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement ste : e.getStackTrace())
System.out.println(ste.getMethodName());
}
}
static void g(){
f();
}
static void h(){
g();
}
public static void main(String[] args) {
f();
System.out.println("=========");
g();
System.out.println("=========");
h();
}
}
程序执行的结果是:
也可以直接打印StackTraceElement,以获取更多信息。
重新抛出异常
有时会需要重新抛出刚捕获的异常,尤其是使用Exception捕获任何异常的时候。
catch(Exception e) {
System.out.println("抛出一个异常");
throw e; // 通过已有的引用进行抛出
}
重新抛出一个异常,会导致其进入邻近的更上层上下文中的异常处理程序。并且同样会忽略同一个try块后面的catch子句。另外,关于这个异常对象的所有信息都会被保留,以保证处理程序对信息的提取。
若重新抛出当前异常,printStackTrace()打印的关于异常的信息,仍会是原来异常抛出点的信息,而不是重新抛出异常的地方的信息。为了加入新的栈轨迹信息,就需要使用fillInStackTrace()。
这一方法所返回的Throwable对象,是通过将当前栈的信息塞到原本的异常对象中创建的。
【例子:fillInStackTrace()的使用例】
public class Rethrowing {
public static void f() throws Exception {
System.out.println("异常产生于f()");
throw new Exception("从f()中抛出异常");
}
public static void g() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("在g()中,调用e.printStackTrace(): ");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("在h()中,调用e.printStackTrace(): ");
e.printStackTrace(System.out);
throw e;
}
}
public static void main(String[] args) {
try {
g();
} catch (Exception e) {
System.out.println("在main()中,调用e.printStackTrace():");
e.printStackTrace(System.out);
System.out.println();
}
try {
h();
} catch (Exception e) {
System.out.println("在main()中,调用e.printStackTrace():");
e.printStackTrace(System.out);
System.out.println();
}
}
}
程序执行的结果是:
由输出可知,fillInStackTrace()被调用的那一行,成为了异常的新起点。
也可以抛出一个与所捕获的异常不同的异常,这同样可以做到类似于fillInStackTrace()的效果。重新抛出会导致异常的原始调用点的信息丢失,只剩下与新的throw相关的信息。
【例子:重新抛出不同的异常】
class OneException extends Exception {
OneException(String s) {
super(s);
}
}
class TwoException extends Exception {
TwoException(String s) {
super(s);
}
}
public class RethrowNew {
public static void f() throws OneException {
System.out.println("异常产生于f()");
throw new OneException("从f()中抛出");
}
public static void main(String[] args) {
try {
try {
f();
} catch (OneException e) {
System.out.println("在内层的try中捕获,e.printStackTrace():");
e.printStackTrace(System.out);
throw new TwoException("从内部的try中抛出");
}
} catch (TwoException e) {
System.out.println("在内层的try中捕获,e.printStackTrace():");
e.printStackTrace(System.out);
}
}
}
程序执行的结果是:
异常也是通过new在堆上创建的对象,因此它们也会被垃圾收集器自动清理。
精准地重新抛出异常
在Java 7之前,若我们捕获了一个异常,那么我们只能重新抛出这个异常。这会导致代码中出现不精准的问题。例如:
【例子:Java 7之前不允许的精准抛出异常】
class BaseException extends Exception {
}
class DerivedException extends BaseException {
}
public class PreciseRethrow {
void catcher() throws DerivedException {
try {
throw new DerivedException();
} catch (BaseException e) { // 捕获了父类
throw e;
}
}
}
这种代码在Java 7之前是不被允许的。因为catch捕获了一个BaseException,所以编译器会强制我们声明catcher() throws BaseException,尽管其抛出的是一个更具体的DerivedException。
从Java 7开始,这种代码可以被编译了,这很有用。
异常链
有时我们会需要捕获一个异常并抛出另一个,并且仍然保留原始异常的信息,这被称为异常链。
在Java 1.4之后,Throwable多了包含cause对象的构造器:
这个cause就是原始的异常,尽管我们会创建和抛出一个新的异常,但原本的栈轨迹依旧可以通过cause进行跟踪。
在Throwable的子类中,有三种基本的异常类提供了这个带cause参数的构造器,它们分别是Error(JVM用它报告系统错误)、Exception和RuntimeException。
除上述三个基本的异常类外,大部分异常没有支持cause参数的构造器。这种情况下就需要使用initCase(),这一方法适用于所有的Throwable的子类。
【例子:异常链】
class DynamicFieldsException extends Exception {
}
public class DynamicFields {
private Object[][] fields;
public DynamicFields(int initialSize) {
fields = new Object[initialSize][2];
for (int i = 0; i < initialSize; i++)
fields[i] = new Object[]{null, null};
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
for (Object[] obj : fields) {
result.append(obj[0]);
result.append(": ");
result.append(obj[1]);
result.append("\n");
}
return result.toString();
}
private int hasField(String id) {
for (int i = 0; i < fields.length; i++)
if (id.equals(fields[i][0]))
return i;
return -1;
}
private int getFieldNumber(String id)
throws NoSuchFieldException {
int fieldNum = hasField(id);
if (fieldNum == -1) // 若不存在指定数据项
throw new NoSuchFieldException();
return fieldNum;
}
private int makeField(String id) {
for (int i = 0; i < fields.length; i++)
if (fields[i][0] == null) {
fields[i][0] = id;
return i;
}
// 若不存在空的数据项,则添加一个:
Object[][] tmp = new Object[fields.length + 1][2];
for (int i = 0; i < fields.length; i++)
tmp[i] = fields[i];
for (int i = fields.length; i < tmp.length; i++)
tmp[i] = new Object[]{null, null};
fields = tmp;
//完成fields扩展后,递归调用
return makeField(id);
}
public Object getField(String id)
throws NoSuchFieldException {
return fields[getFieldNumber(id)][1];
}
public Object setField(String id, Object value)
throws DynamicFieldsException {
if (value == null) {
// DynamicFieldsException没有支持cause的构造器
// 所以需要使用initCause()方法
DynamicFieldsException dfe = new DynamicFieldsException();
dfe.initCause(new NullPointerException());
throw dfe;
}
int fieldNumber = hasField(id);
if (fieldNumber == -1)
fieldNumber = makeField(id);
Object result = null;
try {
result = getField(id); // 处理getField()可能抛出的异常
} catch (NoSuchFieldException e) {
// 若getField()引发异常,就需要处理它
// 将其转为RuntimeException()进行抛出
// RuntimeException()有可以接受cause的构造器
throw new RuntimeException(e);
}
fields[fieldNumber][1] = value;
return result;
}
public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
System.out.println(df);
try {
df.setField("d", "为d赋予一个值");
df.setField("number", 47);
df.setField("number2", 48);
System.out.println(df);
df.setField("d", "赋予d一个新的值");
df.setField("number3", 11);
System.out.println("df: " + df);
System.out.println("df.getField(\"d\"): "
+ df.getField("d"));
Object field = df.setField("d", null); // 引发异常
} catch (NoSuchFieldException
| DynamicFieldsException e) {
e.printStackTrace(System.out);
}
}
}
程序执行的结果是: