《Java编程思想》笔记09------异常处理

本文深入讲解Java中的异常处理机制,包括基本概念、自定义异常、异常链、异常说明及标准异常等。并通过实例演示如何捕获和处理异常,以及如何在程序中合理使用异常。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、基本异常

异常情形: 阻止当前方法或作用域继续执行的问题。

异常处理程序: 将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。在没有其它办法的情况下,异常允许我们强制程序停止运行,并告诉我们出现了什么问题。理想状态下,还可以强制程序处理问题,并返回到稳定状态。

异常参数:用new在堆上创建异常对象,所有标准异常类都有两个构造器,一个默认的,一个带参的。能够抛出任意类型的Throwable对象,它是异常类型的根类。

二、捕获异常

try块: 如果在方法内部抛出了异常,这个方法将在抛出异常的过程中结束,要是不希望方法就此结束,可以在方法中设置一个特殊的块来捕获异常,因为在这个代码块中“尝试”各种可能产生异常的方法调用,所以称为try块。

try{
    //需要捕获异常的代码
}
复制代码

异常处理程序: 抛出的异常会在异常处理程序中得到处理。而且针对每个要捕获的异常,需要准备相应的处理程序。异常处理程序紧跟在try块之后,以关键字catch表示:

try{
    //需要捕获异常的代码
} catch(Type1 id1){
    //Type1异常的处理程序
} catch(Type2 id2){
    //Type2异常的处理程序
} catch(Type3 id3){
    //Type3异常的处理程序
}
复制代码

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

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

终止与恢复

异常处理理论上有两种基本模型:终止模型和恢复模型。

Java支持终止模型,在这种模型中,假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。

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

三、自定义异常

要自己定义异常类,需要从已有的异常类继承,最好是选择意思相近的类继承。建立新的异常类型最简单的方法就是让编译器为你产生默认构造器,几乎不用写多少代码。 eg:

/**
 * 自定义异常
 * @author LiangYu
 * 2018-01-19
 */
public class SimpleExceptionTest {
	//抛出一个带有参数的异常
	public void exceptionWithMessage() throws SimpleException{
		System.out.println("来自exceptionWithMessage的异常");
		throw new SimpleException("Exception from exceptionWithMessage"); //把异常抛出,交给方法调用者来处理
	}
	//抛出一个无参异常
	public void exceptionEmpty() throws SimpleException{
		System.out.println("来自exceptionEmpty的异常");
		throw new SimpleException();  //把异常抛出,交给方法调用者来处理
	}
	@Test
	public void test(){
		try{
			exceptionEmpty();
		}catch (SimpleException e) {
			e.printStackTrace(System.out); //将异常的详细内容打印到控制台
		}
		try{
			exceptionWithMessage();
		}catch (SimpleException e) {
			e.printStackTrace(); //将异常的详细内容打印到错误流(控制台上以红色字显示错误信息)
		}
	}
}


//自定义异常
class SimpleException extends Exception{
	public SimpleException() {}
	public SimpleException(String msg){
		super(msg);
	}
	
}
复制代码

结论:

1.大多数情况下,自定义的异常只需要使用默认构造器即可,它会自动调用基类默认构造器。对于异常来说,最重要的部分是类名。

2.Throwable类声明的printStackTrace()方法会将异常信息输出到标准错误流中。

异常与记录日志

可以使用java.util.logging工具将输出记录到日志中。

/**
 * 异常与记录日志
 * @author LiangYu
 * 2018-01-19
 */
public class LoggingExceptionTest {
	@Test
	public void test() throws Exception {
		try{
			throw new LoggingException();
		}catch(LoggingException e){
			System.err.println("Caught:"+e);
		}
		try{
			throw new LoggingException();
		}catch(LoggingException e){
			System.err.println("Caught:"+e);
		}
	}
}

class LoggingException extends Exception{
	private Logger logger = Logger.getLogger("loggingException");
	public LoggingException(){
		StringWriter trace = new StringWriter();
		printStackTrace(new PrintWriter(trace));  //输出相应的错误信息到控制台
		logger.severe(trace.toString());   //输出异常信息,标注为“严重”
	}
}
复制代码

结论:

  1. Logger.getLogger()方法创建一个String参数相关联的Logger对象,这个Logger对象会将其输出发送到System.err。
  2. 向Logger写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法。这里使用的是severe(),会将异常标注为严重。

开发过程中,更常见的情形是:我们需要捕获和记录其他人编写的异常,因此,我们必须要在异常处理程序中生成日志信息:

eg:

/**
 * 在异常处理程序中记录日志
 * @author LiangYu
 * 2018-01-19
 */
public class LoggingInCatchTest {
	public void exceptionEmpty() throws LoggingException2{
		System.out.println("来自exceptionEmpty的异常");
		throw new LoggingException2();
	}
	public void exceptionWithMessage() throws LoggingException2{
		System.out.println("来自exceptionWithMessage的异常");
		throw new LoggingException2("exception from exceptionWithMessage()");
	}
	public void exceptionWithMessageAndNumber() throws LoggingException2{
		System.out.println("来自exceptionWithMessageAndNumber的异常");
		throw new LoggingException2("exception from exceptionWithMessageAndNumber()",55);
	}
	@Test
	public void test() throws Exception {
		try{
			exceptionEmpty();
		}catch (LoggingException2 e) {
			e.printStackTrace();
		}
		try{
			exceptionWithMessage();
		}catch (LoggingException2 e) {
			e.printStackTrace();
		}
		try {
			exceptionWithMessageAndNumber();
		} catch (LoggingException2 e) {
			e.printStackTrace();
			System.out.println("e.getValue()=" + e.getValue());
		}
	}
}

class LoggingException2 extends Exception{
	private int num;
	public LoggingException2(){}
	public LoggingException2(String msg){
		super(msg);
	}
	//带变量值得构造方法
	public LoggingException2(String msg,int num){
		super(msg);
		this.num = num;
	}
	//获取变量值
	public int getValue(){
		return num;
	}
	@Override
	public String getMessage() {
		return "Detail Message:"+num+" "+super.getMessage();
	}
}
复制代码

结论:

1.可以通过覆盖Throwable的getMessage()方法来产生更详细的信息,对于异常类而言,getMessage()类似于toString()

2.通常情况下,我们只是为了看一下抛出异常的类型,因此,对异常所添加的很多功能也许用不上。

四、异常说明

通过throws关键字,可以让方法的调用者确切知道可以捕获的所有潜在的异常。

代码必须与异常说明相一致,如果方法里的代码产生了异常却没有进行处理,编译器会报错。

可以声明方法将抛出异常,但是实际上并未抛出。编译器将会相信这个声明,并强制此方法的用户像真的抛出异常一样来使用这个方法。这样做的好处是:先给异常占个位置,以后就可以抛出这种异常,而不用修改已有的代码。在定义抽象基类和接口时,这种能力很重要。

五、捕获所有异常

捕获异常类型的基类Exception(还有其它基类),这可以保证异常一定会被捕获,最好把它放到异常处理程序列表的末尾。

Exception可以调用其从基类继承的方法:

String getMessage()
复制代码

获取详细信息(抛出异常对象所带的参数),类似于toString()。

void printStackTrace()

void printStackTrace(PrintStream)

void printStackTrace(java.io.PrintWriter)
复制代码

打印Throwable和Throwable的调用栈轨迹。调用栈显示了“把你带到异常抛出地点”的方法调用序列。其中第一个版本是输出到标准错误,后两个版本是选择要输出的流。

Throwable fillInStackTrace()
复制代码

用于在Throwable对象的内部记录栈帧的当前状态,这在程序重新抛出错误或异常时很有用。

栈轨迹:

printStackTrace()提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组。其中,每一个元素代表栈中的一帧。元素0为栈顶元素,并且,会在调用序列中的最后一个方法调用。数组中的最后一个元素会在调用序列的第一个方法调用。

eg:

/**
 * 栈轨迹演示
 * @author LiangYu
 * 2018-01-19
 */
public class WhoCalledTest {
	void functionA() throws Exception{
		throw new Exception();
	}
	void functionB() throws Exception{
		functionA();
	}
	void functionC() throws Exception{
		functionB();
	}
	void functionD() throws Exception{
		functionC();
	}
	@Test
	public void test(){
		try {
			functionD();
		} catch (Exception e) {
			for(StackTraceElement sElement : e.getStackTrace()){
				System.out.println(sElement.getMethodName());
			}
		}
	}
}
复制代码

输出结果: functionA functionB functionC functionD test ……

重新抛出异常

有时希望把刚捕获的异常重新抛出,尤其是使用Exception捕获所有异常的时候。既然得到了当前异常对象的引用,可以直接将它抛出。

catch(Exception e){
    System.out.println("An exception was thrown");
    throw e;
}
复制代码

重新抛出异常,会把异常抛给上一级环境中的异常处理程序。同一个try块后面的catch方法会被忽略。

如果只是将当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈对象,而不是重新抛出点的调用栈对象。要想更新这个信息,需要调用fillInStackTrace()方法,这将返回一个Throwable对象,它将更新调用栈的信息。

/**
 * 重新抛出异常
 * @author LiangYu
 * 2018-01-19
 */
public class RethrowTest {
	void functionA() throws Exception{
		System.out.println("Exception from functionA");
		throw new OneException("throw from functionA");
	}
	
	void functionB() throws Exception{
		try {
			functionA();
		} catch (Exception e) {
			System.out.println("Exception from functionB");
			e.printStackTrace();
			throw (Exception)e.fillInStackTrace();
		}
	}
	
	@Test
	public void test() {
		try {
			functionB();
		} catch (Exception e) {
			System.out.println("main:stackTrace");
			e.printStackTrace();
		}
		System.out.println("-------------------------------------------------------");
		try{
			try {
				functionA();
			} catch (Exception e) {
				e.printStackTrace();
				throw new TwoException("from inner try");
			}
		}catch (TwoException e) {
			System.out.println("Outer try");
			e.printStackTrace();
		}
	}
}

class OneException extends Exception{
	public OneException(String s) {
		super(s);
	}
}

class TwoException extends Exception{
	public TwoException(String s) {
		super(s);
	}
}
复制代码

结论:

  1. 通过fillInStackTrace()可以获得新的调用栈信息
  2. 有可能在捕获一个异常之后,又抛出一个新的异常。此时,得到的效果类似于fillInStackTrace(),旧的异常发生点信息会消失,剩下的都是与新的异常发生点有关。
  3. 永远不必为清理前一个异常对象而担心,它们都是new在堆上的对象,所以垃圾回收器会自动处理它们。

异常链

要在捕获一个异常之后,抛出下一个异常,并且希望把原始信息保留下来,这被称为异常链。JDK 1.4版本之前,程序员必须自己编写代码来保存原始异常信息。现在所有的Throwable子类在构造器中,都可以接受一个cause(因由)对象作为参数。这个对象就是原始异常,这样就可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能将异常链追踪到异常最初发生的位置。

在Throwable子类中,只有Error、Exception、RuntimeException提供了带cause参数的构造器。如果要把其他异常链接起来,需要调用initCause()方法。

eg: 动态的向对象添加字段。

/**
 * 向一个对象中动态添加字段
 * @author LiangYu
 * 2018-01-19
 */
public class DynamicFieldsTest {
	@Test
	public void test(){
		DynamicFields dFields = new DynamicFields(3);
		System.out.println(dFields);
		try {
			dFields.setField("d", "A value for d");
			dFields.setField("num1", 333);
			dFields.setField("num2", 45);
			System.out.println(dFields);
			dFields.setField("d", "new Value");
			dFields.setField("num3",38);
			System.out.println(dFields);
			System.out.println("d:"+dFields.getField("d"));
			Object field = dFields.setField("d",null);
		} catch (DynamicFieldsException e) {
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		}
	}
}


class DynamicFieldsException extends Exception{}

class DynamicFields{
	private Object[][] fields;
	//初始化,根据字段的个数分配,将所有的字段和他们的值设置为null
	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 stringBuilder = new StringBuilder();
		for(Object[] objects : fields){
			stringBuilder.append(objects[0]);
			stringBuilder.append(":");
			stringBuilder.append(objects[1]);
			stringBuilder.append("\n");
		}
		return stringBuilder.toString();
	}
	
	private int hasField(String fieldName){
		for(int i = 0 ; i < fields.length ; i++){
			if(fieldName.equals(fields[i][0])){
				return i;
			}
		}
		return -1;
	}
	
	private int getFieldNumber(String fieldName) throws NoSuchFieldException{
		int fieldNum = hasField(fieldName);
		if(fieldNum == -1){
			throw new NoSuchFieldException();
		}
		return fieldNum;
	}
	
	private int makeField(String fieldName){
		for(int i = 0 ; i < fields.length ; i++){
			if(fields[i][0] == null){
				fields[i][0] = fieldName;
				return i;
			}
		}
		//如果字段的空间已经满了,自动扩展
		Object[][] temp = new Object[fields.length+1][2];
		for(int i = 0 ; i < fields.length ; i++){
			temp[i] = fields[i];
		}
		for(int i = fields.length ; i < temp.length ; i++){
			temp[i] = new Object[]{null,null};
		}
		fields = temp;
		return makeField(fieldName);
	}
	
	public Object getField(String fieldName) throws NoSuchFieldException{
		return fields[getFieldNumber(fieldName)][1];
	}
	
	public Object setField(String fieldName,Object fieldValue) throws DynamicFieldsException{
		if(fieldValue == null){
			DynamicFieldsException dynamicFieldsException = new DynamicFieldsException();
			//此处,形成异常链,先抛出DynamicFieldsException然后抛出NullPointerException
			dynamicFieldsException.initCause(new NullPointerException());
			throw dynamicFieldsException;
		}
		int fieldNumber = hasField(fieldName);
		if(fieldNumber == -1){
			fieldNumber = makeField(fieldName);
		}
		Object result = null;
		try {
			result = getField(fieldName); //获取之前的值
		} catch (NoSuchFieldException e) {
			throw new RuntimeException(e);
		} 
		fields[fieldNumber][1] = fieldValue;
		return result;
	}
	
}
复制代码

输出结果会先抛出DynamicFieldsException然后抛出NullPointerException

六、Java标准异常

Throwable这个Java类被用来表示任何可以作为异常被抛出的类。

Throwable被分为两类:

  • Error用来表示编译时和系统错误(基本不用关心)
  • Exception是可以被抛出的基本类型,在Java类库、用户方法以及运行时可以抛出Exception型异常。所以Java程序员关心的基类型通常是Exception。

特例:RuntimeException

RuntimeException类属于“不被检查的异常”。这种异常属于错误,将被自动捕获并直达main(),不需要程序员亲自动手。

七、使用finally进行清理

对于一些代码,无论try有无抛出,都希望执行,为了这样可以在异常处理最后加上finally语句。这样无论异常是否抛出,finally子句总能被执行。

try中有return语句,还是会继续执行finally里的语句。

缺憾:异常丢失

public class FinallyTest {
	void i() throws ImportantException{
		throw new ImportantException();
	}
	void l() throws LightException{
		throw new LightException();
	}
	@Test
	public void test(){
		try {
			try {
				i();
			} finally {
				l();
			}
		} catch (Exception e) {
			System.out.println(e);
		}
	}
}

class ImportantException extends Exception{
	@Override
	public String toString() {
		return "import";
	}
}

class LightException extends Exception{
	@Override
	public String toString() {
		return "Light";
	}
}
复制代码

输出结果为Light,ImportantException的异常丢失了。

八、异常的匹配

异常抛出后,找到匹配的处理程序后,就认为异常已经得到了处理,不再继续寻找。

如果异常与第一个匹配,那就捕获,如果不是,就由Exception捕获,因为Exception会捕获Exception以及其他从它派生的异常。


有些章节读不下去,之前从事开发工作的时候也没有用到过,感觉有点枯燥鸡肋。

先在这里标注一下,以后开发过程中如果有不明白的地方再翻书做笔记。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值