Thinking in Java,Fourth Edition(Java 编程思想,第四版)学习笔记(十二)之Error Handling with Exceptions

  The ideal time to catch an error is at compile time, before you even try to run the program. However, not all errors can be detected at compile time.

  To create a robust system, each component must be robust.

  By providing a consistent error-reporting model using exceptions, Java allows components to reliably communicate problems to client code.

Concepts

  C and other earlier languages often had multiple error-handling schemes, and these were generally established by convention and not as part of the programming language. Typically you returned a special value or set a flag, and the recipient was supposed to look at the value of the flag and determine that something was amiss.

  This approach to handling errors was a major limitation to creating large, robust, maintainable programs.

  The solution is to take the casual nature out of error handling and to enforce formality.

  At the point where the problem occurs, you might not know what to do with it, but you do know that you can't just continue on merrily; you must stop, and somebody, somewhere, must figure out what to do. But you don't have enough information in the current context to fix the problem. So you hand the problem out to a higher context where someone is qualified to make the proper decision.

  The other rather significant benefit of exceptions is that they tend to reduce the complexity of error-handling code.

  Without exceptions, you must check for a particular error and deal with it at multiple places in your program

Basic exceptions

  It's important to distinguish an exceptional condition from a normal problem, in which you have enough information in the current context to somehow cope with the difficulty.

  When you throw an exception, several things happen:

    1. the exception object is created in the same way that any Java Object is created: on the heap, with new.

    2. the current path of execution is stopped and the reference for the exception object is ejected from the current context

    3. At this point the exception-handling mechanism takes over and begins to look for an appropriate place to continue executing the problem. This appropriate is the exception handler.

Catching an exception

  a guarded region: a section of code that might produce exceptions and is followed by the code to handle those exceptions.

  The try block

  try {

    // Code that might generate exceptions

  }

  WIth exception handling, you put everything in a try block and capture all the exceptions in one place. This means your code is much easier to write and read because the goal of the code is not confused with the error checking.

  Exception handlers

  Exception handlers immediately follow the try block and are denoted by the keyword catch.

  try {

    // Code that might generate exceptions

  } catch(Type1 id1) {

    // Handle exceptions of Type1

  } catch(Type2 id2) {

    // Handle exceptions of Type2

  } catch(Typen idn) {

    // Handle exceptions of Typen

  }

  Each catch clause (exception handler) is like a little method that takes one and only one argument of a particular type.The identifier (id1, id2, and so on) can be used inside the handler, just like a method argument. Sometimes you never use the identifier because the type of the exception gives you enough information to deal with the exception, but the identifier must still be there.

  If an exception is thrown, the exception-handling mechanism goes hunting for the first handler with an argument that matches the type of the exception. Then it enters that catch clause, and the exception is considered handled.The search for handlers stops once the catch clause is finished.

  Note that within the try block, a number of different method calls might generate the same exception, but you need only one handler.

  Termination vs. resumption

  If you want resumption-like behavior in Java, don’t throw an exception when you encounter an error. Instead, call a method that fixes the problem. Alternatively, place your try block inside a while loop that keeps reentering the try block until the result is satisfactory.

  So although resumption sounds attractive at first, it isn’t quite so useful in practice.The dominant reason is probably the coupling that results: A resumptive handler would need to be aware of where the exception is thrown, and contain non-generic code specific to the throwing location. This makes the code difficult to write and maintain, especially for large systems where the exception can be generated from many points.

Creating your own exceptions

  The most trivial way to create a new type of exception is just to let the compiler create the default constructor for you, so it requires almost no code at all

  class SimpleException extends Exception {}

  in this case you don't get a SimpleException(String) constructor, but in practice that isn't used much (??)

  The most important thing about an exception is the class name, so most of the time an exception like the one shown here is satisfactory.

  System.err is usually a better place to send error information than System.out, which may be redirected. If you send output to System.err, it will not be redirected along with System.out so the user is more liked to notice it.

  class MyException extends Exception {

    public MyException() {}

    public MyException(String msg) { super(msg); }

  }

  e.printStackTrace(System.out);

  e.printStackTrace(); //自动输出到System.err

  Exceptions and logging

  java.util.logging

  

  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());

    }

  }

  The static Logger.getLogger() method creates a Logger object associated with the String argument (usually the same of the package and class that the errors are about) which sends its output to System.err. The easiest way to write to a Logger is just to call the method associated with the level of logging message; here, severe( ) is used.

  Although the approach used by LoggingException is very convenient because it builds all the logging infrastructure into the exception itself, and thus it works automatically without client programmer intervention, it's more common that you will be catching and logging someone else's exception, so you must generate the log message in the exception handler. 

public class LoggingExceptions2 {
  private static Logger logger =
    Logger.getLogger("LoggingExceptions2");
  static void logException(Exception e) { // static 方法
    StringWriter trace = new StringWriter();
    e.printStackTrace(new PrintWriter(trace));
    logger.severe(trace.toString());
  }
  public static void main(String[] args) {
    try {
      throw new NullPointerException();
    } catch(NullPointerException e) {
      logException(e); // 调用
    }
  }
}

The process of creating your own exceptions can be taken further. 

class MyException2 extends Exception {
  private int x;
  public MyException2() {}
  public MyException2(String msg) { super(msg); }
  public MyException2(String msg, int x) {
    super(msg);
    this.x = x;
  }
  public int val() { return x; }
  public String getMessage() {
    return "Detail Message: "+ x + " "+ super.getMessage();
  }
}

// 输出例子:  

  MyException2: Detail Message: 0 Originated in g()
    at ExtraFeatures.g(ExtraFeatures.java:26)
    at ExtraFeatures.main(ExtraFeatures.java:39)

  getMessage( ) is something like toString( ) for exception classes.

  Keep in mind, however, that all this dressing-up might be lost on the client programmers using your packages, since they might simply look for the exception to be thrown and nothing more. (That’s the way most of the Java library exceptions are used.)

The exception specification

  void f() throws TooBig, TooSmall, DivZero { //...
  However, if you say
  void f() { //...
  it means that no exceptions are thrown from the method (except for the exceptions inherited from RuntimeException, which can be thrown anywhere without exception specifications—these will be described later).

  There is one place you can lie: You can claim to throw an exception that you really don’t.

  This has the beneficial effect of being a placeholder for that exception, so you can actually start throwing the exception later without requiring changes to existing code. It’s also important for creating abstract base classes and interfaces whose derived classes or implementations may need to throw exceptions.

  Exceptions that are checked and enforced at compile time are called checked exceptions.

Catching any exception

  there are other types of base exceptions, but Exception is the base that’s pertinent to virtually all programming activities).

  catch(Exception e) {

    // ...

  }

  if you use it you’ll want to put it at the end of your list of handlers to avoid preempting any exception handlers that might otherwise follow it.

  The stack trace

  The information provided by printStackTrace( ) can also be accessed directly using getStackTrace( ). This method returns an array of stack trace elements, each representing one stack frame. Element zero is the top of the stack, and is the last method invocation in the sequence

  Rethrowing an exception

  catch (Exception e) {

    // ....

    throw e;

  }

  If you simply rethrow the current exception, the information that you print about that exception in printStackTrace( ) will pertain to the exception’s origin, not the place where you rethrow it. If you want to install new stack trace information, you can do so by calling fillInStackTrace( ), which returns a Throwable object that it creates by stuffing the current stack information into the old exception object.

  It’s also possible to rethrow a different exception from the one you caught. If you do this, you get a similar effect as when you use fillInStackTrace( )

  Exception chaining

  Often you want to catch one exception and throw another, but still keep the information about the originating exception—this is called exception chaining.

  all Throwable subclasses have the option to take a cause object in their constructor. The cause is intended to be the originating exception, and by passing it in you maintain the stack trace back to its origin, even though you’re creating and throwing a new exception.

  It’s interesting to note that the only Throwable subclasses that provide the cause argument in the constructor are the three fundamental exception classes Error (used by the JVM to report system errors), Exception, and RuntimeException. If you want to chain any other exception types, you do it through the initCause( ) method rather than the constructor.

Standard Java exceptions

  The Java class Throwable describes anything that can be thrown as an exception.

  There are two general types of Throwable objects:

    1. Error: represents compile-time and system errors that you don't worry about catching

    2. Exception: the basic type that can be thrown from any of the standard Java library class methods and from your methods and runtime accidents.

  There isn't anything special between one exception and the next except for the name.

  The basic idea is that the name of the exception represents the problem that occurred, and the exception name is intended to be relatively selfexplanatory.

  The exceptions are not all defined in java.lang; some are created to support other libraries such as util, net, and io. For eaxample, all I/O exceptions are inherited from java.io.IOException.

  Special case: RuntimeException

  if( t == null)

    throw new NullPointerException();

  类似t == null 的检查,在实际项目中是否有必要都显式检查?(思考)

  RuntimeException and any types inherited from RuntimeException are uncheced exceptions

  If you were forced to check for RuntimeExceptions, your code could get too messy.

  A RuntimeException is a special case, since the compiler doesn't require an exception specification for it. The output is reported to System.err.

  If a RuntimeException gets all the way out to main() without being caught, printStackTrace() is called for that exception as the program exits.

  A RuntimeException represents a programming error:

    1. An error you cannot anticipate. For example, a null reference that is outside of your control.

    2. An error that you, as a programmer, should have checked for in your code (such as ArrayIndexOutOf BoundsException where you should have paid attention to the size of the array).

  Java exception handling designed to handle those pesky runtime errors will occur because of forces outside your code's control, and also essential for certain types of programming bugs that the compiler cannot detect.

Performing cleanup with finally

  finally clause is executed whether or not an exception is thrown

  The finally clause is necessary when you need to set something other than memory back to its original state. This is some kind of cleanup like an open file or network connection, something you've drawn on the screen, or even a swich in the outside world.

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();
  }
}

  The goal here is to make sure that the switch is off when main( ) is completed, so sw.off( ) is placed at the end of the try block and at the end of each exception handler. But it’s possible that an exception might be thrown that isn’t caught here, so sw.off( ) would be missed. However, with finally you can place the cleanup code from a try block in just one place

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();
  }
}

  Even in cases in which the exception is not caught in the current set of catch clauses, finally will be executed before the exception-handling mechanism continues its search for a handler at the next higher level

  The finally statement will also be executed in situations in which break and continue statements are involved. Note that, along with the labeled break and labeled continue, finally eliminates the need for a goto statement in Java.

  Using finally during return

  (略)

  Pitfall: the lost exception

  There's a flaw in Java's exception implementation. It's possible for an exception to simply be lost. This happens with a particular configuration using a finally clause. 

public static void main(String[] args) {
  try {
    LostMessage lm = new LostMessage();
    try {
      lm.f(); // throw exception1
    } finally {
      lm.dispose(); //throw exception2 
    }
  } catch(Exception e) { // 捕抓到exception2,丢失exception1
    System.out.println(e);
  }
}

  you will typically wrap any method that throws an exception, such as dispose( ) in the example above, inside a try-catch clause

  An even simpler way to lose an exception is just to return from inside a finally clause

Exception restrictions

  When you override a method, you can throw only the exceptions that have been specified in the base-class version of the method. This is a useful restriction, since it means that code that work with the base class will automatically work with any object derived from the base class, including exceptions.

  1. OK to add new exceptions for constructors

  2. You can choose to not throw any exceptions, even if the base version does

  3. Overridden methods can throw inherited exceptions

  4. A derived-class constructor cannot catch exceptions thrown by its base-class constructor (原因:super(..)必须出现在第一行)

  Although exception specifications are enforced by the compile during inheritance, the exception specifications are not part of the type of a method, which comprises only the method name and argument types.

  the "exception specification interface" for a particular method may narrow during inheritance and overriding, but it may not widen—this is precisely the opposite of the rule for the class interface during inheritance.

Constructors

  The constructor puts the object into a safe starting state, but it might perform some operation—such as opening a file that doesn’t get cleaned up until the user is finished with the object and calls a special cleanup method. If you throw an exception from inside a constructor, these cleanup behaviors might not occur properly. This means that you must be especially diligent while you write your constructor.

  You might think that finally is the solution. But it’s not quite that simple, because finally performs the cleanup code every time. If a constructor fails partway through its execution, it might not have successfully created some part of the object that will be cleaned up in the finally clause.

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 {
      // Don’t close it here!!!
    }
  }

  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");
    }
  }

}

  One of the design issues with exceptions is whether to handle an exception completely at this level, to handle it partially and pass the same exception (or a different one) on, or whether to simply pass it on. Passing it on, when appropriate, can certainly simplify coding.

  This is one of the downsides to Java: All cleanupother than memory cleanup—doesn’t happen automatically, so you must inform the client programmers that they are responsible.

  The safest way to use a class which might throw an exception during construction and which requires cleanup is to use nested try blocks:  

  // 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 constructor
      System.out.println(e);
    } finally {
      nc4.dispose();
    }
  } catch(ConstructionException e) { // nc4 constructor
    System.out.println(e);
  }

Exception matching

  When an exception is thrown, the exception-handling system looks through the "nearest" handlers in the order they are written. When it finds a match, the exception is considered handled, and no further searching occurs.

  Matching an exception doesn’t require a perfect match between the exception and its handler. A derived-class object will match a handler for the base class.

Alternative approaches

  The reason exception-handling systems were developed is because the approach of dealing with each possible error condition produced by each function call was too onerous, and programmers simply weren’t doing it. As a result, they were ignoring the errors. It’s worth observing that the issue of programmer convenience in handling errors was a prime motivation for exceptions in the first place.

  One of the important guidelines in exception handling is "Don’t catch an exception unless you know what to do with it."

  one of the important goals of exception handling is to move the error-handling code away from the point where the errors occur. This allows you to focus on what you want to accomplish in one section of your code, and how you’re going to deal with problems in a distinct separate section of your code. As a result, your mainline code is not cluttered with error-handling logic, and it’s much easier to understand and maintain.

  Exception handling also tends to reduce the amount of error-handling code, by allowing one handler to deal with many error sites.

  history

  Perspectives

  it’s worth noting that Java effectively invented the checked exception (clearly inspired by C++ exception specifications and the fact that C++ programmers typically don’t bother with them). However, it was an experiment which no subsequent language has chosen to duplicate.

  Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result—decreased productivity and little or no increase in code quality.

  Passing 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();
}

  Converting checked to unchecked exceptions

  Throwing an exception from main( ) is convenient when you’re writing simple programs for your own consumption, but is not generally useful.

  The real problem is when you are writing an ordinary method body, and you call another method and realize, "I have no idea what to do with this exception here, but I don’t want to swallow it or print some banal message." With chained exceptions, a new and simple solution prevents itself. You simply "wrap" a checked exception inside a RuntimeException by passing it to the RuntimeException constructor.

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

  This seems to be an ideal solution if you want to "turn off the checked exception—you don’t swallow it, and you don’t have to put it in your method’s exception specification, but because of exception chaining you don’t lose any information from the original exception.

  This technique provides the option to ignore the exception and let it bubble up the call stack without being required to write try-catch clauses and/or exception specifications. However, you may still catch and handle the specific exception by using getCause( )  

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();
    } catch(FileNotFoundException e) {
      print("FileNotFoundException: " + e);
    } catch(IOException e) {
      print("IOException: " + e);
    } catch(Throwable e) {
      print("Throwable: " + e);
    }
  }
}

  Another solution is to create your own subclass of RuntimeException.

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 supporsed 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.)

Summary

  One of the advantages of exception handling is that it allows you to concentrate on the problem you’re trying to solve in one place, and then deal with the errors from that code in another place.

  And although exceptions are generally explained as tools that allow you to report and recover from errors at run time, I have come to wonder how often the "recovery" aspect is implemented, or even possible. My perception is that it is less than 10 percent of the time, and even then it probably amounts to unwinding the stack to a known stable state rather than actually performing any kind of resumptive behavior.

  Whether or not this is true, I have come to believe that the "reporting" function is where the essential value of exceptions lie.

  by laying this question to rest—even if you do so by throwing a RuntimeException—your design and implementation efforts can be focused on more interesting and challenging issues

上一篇:Leetcode 131. Palindrome Partitioning


下一篇:[转帖]GNU/Linux与开源文化的那些人和事