为违例编写代码时我们经常要解决的一个问题是一旦产生违例会正确地进行清除吗?大多数时候都会非常安全但在构建器中却是一个大问题构建器将对象置于一个安全的起始状态但它可能执行一些操作——如打开一个文件除非用户完成对象的使用并调用一个特殊的清除方法否则那些操作不会得到正确的清除若从一个构建器内部掷出一个违例这些清除行为也可能不会正确地发生所有这些都意味着在编写构建器时我们必须特别加以留意
由于前面刚学了finally所以大家可能认为它是一种合适的方案但事情并没有这么简单因为finally每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码因此假如真的用finally进行清除必须在构建器正常结束时设置某种形式的标志而且只要设置了标志就不要执行finally块内的任何东西由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来)所以除非特别需要否则一般不要尝试在finally中进行这种形式的清除
在下面这个例子里我们创建了一个名为InputFile的类它的作用是打开一个文件然后每次读取它的一行内容(转换为一个字串)它利用了由Java标准IO库提供的FileReader以及BufferedReader类(将于第章讨论)这两个类都非常简单大家现在可以毫无困难地掌握它们的基本用法
//: Cleanupjava
// Paying attention to exceptions
// in constructors
import javaio*;
class InputFile {
private BufferedReader in;
InputFile(String fname) throws Exception {
try {
in =
new BufferedReader(
new FileReader(fname));
// Other code that might throw exceptions
} catch(FileNotFoundException e) {
Systemoutprintln(
Could not open + fname);
// Wasnt open so dont close it
throw e;
} catch(Exception e) {
// All other exceptions must close it
try {
inclose();
} catch(IOException e) {
Systemoutprintln(
inclose() unsuccessful);
}
throw e;
} finally {
// Dont close it here!!!
}
}
String getLine() {
String s;
try {
s = inreadLine();
} catch(IOException e) {
Systemoutprintln(
readLine() unsuccessful);
s = failed;
}
return s;
}
void cleanup() {
try {
inclose();
} catch(IOException e) {
Systemoutprintln(
inclose() unsuccessful);
}
}
}
public class Cleanup {
public static void main(String[] args) {
try {
InputFile in =
new InputFile(Cleanupjava);
String s;
int i = ;
while((s = ingetLine()) != null)
Systemoutprintln(+ i++ + : + s);
incleanup();
} catch(Exception e) {
Systemoutprintln(
Caught in main eprintStackTrace());
eprintStackTrace();
}
}
} ///:~
该例使用了Java IO类
用于InputFile的构建器采用了一个String(字串)参数它代表我们想打开的那个文件的名字在一个try块内部它用该文件名创建了一个FileReader对FileReader来说除非转移并用它创建一个能够实际与之交谈的BufferedReader否则便没什么用处注意InputFile的一个好处就是它同时合并了这两种行动
若FileReader构建器不成功就会产生一个FileNotFoundException(文件未找到违例)必须单独捕获这个违例——这属于我们不想关闭文件的一种特殊情况因为文件尚未成功打开其他任何捕获从句(catch)都必须关闭文件因为文件已在进入那些捕获从句时打开(当然如果多个方法都能产生一个FileNotFoundException违例就需要稍微用一些技巧此时我们可将不同的情况分隔到数个try块内)close()方法会掷出一个尝试过的违例即使它在另一个catch从句的代码块内该违例也会得以捕获——对Java编译器来说那个catch从句不过是另一对花括号而已执行完本地操作后违例会被重新掷出这样做是必要的因为这个构建器的执行已经失败我们不希望调用方法来假设对象已正确创建以及有效
在这个例子中没有采用前述的标志技术finally从句显然不是关闭文件的正确地方因为这可能在每次构建器结束的时候关闭它由于我们希望文件在InputFile对象处于活动状态时一直保持打开状态所以这样做并不恰当
getLine()方法会返回一个字串其中包含了文件中下一行的内容它调用了readLine()后者可能产生一个违例但那个违例会被捕获使getLine()不会再产生任何违例对违例来说一项特别的设计问题是决定在这一级完全控制一个违例还是进行部分控制并传递相同(或不同)的违例或者只是简单地传递它在适当的时候简单地传递可极大简化我们的编码工作getLine()方法会变成
String getLine() throws IOException {
return inreadLine();
}
但是当然调用者现在需要对可能产生的任何IOException进行控制
用户使用完毕InputFile对象后必须调用cleanup()方法以便释放由BufferedReader以及/或者FileReader占用的系统资源(如文件句柄)——注释⑥除非InputFile对象使用完毕而且到了需要弃之不用的时候否则不应进行清除大家可能想把这样的机制置入一个finalize()方法内但正如第章指出的那样并非总能保证finalize()获得正确的调用(即便确定它会调用也不知道何时开始)这属于Java的一项缺陷——除内存清除之外的所有清除都不会自动进行所以必须知会客户程序员告诉他们有责任用finalize()保证清除工作的正确进行
⑥在C++里破坏器可帮我们控制这一局面
在Cleanupjava中我们创建了一个InputFile用它打开用于创建程序的相同的源文件同时一次读取该文件的一行内容而且添加相应的行号所有违例都会在main()中被捕获——尽管我们可选择更大的可靠性
这个示例也向大家展示了为何在本书的这个地方引入违例的概念违例与Java的编程具有很高的集成度这主要是由于编译器会强制它们只有知道了如何操作那些违例才可更进一步地掌握编译器的知识