每天学一点Thinking in java: 32 处理异常

处理异常

java的基本哲学就是“格式糟糕的代码将不会运行”(badly formed code will not be run)。

捕获错误最佳时机是在编译的时候(compile time),甚至是在运行程序之前。但不是所有的错误都能在编译的时候检测到。那么要在运行时通过某种规范让错误的发起者将适当的信息发送给知道怎么解决问题的地方。

提高错误处理(error recovery)能很好地增强代码的健壮性。错误处理时每个程序的基本关注点,但在java中尤其重要,因为java的一大基本目标就是写程序组件供他人使用。要创建出健壮的系统,每个组件也同样要健壮。通过使用异常来提供统一的报错范式(error-reporting model),Java允许组件可靠地将问题传达给客户端代码。

Java异常处理的目标是使用更少的代码创建大而可靠的程序,并且对于程序不会有未处理的错误有更多的信心

概念

“异常”指的是“我对此不以为然”(I take exception to that)。错误发生的时候,可能不知道该怎么办,但你知道不能再愉快地进行下去。必须停止,然后一些人在什么地方知道该怎么办。但你在当前的语境没有足够的信息解决问题。所以要将问题交给更高的语境从而让有资格的人做出合适的决定。
异常另一个好处是能减少处理错误代码的复杂度。有了异常,就不再需要在调用方法的时候检测错误,因为异常会保障错误被捕获。只要在所谓的异常处理(exception handler)处理问题即可。这样就能将正常运行的代码和出现异常时运行的代码区分开。

基本异常

异常情况(exceptional condition)阻止当前方法或作用域的连续性。从普通问题中区别出异常情况很重要。出现异常情况后,不能再继续处理,因为在当前语境没有关键的信息去处理问题。只能跳出当前语境,到更高的语境处理问题。这是抛出异常时会发生的事。

抛出异常时会发生如下事情:
首先,异常对象被创建(和创建一般Java对象一样,使用new创建)。
之后,当前执行路径停止,异常对象的句柄从当前语境中弹出。
此时,异常处理机制接受,并开始寻找合适的地方继续执行程序,这个合适的地方 就是异常处理(exception handler),它的作用就是从错误中恢复从而使程序 能够改变方针或者继续进行。

例如:有一个对象句柄叫t,如果想在用对象句柄调用方法前检查,可以通过新建一个对象将错误信息发送到更大的语境,然后将其“抛出”你现有的语境。这叫做抛出异常,见下例:

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

可以将所有在做的事情看作事务,异常就是保护这些事务。也可以把异常看作是内置的undo系统,如果程序哪部分失败,则异常会“undo”这部分然后回到程序稳定的点。

异常最重要的一点是如果有坏事发生,异常不会允许程序继续进行下去。异常强行让程序停下来并指出哪里出现问题,或者强迫程序去处理问题然后回到稳定的状态

异常参数

所有标准异常有两个构造方法:1. 默认构造方法;2. 有String类型参数的构造方法,可以把相关的信息放进异常里。如下:

throw new NullPointerException(“t = null”);

理解异常处理最简单的方法就是把它看成是另一种return机制。也可以通过抛出异常来退出当前作用域,关键字throw在这里可以看作是另一种return。异常对象返回,退出方法或作用域。但也就这一点是类似的,因为异常返回的地方和普通方法调用返回的地方截然不同。异常处理的地方会和异常抛出的地方隔很多层堆栈调用。

除此之外,也可以抛出任一类型的Throwable,因为Throwable是异常的根类。通常要为不同类型的错误抛出不同的异常。异常对象和异常类的名字显示出错误的信息。(唯一有用的信息是异常的类型(type of exception),异常对象中存储的通常没什么意义)

捕获异常

要知道异常时怎么捕获的首先要了解一个概念即:防卫区(guarded region):即一段可能产生异常的代码后接处理这些异常的代码。

try代码块
如果不想使用throw直接退出方法,可以使用try代码块,示例如下:
try{
//可能产生异常的代码
}
将要监测异常的代码都放在try代码块中,然后在同一个地方捕获所有异常。

异常处理

所有的异常都在异常处理中处理。异常处理紧接try代码块之后,由关键字catch表示

try { 
	// 可能产生异常的代码
} catch(Type1 id1)|{ 
	// 处理异常Type1
} catch(Type2 id2) { 
	// Handle exceptions of Type2 
} catch(Type3 id3) { 
	// Handle exceptions of Type3 
} 
// etc...

每个catch子句就像一个小型的只接受一个特殊类型参数的方法。有时根本不需要使用标识符,因为知道异常类型就已经知道该怎么处理异常,但标识符还是必须得在那儿。

处理程序必须直接出现在try之后。如果抛出异常,异常处理机制将搜索第一个参数与异常类型匹配的处理程序。然后进入catch子句,并将异常视为已经处理。一旦catch子句完成,将停止对处理程序的搜索。只有匹配的catch子句才能执行;它不像switch,为了防止剩余的子句执行之后加break。注意,在try中,不同方法的调用可能会抛出相同的异常,但是只需要一个异常处理。

终止(Termination)&恢复(resumption)

异常处理有两种基本模式。
一种是终止,即错误太严重,无法再回到异常发生的地方,抛出异常者决定这种情况无法挽救,不再返回远处处理
另一种是恢复,即异常处理希望做些事来进行修复,之后出错的方法废弃,假定下次会成功。要用恢复(resumption),就意味着在处理异常后仍希望程序继续执行。

如果想要在Java中进行类似恢复这样的操作,那么遇到错误时不要抛出异常,取而代之的是调用一个方法来解决问题。或者在while循环中放入try,直到满意结果,否则一直重进入try

恢复听起来很美好,但是在实际中并不实用。主要是因为恢复处理需要直到异常抛出的具体位置,并且要包含具体到抛出位置的非泛型代码(contain non-generic code specific to the throwing location)。这使得代码难以编写和维护,特别是异常会从多个点产生的大型系统

创建自己的异常

要创建自己的异常,则必须继承现有的异常类,最好是和新建的名字相关联的异常类。见下例:

//: 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!"); 
		} 
	  } 
	} /* Output: 
	Throw SimpleException from f()
	Caught it! 
	*///:~

也可以创建构造方法带String参数的异常类,见下例:

//: exceptions/FullConstructors.java 
class MyException extends Exception { 
	public MyException() {} 
	public 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); 
		} 
	} 
} 
/* Output: 
Throwing MyException from f() 
MyException 	
	at 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) 
*///:~

注意printStackTrace(System.out),产生了直到异常发生的方法序列的信息,该信息被发送到System.out,然后自动捕获并显示在结果中。
如果用默认的版本:e.printStackTrace(),则显示的信息是标准错误流(standard error stream)

异常和日志记录

使用java.util.logging将结果记录到日志中。见下例:

//: exceptions/LoggingExceptions.java 
// An exception that reports through a Logger. 
import java.util.logging.*; 
import java.io.*; 
class LoggingException extends Exception { 
	private static Logger logger = Logger.getLogger("LoggingException"); 
	public 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); } 
		} 
} /* 
Output: (85% match) 
Aug 30, 2005 4:02:31 PM LoggingException  
SEVERE: LoggingException 
	    at LoggingExceptions.main(LoggingExceptions.java:19) 

Caught LoggingException 
Aug 30, 2005 4:02:31 PM LoggingException  
SEVERE: LoggingException 
		at LoggingExceptions.main(LoggingExceptions.java:24) 

Caught LoggingException 
*///:~

注意:printStackTrace()默认不输出String,要获得String则要重载printStackTrace(),传入java.io.PrintWriter做参数,向PrintWriter构造器传入java.io.StringWriter对象,结果会调用toString()方法转换为String
printStackTrace(new PrintWriter(trace));

异常类的getMessage()类似toString()

异常规范(exception specification)
异常规范即通过Java句法告诉客户程序员方法抛出的有哪些异常,从而使得客户程序员能处理这些异常。
异常规范使用关键字throws,throws后接一系列异常类型,例如:

void f() throws TooBig, TooSmail,…{
	//…
}

除了继承RunTimeException的异常,这类异常不需要异常规范也能再任意地方抛出。

你逃不过编译器的法眼。如果方法中的代码产生异常,但方法并没有处理异常,并且被编译器发现,那么编译器会告诉你要么处理异常,要么你的方法必须使用异常规范抛出异常。强制执行异常规范,确保在编译时保障异常的正确性。

但是有一种方法可以瞒过编译器:可以抛出一个实际上并不会抛出的异常。编译器会引以为真,并强制使用该方法的人抛出指定的异常。这样做有个好处就是为异常空出位置,但你以后真的要抛出该异常时就不用去更改存在的代码。为可能会抛出异常的衍生类和实现类,创建抽象类和接口也十分重要

在编译时检查并强制执行的异常,叫做检查型异常(checked exception)

捕获异常

可以通过捕获异常基类:Exception来捕获任何一种异常,如下例:

catch(Exception e) { 
	System.out.println("Caught an exception"); 
}

因为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()(获取包名), getSimpleName()(获取类名)

堆栈轨迹(stack trace)

printStackTrace()提供的信息也可以直接通过使用getStackTrace()访问。该方法返回堆栈轨迹元素(stack trace elements)的数组,每个元素表示一个堆栈结构。元素0是堆栈的顶部,下标为0的元素是堆栈顶部,并且使序列中最后一个方法调用;数组最后一个元素是堆栈的底部也是序列中第一个方法调用。见下例:

//: exceptions/WhoCalled.java 
// Programmatic access to stack trace information. 
public class WhoCalled { 
	static void f() { 
		// Generate an exception to fill in the stack trace 
		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(); 
	} 
} /* Output: 
f 
main -------------------------------- 
f
g 
main -------------------------------- 
f
g 
h 
main (注意顺序先f,再g,再h)


重新抛出异常(rethrowing an exception)

有时要重新抛出刚刚捕获到的异常,特别是使用Exception来捕获异常的时候。直接抛出句柄即可,见下例:

catch(Exception e){
	System.out.println(“An exception was thrown”);
	throw e;
}

重新抛出异常会导致转到下一个更详细语境进行异常处理。同一try的其他catch子句仍然会被忽略。此外,异常对象的所有内容都将保留,因此更高语境中捕获特定异常类型的处理程序可以从该对象中提取所有信息。

**如果只是简单地抛出当前异常,则在printStackTrace()**中打印的有关该异常的信息将与产生异常的位置有关,与抛出异常的位置无关。如果要放入新的堆栈跟踪信息,可以通过fillInStackTrace(),该方法返回一个通过将当前堆栈信息填充到旧异常对象中而创建的Throwable对象。见下例:

//: exceptions/Rethrowing.java 
// Demonstrating fillInStackTrace() 
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); 
		} 
	}
} /* Output: 
originating the exception in f() 
Inside g(),e.printStackTrace() 
java.lang.Exception: thrown from f() 
	at Rethrowing.f(Rethrowing.java:7) 
	at Rethrowing.g(Rethrowing.java:11) 
	at Rethrowing.main(Rethrowing.java:29) 
main: printStackTrace() 
java.lang.Exception: thrown from f() 
	at Rethrowing.f(Rethrowing.java:7) 
	at Rethrowing.g(Rethrowing.java:11) 
	at Rethrowing.main(Rethrowing.java:29) 
originating the exception in f() 
Inside h(),e.printStackTrace() 
java.lang.Exception: thrown from f() 
	at Rethrowing.f(Rethrowing.java:7) 
	at Rethrowing.h(Rethrowing.java:20) 
	at Rethrowing.main(Rethrowing.java:35) 
main: printStackTrace() 
java.lang.Exception: thrown from f() 
	at Rethrowing.h(Rethrowing.java:24) 
	at Rethrowing.main(Rethrowing.java:35) 
*///:~

调用fillInStackTrace()那一行成了异常的新起点

也可以重新抛出一个和捕获的异常不同的异常。如果这样做,那么效果和使用fillInStackTrace()时类似

//: exceptions/RethrowNew.java 
// Rethrow a different object from the one that was caught. 
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); 
		} 
	}
} /* Output:
originating the exception in f() 
Caught in inner try, e.printStackTrace() 
OneException: thrown from f() 
	at RethrowNew.f(RethrowNew.java:15) 
	at RethrowNew.main(RethrowNew.java:20) 
Caught in outer try, e.printStackTrace() 
TwoException: from inner try 
	at RethrowNew.main(RethrowNew.java:25) 
*///:~

最后一个异常只知道其来自内部try块,而不是f()

不要担心清除之前的异常,它们是用new创建的,垃圾回收器会自动清理它们

异常链(Exception chaining)

捕获一个异常并抛出另一个异常,但仍然保留原始异常的信息,这称为异常链接。
现在所有Throwable子类都可以在构造器中获取cause对象。cause应该是原始异常,即便创建并抛出新的异常,通过传递cause对象也可以保持堆栈跟踪回到初始位置。

目前这样的Throwable子类是三个基本异常类:Error(由JVM用于汇报系统错误),Exception和RuntimException。要链接其他异常类型,可以通过initCause()方法,见下例:

//: exceptions/DynamicFields.java
// A Class that dynamically adds fields to itself. 
// Demonstrates exception chaining. 
import static net.mindview.util.Print.*; 
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() { 
		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; 
	} 
	// No empty fields. Add one: 
	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; 
	// Recursive call with expanded 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) { 
		// Most exceptions don’t have a "cause" constructor. 
		// In these cases you must use initCause(), 
		// available in all Throwable subclasses. 
		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); 
		// Get old value 
	} catch(NoSuchFieldException e) { 
	 // Use constructor that takes "cause": 
	 throw new RuntimeException(e); 
	} 
	fields[fieldNumber][1] = value; 
	return result; 
} 
public static void main(String[] args) { 
	DynamicFields df = new DynamicFields(3); 
	print(df); 
	try { 
		df.setField("d", "A value for d"); 
		df.setField("number", 47); 
		df.setField("number2", 48); 
		print(df); 
		df.setField("d", "A new value for d"); 
		df.setField("number3", 11); 
		print("df: " + df); 
		print("df.getField(\"d\") : " + df.getField("d")); 
		Object field = df.setField("d", null); 
		// Exception 
	} catch(NoSuchFieldException e) { 
		e.printStackTrace(System.out); 
	} catch(DynamicFieldsException e) { 
		e.printStackTrace(System.out); 
	} 
 } 
} /* Output: 
null: null 
null: null 
null: null 

d: A value for d 
number: 47 
number2: 48 

df: d: A new value for d 
number: 47 
number2: 48 
number3: 11 

df.getField("d") : A new value for d 
DynamicFieldsException 
	at DynamicFields.setField(DynamicFields.java:64) 
	at DynamicFields.main(DynamicFields.java:94) 
Caused by: java.lang.NullPointerException 
	at DynamicFields.setField(DynamicFields.java:66) 
	... 1 more 
*///:~

如果要插入null值,则会通过创建一个并通过使用initCause()将NullPointerException作为cause插入从而抛出DynamicFieldsException

作为返回值,setField()还使用getField()取出该字段位置的旧值,这可能会抛出NoSuchFieldException。 如果客户端程序员调用getField(),那么他们负责处理NoSuchFieldException,但是如果在setField()中抛出此异常,则是编程错误,因此使用带有cause参数的构造函数将NoSuchFieldException转换为RuntimeException(throw new RuntimeException(e))

标准Java异常

java类Throwable描述的是可以被当作异常抛出。大体上有两种Throwable对象的类型(类型指的是其衍生类):1. 一种是错误(Error),其表示编译和系统的错误,该类错误不用费心思去捕获(特殊情况除外)2. 另一种是异常(Exception),其是可能从任何Java标准库类方法,自定义的方法以及运行事故中抛出的基本类型。

关于异常,基本思想是异常的名字表示出现的是什么问题,并且异常的名字旨在自我解释(selfexplanatory)。异常不是都在java.lang中,有些在其他的库里如,util,net和io,这可以从异常的全类名或继承的包中得知。

特殊例子:RuntimException
见下例:
if(t == null)
throw new NullPointerException();

要检查每一个传入方法的参数是否为null,看起来有点吓人。但实际上并不需要这么做,当有null值传入时,Java会自动抛出NullPointerException。所以上面的代码是多余的,即便是为了防止NullPointerException的出现做了其他的措施。

当然不需要为方法抛出RuntimeException(或者继承RuntimeException的类),这些异常是非检查型异常(unchecked exceptions)。因而不用捕获RuntimeException。如果要强制检查RuntimeException则你的代码太混乱了。虽然不用捕获RuntimeException,但在自己的包中可以选择抛出一些RuntimeException

那不捕获这样的异常会发生什么?见下例:

//: exceptions/NeverCaught.java 
// Ignoring RuntimeExceptions. 
// {ThrowsException} 
public class NeverCaught { 
	static void f() { 
		throw new RuntimeException("From f()"); 
	} 
	static void g() { 
		f(); 
	} 
	public static void main(String[] args) { 
		g(); 
	} 
} ///:~

结果汇报到System.err

Exception in thread "main" Java.lang.RuntimeException: From f() 
	at NeverCaught.f(NeverCaught.Java:7) 
	at NeverCaught.g(NeverCaught.Java:10)
	at NeverCaught.main(NeverCaught.Java:13)

所以答案是:如果没有捕获RuntimeException,则RuntimeException要一直到主方法中,且在程序退出时为该异常调用printStackTrace()。

使用finally清除

finally后接每次都会发生的事,见下例:

try { 
	// The guarded region: Dangerous activities 
	// that might throw A, B, or C 
} catch(A a1) { 
	// Handler for situation A 
} catch(B b1) { 
	// Handler for situation B
} catch(C c1) { 
	// Handler for situation C 
} finally { 
	// Activities that happen every time 
}

finally每次都会运行

为什么要用finally?

当需要将除内存外的内容设置回其原始状态(when need to set sth other than memory back to its original state),这时就需要finally。这是一种清理方法,就像打开的文件或网络连接或者在屏幕上绘制的内容,甚至是外部世界中的交换机,见下例:

//: exceptions/Switch.java 
import static net.mindview.util.Print.*; 

public class Switch { 
	private boolean state = false; 
	public boolean read() { return state; } 
	public void on() { state = true; print(this); } 
	public void off() { state = false; print(this); } 
	public String toString() { return state ? "on" : "off"; } 
} ///:~ 

//: exceptions/OnOffException1.java 
public class OnOffException1 extends Exception {} ///:~
//: exceptions/OnOffException2.java 
public class OnOffException2 extends Exception {} ///:~ 

//: exceptions/OnOffSwitch.java 
// Why use finally? 

public class OnOffSwitch { 
	private static Switch sw = new Switch(); 
	public static void f() throws OnOffException1,OnOffException2 {} 
	public static void main(String[] args) { 
		try { 
			sw.on(); 
			// Code that can throw exceptions... 
			f(); 
			sw.off(); 
		} catch(OnOffException1 e) { 
			System.out.println("OnOffException1"); 
		} catch(OnOffException2 e) { 
		    System.out.println("OnOffException2"); 
		} finally { 
			sw.off(); 
		  }
	  } 
	} /* Output: 
	on 
	off 
	*///:~

每次都会运行finally中的sw.off(),不论有什么情况发生。

return中使用finally

既然finally子句总是会被执行,所以可以从一个方法中的多个点返回,并仍然能保证将执行重要的清理,见下例:

//: exceptions/MultipleReturns.java 
import static net.mindview.util.Print.*; 
public class MultipleReturns { 
	public static void f(int i) { print("Initialization that requires cleanup");
	try { 
		print("Point 1"); 
		if(i == 1) return; 
		print("Point 2"); 
		if(i == 2) return; 
		print("Point 3"); 
		if(i == 3) return; 
		print("End"); 
		return; 
	} finally { 
		print("Performing cleanup"); 
	 } 
} 
public static void main(String[] args) { for(int i = 1; i <= 4; i++) f(i); }
} /* Output: 
Initialization that requires cleanup 
Point 1 
Performing cleanup 
Initialization that requires cleanup 
Point 1 
Point 2 
Performing cleanup 
Initialization that requires cleanup 
Point 1 
Point 2 
Point 3 
Performing cleanup 
Initialization that requires cleanup 
Point 1 
Point 2 
Point 3 
End 
Performing cleanup 
*///:~

陷阱:丢失异常

异常有时会丢失,使用finally子句的特定配置时会发生丢失情况,见下例:

//: exceptions/LostMessage.java 
// How an exception can be lost. 
class VeryImportantException extends Exception { 
	public String toString() {
		return "A very important exception!"; 
	} 
} 
class HoHumException extends Exception { 
	public String toString() { 
		return "A trivial exception"; 
	} 
} 
public class LostMessage { 
	void f() throws VeryImportantException { 
		throw new VeryImportantException(); 
	} 
	void dispose() throws HoHumException { 
		throw new HoHumException(); 
	} 
	public static void main(String[] args) { 
		try { 
			LostMessage lm = new LostMessage(); 
			try { 
				lm.f(); 
			} finally { 
				lm.dispose(); 
			} 
		} catch(Exception e) { 
			System.out.println(e); 
		} 
	}
} /* Output: 
A trivial exception 
*///:~

上例中异常VeryImportantException丢失了。还有一种更简单的丢失异常的方式:在finally子句中用return

//: exceptions/ExceptionSilencer.java 
public class ExceptionSilencer { 
	public static void main(String[] args) { 
		try { throw new RuntimeException(); 
	} finally { 
	 // Using ‘return’ inside the finally block 
	 // will silence any thrown exception. 
	 return; 
	} 
  } 
} ///:~

异常限制

重写方法时,只能抛出在该方法的基类版本指定的异常(you can throw only the exceptions that have been specified in the base-class version of the method)。该限制很有用,因为这样的话与基类一起工作的代码将自动与从基类派生的任何对象(当然是基本OOP概念)一起工作,包括异常。见下例:

//: exceptions/StormyInning.java 
// 重写的方法只能抛出在其基类版本中定义的异常,或者从基类异常衍生的异常
class BaseballException extends Exception {} 
class Foul extends BaseballException {} 
class Strike extends BaseballException {} 
abstract class Inning { 
	public Inning() throws BaseballException {} 
public void event() throws BaseballException { 
	// Doesn’t actually have to throw anything 
} 
public abstract void atBat() throws Strike, Foul; 
public void walk() {} // 抛出非检查型的异常
} 

class StormException extends Exception {} 
class RainedOut extends StormException {} 
class PopFoul extends Foul {} 

interface Storm { 
	public void event() throws RainedOut; 
	public void rainHard() throws RainedOut; 
}

public class StormyInning extends Inning implements Storm { 
	//可以为构造方法加入新的异常,但是必须先处理基类构造方法的异常
 
public StormyInning() throws RainedOut, BaseballException {} 
public StormyInning(String s) throws Foul, BaseballException {} 
//通常方法必须遵从基类 
//! void walk() throws PopFoul {} 
//编译错误 
//接口不能为基类已存在的方法添加异常 
//! public void event() throws RainedOut {} 
//如果方法在基类中不存在,则可以添加异常,见下例 
public void rainHard() throws RainedOut {} 

//也可以选择不抛出异常,即便基类版本的方法抛出了
	public void event() {} 
	
//重写的方法抛出继承的异常 
public void atBat() throws PopFoul {} 
public static void main(String[] args) { 
	try { 
		StormyInning si = new StormyInning(); 
		si.atBat(); 
	} catch(PopFoul e) { 
		System.out.println("Pop foul"); 
	} catch(RainedOut e) { 
		System.out.println("Rained out"); 
	} catch(BaseballException e) { 
		System.out.println("Generic baseball exception"); 
	} 
	//衍生版本没有抛出Strike异常
	try { 
		//如果上溯造型会怎样? 
		Inning i = new StormyInning(); 
		i.atBat();
		//那么必须捕获该方法基类版本的异常
	} catch(Strike e) { 
		System.out.println("Strike"); 
	} catch(Foul e) { 
		System.out.println("Foul"); 
	} catch(RainedOut e) { 
		System.out.println("Rained out"); 
	} catch(BaseballException e) { 
		System.out.println("Generic baseball exception"); 
	  } 
       } 
} ///:~

注意:衍生类构造方法不能捕获基类构造方法抛出的异常

注意:不能根据异常规范(throws)重载方法。并且基类版本的方法抛出了哪些异常并不意味着子类版本的方法也要抛出这些异常。

构造方法

假设构造方法打开一个文件,而这个文件要在用户用完并调用某个特殊的清理方法才能将该文件清除时,如果在构造方法中抛出异常,那么这些清理行为可能不会正确地发生并执行。所以在写构造方法时要格外谨慎。
见下例:

//: exceptions/InputFile.java 
// 注意构造方法中的异常
import java.io.*;
public class InputFile { 
	private BufferedReader in; 
	public InputFile(String fname) throws Exception { 
		try { 
			in = new BufferedReader(new FileReader(fname)); 
			// Other code that might throw exceptions 
		} catch(FileNotFoundException e) { 
			System.out.println("Could not open " + fname); 
			// Wasn’t open, so don’t close it 
			throw e; 
		} catch(Exception e) { 
			// All other exceptions must close it 
			try { 
				in.close(); 
			} catch(IOException e2) { 
				System.out.println("in.close() unsuccessful"); 
			  } 
			 throw e; // Rethrow 
			} finally { 
				// 不要在finally中关闭
		      } 
    	  }
		public String getLine() { 
			String s; 
			try { 
				s = in.readLine(); 
			} catch(IOException e) { 
				throw new RuntimeException("readLine() failed"); 
			} 
				return s; 
			} 
			public void dispose() { 
				try { 
					in.close(); 
					System.out.println("dispose() successful"); 
				} catch(IOException e2) { 
					throw new RuntimeException("in.close() failed"); 
				} 
			} 
	} ///:~

在上例中如果FileReader构造方法失败,则抛出FileNotFoundException异常,这时候就不应该关闭文件,因为文件都没有开启。在其他catch语句中可能会关闭文件,因为进到这些catch语句的时候文件已经开启了。当然如果有多个方法抛出FileNotFoundException,事情就会变得很诡异。这时候就要切分多个try块。close()方法可能会抛出异常,即便是在另一个catch子句中也要用try-catch包裹起来。在执行完本地操作后,异常被重新抛出,这是合理的,因为构造方法失败后不要让调用的方法觉得对象已经被成功创建,可以正常使用

在此例中,finally中没有放close()方法来关闭文件,如果那么做的话每次构造方法完成都会关闭一次文件。在InputFile对象生命周期里,文件要是开启的。

应对构造方法抛出异常以及需要清理的情况,最安全的方法是使用嵌套try:

//: exceptions/Cleanup.java 
// Guaranteeing proper cleanup of a resource. 
public class Cleanup { 
	public static void main(String[] args) { 
		try { 
			InputFile in = new InputFile("Cleanup.java"); 
			try { 
				String s; 
				int i = 1; 
				while((s = in.getLine()) != null) ; 
					// Perform line-by-line processing here... 
			} catch(Exception e) { 
				System.out.println("Caught Exception in main"); 
				e.printStackTrace(System.out); 
			} finally { 
				in.dispose(); 
			 } 
			} catch(Exception e) { 
				System.out.println("InputFile construction failed"); 
			  } 
		}
} /* Output: 
dispose() successful 
*///:~

注意上例的逻辑:如果构造器失败,就进入外部的catch子句,不会调用dispose()方法。紧跟构造方法后创建新的try。finally中执行的清除方法和内部try相关联;如果构造方法失败,finally不会执行,如果构造方法成功,finally总是会执行

就算构造方法不抛出异常也仍然要使用这种清理语句。即:在创建需要清理的对象后,立马开始try-finally
见下例:

//: exceptions/CleanupIdiom.java 
// Each disposable object must be followed by a try-finally
class NeedsCleanup { // 不会失败的构造方法
	private static long counter = 1; 
	private final long id = counter++; 
	public void dispose() { 
		System.out.println("NeedsCleanup " + id + " disposed"); 
	} 
} 

class ConstructionException extends Exception {} 

class NeedsCleanup2 extends NeedsCleanup { 
	// 会失败的构造方法
	public NeedsCleanup2() throws ConstructionException {} 
}

public class CleanupIdiom { 
	public static void main(String[] args) { 
		// Section 1: 
		NeedsCleanup nc1 = new NeedsCleanup();  
		try {   // 既然该构造方法不会失败,也就不需要catch去捕获异常
			// ... 
		} finally { 
			nc1.dispose(); 
		} 

// Section 2: 
// 构造方法不会失败,可以放在一起创建对象
NeedsCleanup nc2 = new NeedsCleanup(); 
NeedsCleanup nc3 = new NeedsCleanup(); 
try { 
	// ... 
} finally { 
	nc3.dispose(); // Reverse order of construction 
	nc2.dispose(); 
}

// Section 3: // 构造方法会失败,创建对象时要分开放在各自的try-finally中
try { 
	NeedsCleanup2 nc4 = new NeedsCleanup2(); 
	try { 
		NeedsCleanup2 nc5 = new NeedsCleanup2(); 
		try { 
			// ... 
			} finally { 
				nc5.dispose(); 
			  } 
		 } catch(ConstructionException e) { // nc5 constructor 
			System.out.println(e); 
		 } finally { 
			nc4.dispose(); 
		  } 
	  } catch(ConstructionException e) { // nc4 constructor 
			System.out.println(e); 
        } 
    }
} /* Output:
NeedsCleanup 1 disposed 
NeedsCleanup 3 disposed 
NeedsCleanup 2 disposed 
NeedsCleanup 5 disposed 
NeedsCleanup 4 disposed 
*///:~

注意如果dispose()会抛出异常,那么要为dispose()添加额外的try。

异常匹配(Exception matching)

抛出异常时,异常处理系统会按写入的顺序依次检索“最靠前”的处理程序。找到匹配项时,该异常被视为已处理,不会再进一步搜索。

匹配异常并不要求异常与其处理程序完全匹配。派生类对象会和基类的处理程序匹配,见下例:

//: exceptions/Human.java 
// Catching exception hierarchies. 

class Annoyance extends Exception {} 
class Sneeze extends Annoyance {} 

public class Human { 
	public static void main(String[] args) { 
		// Catch the exact type: 
		try { 
			throw new Sneeze(); 
		} catch(Sneeze s) { 
			System.out.println("Caught Sneeze"); 
		} catch(Annoyance a) { 
			System.out.println("Caught Annoyance");
		} 
		// Catch the base type: 
		try { 
			throw new Sneeze(); 
		} catch(Annoyance a) { 
			System.out.println("Caught Annoyance"); 
		  } 
		} 
} /* Output: 
Caught Sneeze 
Caught Annoyance 
*///:~

catch(Annoyance a)将捕获Annoyance或其衍生类。这样的话,如果向方法中添加更多派生的异常,那么只要客户机捕获基类异常,客户机程序员的代码就不需要更改。

如果是下面这样的代码:

try { 
	throw new Sneeze(); 
} catch(Annoyance a) { 
	// ... 
} catch(Sneeze s) { 
	// ... 
}

编译器会报错,因为编译器看到了Sneeze的catch子句却不能访问

当你发现有些检查型异常在前面挡路,或者特别是被强制要抛出某些异常时,而你不知道该怎么处理这些异常的时候,下面有几种方法:

将异常传递到控制台
在简单的小型程序里,可以通过主方法将异常传递到控制台中,见下例:

//: exceptions/MainException.java 
import java.io.*; 
public class MainException { 
	// Pass all exceptions to the console: 
	public static void main(String[] args) throws Exception { //主方法抛出异常
		// Open the file: 
		FileInputStream file = new FileInputStream("MainException.java"); 
		// Use the file ... 
		// Close the file: 
		file.close(); 
	} 
} ///:~

注意:主方法抛出的是Exception异常

将检查型异常转换成非检查型异常
主方法抛出异常在小型项目中可行,但是当你在主方法方法体中调用另一个方法时会发现“我不知道该怎么处理这个异常,但是我不想这个异常被吞掉或者输出没什么用的信息“。那么通过使用异常链,只需要把检查型异常传递到RuntimeException的构造方法中,从而将检查行异常包裹在RuntimeException中即可:

try { 
	// ... to do something useful 
} catch(IDontKnowWhatToDoWithThisCheckedException e) { 
	throw new RuntimeException(e); }

通过这种技术,可以关闭检查型异常,不会吞掉异常,不用通过throw抛出,而且使用了异常链不会丢掉原始异常的任何信息。

通过该技术可以忽略异常并在不使用try/catch或throw的情况下让异常冒泡到调用堆栈中。但是仍然可以通过使用getCause()来捕获和处理具体的异常:

//: exceptions/TurnOffChecking.java 
// "Turning off" Checked exceptions. 
import java.io.*; 
import static net.mindview.util.Print.*; 

class WrapCheckedException { 
	void throwRuntimeException(int type) { 
		try { 
			switch(type) { 
				case 0: throw new FileNotFoundException(); 
				case 1: throw new IOException(); 
				case 2: throw new RuntimeException("Where am I?"); 
				default: return; 
		    } 
		  } catch(Exception e) { // Adapt to unchecked: 
			throw new RuntimeException(e); 
			} 
		} 
} 

class SomeOtherException extends Exception {} 

public class TurnOffChecking { 
	public static void main(String[] args) { 
		WrapCheckedException wce = new WrapCheckedException(); 
		// block, and let RuntimeExceptions leave the method: 	
		wce.throwRuntimeException(3); 
		// Or you can choose to catch exceptions: 
		for(int i = 0; i < 4; i++) 
			try { 
				if(i < 3) 
					wce.throwRuntimeException(i); 
				else 
					throw new SomeOtherException(); 
			   } catch(SomeOtherException e) { 
					print("SomeOtherException: " + e); 
			   } catch(RuntimeException re) { 
					try { 
						throw re.getCause();  //注意re是RuntimeException
					   } catch(FileNotFoundException e) { 
						print("FileNotFoundException: " + e); 
					   } catch(IOException e) { 
						print("IOException: " + e); 
					   } catch(Throwable e) { 
						print("Throwable: " + e); 
						} 
				} 
		} 
} /* Output: 
FileNotFoundException: java.io.FileNotFoundException 
IOException: java.io.IOException 
Throwable: java.lang.RuntimeException: Where am I? 
SomeOtherException: SomeOtherException 
*///:~

Exception guidelines
Use exceptions to:

  1. Handle problems at the appropriate level. (Avoid catching exceptions unless you know what to do with them.)
  2. Fix the problem and call the method that caused the exception again.
  3. Patch things up and continue without retrying the method.
  4. Calculate some alternative result instead of what the method was supposed to produce.
  5. Do whatever you can in the current context and rethrow the same exception to a higher context.
  6. Do whatever you can in the current context and throw a different exception to a higher context.
  7. Terminate the program.
  8. Simplify. (If your exception scheme makes things more complicated, then it is painful and annoying to use.)
  9. Make your library and program safer. (This is a short-term investment for debugging, and a long-term investment for application robustness.)

你可能感兴趣的:(每天学一点Thinking,in,java)