写之前还是觉得多数人对异常的理解太偏而且在带有智能的ide下,根本不关注异常处理,实际上,在一个重量级的项目下,每一个异常都很重要,因为异常也是new在堆上的,这是消耗内存的,并且还要在栈上做文章,本身异常处理就是一笔不小的开销。
发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前。然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。
发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前。然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。
try { // Code that might generate exceptions } catch(Type1 id1) { // Handle exceptions of Type1 } catch(Type2 id2) { // Handle exceptions of Type2 } catch(Type3 id3) { // Handle exceptions of Type3 } // etc.
异常处理程序必须紧跟在 try 块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。注意,只有匹配的 catch 子句才能得到执行;这与 switch 语句不同,switch 语句需要在每一个 case 后面跟一个 break,以避免执行后续的 case 子句。
注意在 try 块的内部,许多不同的方法调用可能会产生类型相同的异常,而你只需要提供一个针对此类型的异常处理程序。
异常处理模型有两种,终止和恢复。
终止就是error级别的,一旦产生就程序就不会再回来执行了。
恢复这种会自动认为第二次会成功,所以会继续执行代码,比如我们将try放到while里,它就会不停的尝试。
实际上,第二种恢复模型很奇怪,是非是一种goto的变种实现,不常用。
自定义异常,这在业务场景里用的很多,实际上本人看来,主要是异常类名。
// exceptions/FullConstructors.java class MyException extends Exception { MyException() {} 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); } } }
异常与记录日志
// exceptions/LoggingExceptions.java // An exception that reports through a Logger // {ErrorOutputExpected} import java.util.logging.*; import java.io.*; class LoggingException extends Exception { private static Logger logger = Logger.getLogger("LoggingException"); 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); } } }
___[ Error Output ]___ May 09, 2017 6:07:17 AM LoggingException <init> SEVERE: LoggingException at LoggingExceptions.main(LoggingExceptions.java:20) Caught LoggingException May 09, 2017 6:07:17 AM LoggingException <init> SEVERE: LoggingException at LoggingExceptions.main(LoggingExceptions.java:25) Caught LoggingException
为了产生日志记录消息,我们欲获取异常抛出处的栈轨迹,但是 printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一个 java.io.PrintWriter 对象作为参数
代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java 在编译时就可以保证一定水平的异常正确性。
这种在编译时被强制检查的异常称为被检查的异常。
捕获所有异常
catch(Exception e) { System.out.println("Caught an exception"); }
这将捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常捕获了。
因为 Exception 是与编程有关的所有异常类的基类,所以它不会含有太多具体的信息,不过可以调用它从其基类 Throwable 继承的方法:
String getMessage()
String getLocalizedMessage()
多重捕获
// exceptions/SameHandler.java class EBase1 extends Exception {} class Except1 extends EBase1 {} class EBase2 extends Exception {} class Except2 extends EBase2 {} class EBase3 extends Exception {} class Except3 extends EBase3 {} class EBase4 extends Exception {} class Except4 extends EBase4 {} public class SameHandler { void x() throws Except1, Except2, Except3, Except4 {} void process() {} void f() { try { x(); } catch(Except1 e) { process(); } catch(Except2 e) { process(); } catch(Except3 e) { process(); } catch(Except4 e) { process(); } } }
jdk7以后可以这么写了
// exceptions/MultiCatch.java public class MultiCatch { void x() throws Except1, Except2, Except3, Except4 {} void process() {} void f() { try { x(); } catch(Except1 | Except2 | Except3 | Except4 e) { process(); } } }
// exceptions/MultiCatch2.java public class MultiCatch2 { void x() throws Except1, Except2, Except3, Except4 {} void process1() {} void process2() {} void f() { try { x(); } catch(Except1 | Except2 e) { process1(); } catch(Except3 | Except4 e) { process2(); } } }
栈轨迹,这边就是一个消耗性能的地方,它需要按照你的调用将方法压栈
// 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(); } }
输出
f main ******* f g main ******* f g h main
精准的重抛异常
class BaseException extends Exception {} class DerivedException extends BaseException {} public class PreciseRethrow { void catcher() throws DerivedException { try { throw new DerivedException(); } catch(BaseException e) { throw e; } } }
这里已经抛出DerivedException但是仍然还要抛出更精确的BaseException
异常链
常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。在 JDK1.4 以前,程序员必须自己编写代码来保存原始异常的信息。现在所有 Throwable 的子类在构造器中都可以接受一个 cause(因由)对象作为参数。这个 cause 就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。
运行时的异常RuntimeException
属于运行时异常的类型有很多,它们会自动被 java 虚拟机抛出,所以不必在异常说明中把它们列出来。这些异常都是从 RuntimeException 类继承而来,所以既体现了继承的优点,使用起来也很方便。这构成了一组具有相同特征和行为的异常类型。并且,也不再需要在异常说明中声明方法将抛出 RuntimeException 类型的异常(或者任何从 RuntimeException 继承的异常),它们也被称为“不受检查异常”。这种异常属于错误,将被自动捕获,就不用你亲自动手了。要是自己去检查 RuntimeException 的话,代码就显得太混乱了。不过尽管通常不用捕获 RuntimeException 异常,但还是可以在代码中抛出 RuntimeException 类型的异常。
- 无法预料的错误。比如从你控制范围之外传递进来的 null 引用。
- 作为程序员,应该在代码中进行检查的错误。(比如对于 ArrayIndexOutOfBoundsException,就得注意一下数组的大小了。)在一个地方发生的异常,常常会在另一个地方导致错误。
// 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(); } }
我们可以用来当作一种检查运行时的方式
finally
什么时候必须用finally?
当要把除内存之外的资源恢复到它们的初始状态时,就要用到 finally 子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关,如下面例子所示:
// exceptions/Switch.java public class Switch { private boolean state = false; public boolean read() { return state; } public void on() { state = true; System.out.println(this); } public void off() { state = false; System.out.println(this); } @Override 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"); sw.off(); } catch(OnOffException2 e) { System.out.println("OnOffException2"); sw.off(); } } }
这种没有finally的处理,仅靠单纯的逻辑处理有些冗余且不保险,异常被抛出,但没被处理程序捕获,这时 sw.off() 就得不到调用。但是有了 finally,只要把 try 块中的清理代码移放在一处即可:
// exceptions/WithFinally.java // Finally Guarantees cleanup public class WithFinally { static Switch sw = new Switch(); public static void main(String[] args) { try { sw.on(); // Code that can throw exceptions... OnOffSwitch.f(); } catch(OnOffException1 e) { System.out.println("OnOffException1"); } catch(OnOffException2 e) { System.out.println("OnOffException2"); } finally { sw.off(); } } }
输出
on
off
return和finally
// exceptions/MultipleReturns.java public class MultipleReturns { public static void f(int i) { System.out.println( "Initialization that requires cleanup"); try { System.out.println("Point 1"); if(i == 1) return; System.out.println("Point 2"); if(i == 2) return; System.out.println("Point 3"); if(i == 3) return; System.out.println("End"); return; } finally { System.out.println("Performing cleanup"); } } public static void main(String[] args) { for(int i = 1; i <= 4; i++) f(i); } }
输出
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
异常丢失
// 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; } } }
这个直接不会处理异常
finally的错误使用也会导致异常缺失
// exceptions/LostMessage.java // How an exception can be lost class VeryImportantException extends Exception { @Override public String toString() { return "A very important exception!"; } } class HoHumException extends Exception { @Override 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(VeryImportantException | HoHumException e) { System.out.println(e); } } }
输出
A trivial exception
这里完全被finally里的压制了,典型的上一个异常没处理直接到下一个了。
异常限制
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,若当基类使用的代码应用到其派生类对象的时候,一样能够工作(当然,这是面向对象的基本概念),异常也不例外。
构造器
有一点很重要,即你要时刻询问自己“如果异常发生了,所有东西能被正确的清理吗?"尽管大多数情况下是非常安全的,但涉及构造器时,问题就出现了。构造器会把对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理。如果在构造器内抛出了异常,这些清理行为也许就不能正常工作了。这意味着在编写构造器时要格外细心。
你也许会认为使用 finally 就可以解决问题。但问题并非如此简单,因为 finally 会每次都执行清理代码。如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在 finaly 子句中却是要被清理的。
在下面的例子中,建立了一个 InputFile 类,它能打开一个文件并且每次读取其中的一行。这里使用了 Java 标准输入/输出库中的 FileReader 和 BufferedReader 类,这些类的基本用法很简单,你应该很容易明白:
// 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"); } } }
嵌套try会更安全的保证的你的文件是打开状态下的操作。
dispose() successful
请仔细观察这里的逻辑:对 InputFile 对象的构造在其自己的 try 语句块中有效,如果构造失败,将进入外部的 catch 子句,而 dispose() 方法不会被调用。但是,如果构造成功,我们肯定想确保对象能够被清理,因此在构造之后立即创建了一个新的 try 语句块。执行清理的 finally 与内部的 try 语句块相关联。在这种方式中,finally 子句在构造失败时是不会执行的,而在构造成功时将总是执行。
这种通用的清理惯用法在构造器不抛出任何异常时也应该运用,其基本规则是:在创建需要清理的对象之后,立即进入一个 try-finally 语句块:之后的意思是创建完成了。
// exceptions/CleanupIdiom.java // Disposable objects must be followed by a try-finally class NeedsCleanup { // Construction can't fail 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 { // Construction can fail: NeedsCleanup2() throws ConstructionException {} } public class CleanupIdiom { public static void main(String[] args) { // [1]: NeedsCleanup nc1 = new NeedsCleanup(); try { // ... } finally { nc1.dispose(); } // [2]: // If construction cannot fail, // you can group objects: NeedsCleanup nc2 = new NeedsCleanup(); NeedsCleanup nc3 = new NeedsCleanup(); try { // ... } finally { nc3.dispose(); // Reverse order of construction nc2.dispose(); } // [3]: // If construction can fail you must guard each one: try { NeedsCleanup2 nc4 = new NeedsCleanup2(); try { NeedsCleanup2 nc5 = new NeedsCleanup2(); try { // ... } finally { nc5.dispose(); } } catch(ConstructionException e) { // nc5 const. System.out.println(e); } finally { nc4.dispose(); } } catch(ConstructionException e) { // nc4 const. System.out.println(e); } } }
NeedsCleanup 1 disposed NeedsCleanup 3 disposed NeedsCleanup 2 disposed NeedsCleanup 5 disposed NeedsCleanup 4 disposed
InputFile.java 一个更好的实现方式是如果构造函数读取文件并在内部缓冲它 —— 这样,文件的打开,读取和关闭都发生在构造函数中。或者,如果读取和存储文件不切实际,你可以改为生成 Stream。理想情况下,你可以设计成如下的样子:
// exceptions/InputFile2.java import java.io.*; import java.nio.file.*; import java.util.stream.*; public class InputFile2 { private String fname; public InputFile2(String fname) { this.fname = fname; } public Stream<String> getLines() throws IOException { return Files.lines(Paths.get(fname)); } public static void main(String[] args) throws IOException { new InputFile2("InputFile2.java").getLines() .skip(15) .limit(1) .forEach(System.out::println); } }
输出
main(String[] args) throws IOException {
// exceptions/MessyExceptions.java import java.io.*; public class MessyExceptions { public static void main(String[] args) { InputStream in = null; try { in = new FileInputStream( new File("MessyExceptions.java")); int contents = in.read(); // Process contents } catch(IOException e) { // Handle the error } finally { if(in != null) { try { in.close(); } catch(IOException e) { // Handle the close() error } } } } }
上面这种属于很复杂的写法,我们过分的将重点放到了文件的开启状态了,实际上应该交给jdk内部机制 try-with-resource
// exceptions/TryWithResources.java import java.io.*; public class TryWithResources { public static void main(String[] args) { try( InputStream in = new FileInputStream( new File("TryWithResources.java")) ) { int contents = in.read(); // Process contents } catch(IOException e) { // Handle the error } } }
// exceptions/StreamsAreAutoCloseable.java import java.io.*; import java.nio.file.*; import java.util.stream.*; public class StreamsAreAutoCloseable { public static void main(String[] args) throws IOException{ try( Stream<String> in = Files.lines( Paths.get("StreamsAreAutoCloseable.java")); PrintWriter outfile = new PrintWriter( "Results.txt"); // [1] ) { in.skip(5) .limit(1) .map(String::toLowerCase) .forEachOrdered(outfile::println); } // [2] } }
- [1] 你在这里可以看到其他的特性:资源规范头中可以包含多个定义,并且通过分号进行分割(最后一个分号是可选的)。规范头中定义的每个对象都会在 try 语句块运行结束之后调用 close() 方法。
- [2] try-with-resources 里面的 try 语句块可以不包含 catch 或者 finally 语句而独立存在。在这里,IOException 被 main() 方法抛出,所以这里并不需要在 try 后面跟着一个 catch 语句块。
Java 5 中的 Closeable 已经被修改,修改之后的接口继承了 AutoCloseable 接口。所以所有实现了 Closeable 接口的对象,都支持了 try-with-resources 特性。
try-with-resource机制
// exceptions/AutoCloseableDetails.java class Reporter implements AutoCloseable { String name = getClass().getSimpleName(); Reporter() { System.out.println("Creating " + name); } public void close() { System.out.println("Closing " + name); } } class First extends Reporter {} class Second extends Reporter {} public class AutoCloseableDetails { public static void main(String[] args) { try( First f = new First(); Second s = new Second() ) { } } }
输出
Creating First
Creating Second
Closing Second
Closing First
如果其中一个构造函数抛出异常
// exceptions/ConstructorException.java class CE extends Exception {} class SecondExcept extends Reporter { SecondExcept() throws CE { super(); throw new CE(); } } public class ConstructorException { public static void main(String[] args) { try( First f = new First(); SecondExcept s = new SecondExcept(); Second s2 = new Second() ) { System.out.println("In body"); } catch(CE e) { System.out.println("Caught: " + e); } } }
输出
Creating First
Creating SecondExcept
Closing First
Caught: CE
正如预期的那样,First 创建时没有发生意外,SecondExcept 在创建期间抛出异常。请注意,不会为 SecondExcept 调用 close(),因为如果构造函数失败,则无法假设你可以安全地对该对象执行任何操作,包括关闭它。由于 SecondExcept 的异常,Second 对象实例 s2 不会被创建,因此也不会有清除事件发生。
如果没有构造函数抛出异常,但你可能会在 try 的主体中获取它们,则再次强制你实现 catch 子句:
// exceptions/BodyException.java class Third extends Reporter {} public class BodyException { public static void main(String[] args) { try( First f = new First(); Second s2 = new Second() ) { System.out.println("In body"); Third t = new Third(); new SecondExcept(); System.out.println("End of body"); } catch(CE e) { System.out.println("Caught: " + e); } } }
输出为
Creating First
Creating Second
In body
Creating Third
Creating SecondExcept
Closing Second
Closing First
Caught: CE
请注意,第 3 个对象永远不会被清除。那是因为它不是在资源规范头中创建的,所以它没有被保护。这很重要,因为 Java 在这里没有以警告或错误的形式提供指导,因此像这样的错误很容易漏掉。实际上,如果依赖某些集成开发环境来自动重写代码,以使用 try-with-resources 特性,那么它们(在撰写本文时)通常只会保护它们遇到的第一个对象,而忽略其余的对象。
最后,让我们看一下抛出异常的 close() 方法:
// exceptions/CloseExceptions.java class CloseException extends Exception {} class Reporter2 implements AutoCloseable { String name = getClass().getSimpleName(); Reporter2() { System.out.println("Creating " + name); } public void close() throws CloseException { System.out.println("Closing " + name); } } class Closer extends Reporter2 { @Override public void close() throws CloseException { super.close(); throw new CloseException(); } } public class CloseExceptions { public static void main(String[] args) { try( First f = new First(); Closer c = new Closer(); Second s = new Second() ) { System.out.println("In body"); } catch(CloseException e) { System.out.println("Caught: " + e); } } }
输出
// exceptions/CloseExceptions.java class CloseException extends Exception {} class Reporter2 implements AutoCloseable { String name = getClass().getSimpleName(); Reporter2() { System.out.println("Creating " + name); } public void close() throws CloseException { System.out.println("Closing " + name); } } class Closer extends Reporter2 { @Override public void close() throws CloseException { super.close(); throw new CloseException(); } } public class CloseExceptions { public static void main(String[] args) { try( First f = new First(); Closer c = new Closer(); Second s = new Second() ) { System.out.println("In body"); } catch(CloseException e) { System.out.println("Caught: " + e); } } }
异常匹配
// 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"); } } }
输出
Caught Sneeze
Caught Annoyance
尽量还是下级异常匹配
异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常"。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样一来,主要代码就不会与错误处理逻辑混在一起,也更容易理解和维护。通过允许一个处理程序去处理多个出错点,异常处理还使得错误处理代码的数量趋于减少。
“被检查的异常”使这个问题变得有些复杂,因为它们强制你在可能还没准备好处理错误的时候被迫加上 catch 子句,这就导致了吞食则有害(harmful if swallowed)的问题:
过于慎重就导致问题的复杂性增加。被检查异常真的又麻烦又多余,至少我这样认为。
被检查到不检查异常
// exceptions/TurnOffChecking.java // "Turning off" Checked exceptions import java.io.*; 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(IOException | RuntimeException 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(); // You can call throwRuntimeException() without // a try 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) { System.out.println( "SomeOtherException: " + e); } catch(RuntimeException re) { try { throw re.getCause(); } catch(FileNotFoundException e) { System.out.println( "FileNotFoundException: " + e); } catch(IOException e) { System.out.println("IOException: " + e); } catch(Throwable e) { System.out.println("Throwable: " + e); } } } }
这种方式是第一次接触
- 尽可能使用 try-with-resource。
- 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
- 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)