Effective Java of exception handling

  • 2020-04-01 01:31:49
  • OfStack

57. Exceptions are only used for exceptional situations:

          I don't know if you've ever encountered the following code:


     try {
              int i = 0;3
       while (true)    
       range[i++].climb();
       } 
       catch (ArrayIndexOutOfBoundsException e) {
       }

  Intention of this code is not very obvious, the intent is to traverse the array variable range of each element, and execute element climb method, when the subscript is beyond the range of the length of the array, will directly thrown ArrayIndexOutOfBoundsException abnormalities, catch block will catch the exception, but did not make any processing, just to see the error as part of the normal working process. This really gives people a strange feeling, let's take a look at the revised writing:

 for (Mountain m : range) {
        m.climb();
 }

          Compared to the way it was written before, the readability is self-evident. So why would anyone write it the first way? Clearly, they were misguided in their attempt to avoid the out-of-bounds checks that the JVM makes on every array access in the for-each loop. This is undoubtedly redundant and even counterproductive because putting code in a try-catch block prevents certain optimizations of the JVM, and as for array boundary checking, many JVM implementations now tune them out. In actual tests, we found that the abnormal way was much slower than the normal way.
          Code readability and efficiency in addition to the mentioned problem, the first writing will also cover some potential bugs, suppose climb method of array elements can also access a particular array, and in the process of access to an array of problems, based on the error, the JVM will be thrown ArrayIndexOutOfBoundsException abnormalities, unfortunately, the exception will be climb catch statement capture function, after do not make any processing, according to the normal process to continue, this Bug is hidden.
          The lesson of this example is simple: "exceptions should only be used for exceptions, they should never be used for normal control flow." While there are times when one might argue that this quirk can lead to performance improvements, the performance advantage of this exception pattern is unlikely to be sustained as platform implementations improve. However, the subtle bugs and maintenance pains of this overly clever model remain.
          Based on this principle, we can also be inspired when designing apis. A well-designed API should not ** its clients use exceptions for normal control flow. For example, Iterator, the JDK was designed with this in mind. Before the client can execute the next method, it needs to call the hasNext method to confirm whether there are still any readable collection elements, as shown in the following code:


     for (Iterator i = collection.iterator(); i.hasNext(); ) {
     Foo f = i.next();
     }

If Iterator lacks the hasNext method, the client changes the ** to the following:

 try {
 Iterator i = collection.iterator();
 while (true)
 Foo f = i.next();
 }
 catch (NoSuchElementException e) {
 }

  This should be very similar to the example of traversing groups given at the beginning of this entry. In the actual design, there is another way to verify an identifiable error return value, but this is not appropriate for this example because it may be legal for next to return null. So what are the differences between the two design approaches in practice?
          1. If there is a lack of synchronized concurrent access, or the state can be changed by the outside world, it is necessary to use methods that recognize the return value, because there is a time window between the test state (hasNext) and the corresponding call (next), in which the state of the object may change. Therefore, you should choose how to return an identifiable error return value in this case.
          2. If the state test method (hasNext) and the corresponding calling method (next) use the same code, it is not necessary to repeat the same work twice for performance reasons.
          3. In other cases, the "status test" design should be considered as much as possible, because it can lead to better readability.

58. Use checked exceptions for recoverable situations and runtime exceptions for programming errors:


          Three throwable structures are provided in Java: checked exceptions, runtime exceptions, and errors. This entry gives general principles for scenarios where all three types apply.
          1. If the caller is expected to recover properly, a checked exception should be used in this case, where a custom checked exception can be thrown if someone intends to make an online purchase and the balance turns out to be insufficient. By throwing the checked exception, the ** caller handles the exception in the catch clause, or continues to propagate up. Therefore, declaring checked exceptions in methods is a potential reminder to API users.
          2. Use run-time exceptions to indicate programming errors. Most run-time exceptions represent "prerequisite violations" where the user of the API does not comply with the usage conventions established by the API designer. Such problems as group access over the boundary.
          3. For errors, they are typically reserved by the JVM to indicate insufficient resources, failed constraints, or other conditions that make it impossible for the program to continue.
          The entry also shows a very practical trick for custom checked exceptions, where the caller can call the interface method provided by the custom exception to get more specific error information, such as the current balance, when the exception is caught.

59. Avoid unnecessary use of detected abnormalities:

          Checked exceptions are a nice feature that Java provides. Unlike return values, they ** the programmer has to handle exceptional conditions, which greatly increases the reliability of the program. Excessive use of b. if abnormal, however, can make the API when use is very inconvenient, after all, we still need to use some additional code to handle these exceptions thrown, if in a function, it is called five API will throw an exception, then write such function code will be a frustrating work.
          This burden is justified if the correct use of the API does not prevent the exception condition from occurring, and if the exception occurs, the programmer using the API can immediately take a useful action. Unless both of these conditions are true, it is better to use undetected exceptions, as shown in the following test:


      try { 
      dosomething(); 
      } catch (TheCheckedException e) { 
      throw new AssertionError(); 
      } 

    try { 
    donsomething(); 
    } catch (TheCheckedException e) {
    e.printStackTrace();
    System.exit(1);
    }

          When using checked exceptions, it is recommended that you consider using an unchecked exception if the catch clause handles the exception only in the two examples above, or worse. The reason is simply that they do nothing in the catch clause to restore the exception.

60. Preferential use of standard exceptions:


          Using standard exceptions not only reuses existing code better, but also makes your API easier to learn and use because it is more consistent with idioms that programmers are already familiar with. Another advantage is that the code is more readable and the programmer doesn't have to read more unfamiliar code. This entry shows some very common exceptions that are easy to reuse, as shown in the following table:
          Abnormal                                                                                             applications
          IllegalArgumentException                          The non-null parameter value is incorrect.
          IllegalStateException                                        Object state is not appropriate for method invocation.
          NullPointerException                                        The parameter value is null if null is not allowed.
          IndexOutOfBoundsException                The subscript parameter value is out of bounds
          ConcurrentModificationException    Concurrent modification of an object was detected when concurrent modification was disabled.
          UnsupportedOperationException      Object does not support the method requested by the user.
          Of course, there are many other exceptions in Java, such as ArithmeticException, NumberFormatException, and so on, all of which have their own application situations. However, it is important to note that the application situations of these exceptions are sometimes not very well defined, and it is more dependent on the context to determine which one is appropriate.
          Finally, it is important to make sure that the conditions for throwing an exception are consistent with those described in the exception document.

61. Throws an exception corresponding to the abstraction:


          This can be overwhelming if the exception thrown by a method is not explicitly related to the task it is performing. In particular, when exceptions are thrown from the bottom up, if nothing is done in the middle, the implementation details at the bottom will directly pollute the API interfaces at the top. To solve such problems, we usually do the following:


  try {
  doLowerLeverThings();
  } catch (LowerLevelException e) {
  throw new HigherLevelException(...);
  }

  This handling is called exception translation. In fact, there is a more convenient form of translation available in Java - the exception chain. Consider the sample code above. During debugging, it would be very helpful to find the root cause of the problem if the high-level application logic could understand the underlying cause of the actual exception, as shown in the following code:

     try {         doLowerLevelThings();
          } catch (LowerLevelException cause) {
   throw new HigherLevelException(cause);
   }

        The underlying exception is passed to the high-level exception as a parameter, and for most standard exceptions the constructor of the exception chain is supported, if not, the cause can be set using the initCause method of Throwable. The exception chain not only allows you to access the cause through the interface function getCause, it also integrates the cause's stack trace into higher-level exceptions.
          With this exception chain approach, you can effectively separate the low-level implementation details from the high-level application logic.      

63. Include failure capture information in details:


      When a program fails due to an uncaught exception, the system automatically prints out the stack trace of that exception. Contains the string representation of the exception in the stack trace, which is the result returned by the toString method. If we provide detailed error information for the exception at this point, it makes perfect sense to locate and trace the error. For example, we format the input parameters of the function that throws an exception and the field values of the class in which the function resides, and then package the information and pass it to the exception object to be thrown. Assume our high-level application capture IndexOutOfBoundsException abnormalities, if at this point the exception object can carry lower bound and upper bound of the array, and the values of the current cross-border information, after see this information, we can quickly make the right judgment and revised the Bug.
      Especially for checked exceptions, if the type of exception thrown also provides some additional interface methods for retrieving the data or information that caused the error, it is important for error recovery of the calling function that caught the exception.

64. Strive to keep failure atomic:


          This is a very important tip, because in the real world when you're an interface developer, you often ignore it and think it's okay if you don't guarantee it. On the other hand, if you are the user of the interface, you will also ignore it and take it for granted that the implementer of the interface should do it.
          When an object throws an exception, it is usually expected that the object will remain in a well-defined available state, even if the failure occurs in the middle of an operation. This is especially important for checked exceptions, because the caller wants to recover from such exceptions. In general, a failed method call should leave the object as it was before it was called. Methods that have this property are said to have "failed atomicity".
          There are several ways to maintain this atomicity.
          The easiest way is to design immutable objects. Because a failed operation only causes the creation of a new object to fail, it does not affect an existing object.
          2. For mutable objects, the general method is to verify the validity of parameters before operating on the object, which can make the object throw more meaningful exceptions before being modified, such as:


  public Object pop() {
  if (size == 0)
  throw new EmptyStackException();
  Object result = elements[--size];
  elements[size] = null;
  return result;
  }

        If the size is not verified before the operation, the array of elements throws an exception, but since the value of the size has changed, it will never return to its normal state when the object is used again later.
          3. Write recovery code in advance and execute the strip code when there is an error. This method is rarely used because it will bring a large maintenance cost in the process of code writing and code maintenance, and it is relatively inefficient.
          4. Create a temporary copy of the object, and use the copy object to reinitialize the state of the current object if an exception occurs during the operation.
          Although in general all hope to achieve atomicity failure, however in some cases it is difficult to do, such as two threads to modify a mutable object at the same time, in the absence of good synchronization, once throw ConcurrentModificationException, it will be difficult to return to original state.

65. Do not ignore exceptions:

          This is an obvious common sense, but it is often violated, so the entry reintroduces it, such as:


try {
dosomething();
} catch (SomeException e) {
}

          A predictable situation where you can ignore exceptions is when you close the FileInputStream because the data has been read. Even so, if you output a prompt when you catch the exception, it can be very helpful to dig out some potential problems. Otherwise, some potential problems will remain hidden until they suddenly explode at some point, causing irreparable consequences.
          The recommendations in this entry apply to both checked and unchecked exceptions.


Related articles: