一、捕获所有异常
可以只写一个异常处理程序来捕获所有类型的异常。通常捕获异常类型的基类Exception,就可以做到这一点(事实上还有其他的基类,但Exception是同编程活动相关的基类):
catch(Exception e){
System.out.println("Caught an exception");
}
这将捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常捕获了。
因为Exception是与编程有关的所有异常类的基类,所以它不会含有太多具体的信息,不过可以调用它从其基类Throwable继承的方法:
String getMessage();
String getLocalizedMessage();
用来获取详细信息,或用本地语言表示的详细信息。
String toString();
返回对Throwable的简单描述,要是有详细信息的话,也会把它包含在内。
void printStackTrace();
void printStackTrace(PrintStream);
void printStackTrace(java.io.PrintWriter);
打印Throwable和Throwable的调用栈轨迹。调用栈显示了“把你带到异常抛出地点”的方法调用序列。其中第一个版本输出到标准错误,后两个版本允许选择要输出的流。
Throwable fillInStackTrace();
用于在Throwable对象的内部记录栈帧的当前状态。这在程序重新抛出错误或异常时很有用。
此外,也可以使用Throwable从其基类Object继承的方法。对于异常来说,getClass()也许是个很好用的方法,它将返回一个表示此对象类型的对象。然后可以使用getName()方法查询这个Class对象包含信息的名称,或者使用只产生类名称的getSimpleName()方法。
下面的例子演示了如何使用Exception类型的方法:
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch (Exception e) {
System.out.println("Caught Exception");
System.out.println("getMessage(): " + e.getMessage());
System.out.println("getLocalizedMessage(): " + e.getLocalizedMessage());
System.out.println("toString(): " + e);
System.out.println("printStackTrace(): ");
e.printStackTrace(System.out);
}
}
}
可以发现每个方法都比前一个提供了更多的信息--实际上他们每一个都是前一个的超集。
二、栈轨迹
printStackTrace()方法所提供的信息可以通过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(An exception was thrown);
throw e;
}
如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的,就像这样:
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside g(),e.printStackTrace(): ");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside h(),e.printStackTrace(): ");
e.printStackTrace(System.out);
throw (Exception) e.fillInStackTrace();
}
}
public static void main(String[] args) {
try {
g();
} catch (Exception e) {
System.out.println("main:printStackTrace()");
e.printStackTrace(System.out);
}
try {
h();
} catch (Exception e) {
System.out.println("main:printStackTrace()");
e.printStackTrace(System.out);
}
}
}
调用fillInStackTrace()的那一行就成了异常的新发生地了。
有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息:
class OneException extends Exception {
public OneException(String s) {
super(s);
}
}
class TwoException extends Exception {
public TwoException(String s) {
super(s);
}
}
public class RethrowNew {
public static void f() throws OneException {
System.out.println("originating the exception in f()");
throw new OneException("thrown from f()");
}
public static void main(String[] args) {
try {
try {
f();
} catch (OneException e) {
System.out.println("Caught in inner try, e.printStackTrace()");
e.printStackTrace(System.out);
throw new TwoException("from inner try");
}
} catch (TwoException e) {
System.out.println("Caught in outer try, e.printStackTrace()");
e.printStackTrace(System.out);
}
}
}
最后那个异常仅知道自己来自main(),而对f()一无所知。
永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心。它们都是用new在堆上创建的对象,所以垃圾回收器会自动把它们清理掉。
四、异常链
常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。在JDK1.4以前,程序员必须自己编写代码来保存原始异常的信息。现在所有Throwable的子类在构造器中都可以接受一个cause(因由)对象作为参数。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。
有趣的是,在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器。它们是Error(用于java虚拟机报告系统错误)、Exception以及RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause()方法而不是构造器。
下面的例子能让你在运行时动态地向DynamicFields对象添加字段:
/**
* 动态字段异常
*/
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 };
}
// 输出数组内容
public String toString() {
StringBuffer result = new StringBuffer();
for (Object[] obj : fields) {
result.append(obj[0]);
result.append(":");
result.append(obj[1]);
result.append("\n");
}
return result.toString();
}
// 根据id判断数组中是否含有该key-value
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;
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 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);
} catch (NoSuchFieldException e) {
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", "A value for d");
df.setField("Number", 47);
df.setField("Number2", 48);
System.out.println(df);
df.setField("d", "A new value for 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 e) {
e.printStackTrace();
} catch (DynamicFieldsException e) {
e.printStackTrace();
}
}
}
每个DynamicFields对象都含有一个数组,其元素是“成对的对象”。第一个对象表示字段标识符(一个字符串),第二个表示字段值,值的类型可以是除基本类型外的任意类型。当创建对象的时候,要合理估计一下需要多个字段。当调用setField()方法的时候,它将试图通过标识修改已有字段的值,否则就建一个新的字段,并把值放入。如果空间不够了,将建立一个更长的数组,并把原来数组的元素复制进去。如果你试图为字段设置一个空值,将抛出一个DynamicFieldsException异常,它是通过使用initCause()方法把NullPointerException对象插入而建立的。
至于返回值,setField()将用getField()方法把此位置的旧值取出,这个操作可能会抛出NoSuchFieldException异常。如果客户端程序员调用了getField()方法,那么他就有责任处理这个可能抛出的NoSuchFieldException异常,但如果异常是从setField()方法里抛出的,这种情况将被视为编程错误,所以就使用接受cause参数的构造器把NoSuchFieldException异常转换为RuntimeException异常。
你会注意到,toString()方法使用了一个StringBuilder来创建其结果。在循环中拼接字符串,使用它可以有效地提高效率。
如果本文对您有很大的帮助,还请点赞关注一下。