通过异常处理错误(3):捕获所有异常

一、捕获所有异常

    可以只写一个异常处理程序来捕获所有类型的异常。通常捕获异常类型的基类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来创建其结果。在循环中拼接字符串,使用它可以有效地提高效率。

如果本文对您有很大的帮助,还请点赞关注一下。

发布了112 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104472399