You use the Java exception mechanism every day and are familiar with the try-catch-finally execution process. This article looks at some of the details of the Java exception mechanism that, while small, have a significant impact on code performance and readability.

1. Introduction to Java exception architecture

Before learning a technology, we must first stand in the commanding heights overlooking the overall situation of technology, from the macro control of the entire structure of a technology. This way you can focus on the most important points of the architecture without getting bogged down in the details. So, before introducing some of the secrets of Java exceptions that you may not have known, let’s review the Java exception architecture.

Throwable is the top-level parent class of the entire Java Exception architecture. It has two subclasses: Error and Exception.

Error indicates fatal system errors that cannot be handled by the program, such as OutOfMemoryError and ThreadDeath. When these errors occur, the Java virtual machine can only terminate the thread.

Exceptions are exceptions that can be handled by the program itself. There are two main categories of runtime exceptions and non-runtime exceptions.

Runtime exceptions are RuntimeException class and its subclasses abnormalities, such as NullPointerException, IndexOutOfBoundsException, these exceptions to unchecked exceptions, the developer can select processing, also can not deal with. These exceptions are usually caused by program logic errors, and programs should logically avoid them as much as possible.

In the Exception Exception framework, all exceptions except those of the RuntimeException class and its subclasses are checked. When you call the method that throws these exceptions, you must handle them. If not, the program will not compile. Such as IOException, SQLException, user-defined Exception, and so on.

2. try-with-resources

Prior to JDK 1.7, handling IO operations was cumbersome. Because IOExceptions are checked exceptions, the caller must handle them with a try-catch; Because the resource needs to be closed after the IO operation, the close() method that closes the resource also throws a checked exception, you also need to use a try-catch to handle that exception. As a result, a small piece of IO code is wrapped in a complex try-catch, greatly reducing the readability of the program.

A standard IO operation code is as follows:

public class Demo {
    public static void main(String[] args) {
        BufferedInputStream bin = null;
        BufferedOutputStream bout = null;
        try {
            bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
            bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
            int b;
            while((b = bin.read()) ! = -1) { bout.write(b); }}catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if(bin ! =null) {
                try {
                    bin.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
                finally {
                    if(bout ! =null) {
                        try {
                            bout.close();
                        }
                        catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
Copy the code

The code above writes data from one file to another using an output stream bin and an input six bout. Because IO resources are so valuable, you must release both resources separately in finally after the operation is complete. In order to release the IO resources correctly, two finally blocks of code need to be nested to release the resources.

Of the 40 lines above, less than 10 actually deal with IO operations, while the remaining 30 lines are devoted to ensuring proper resource release. This obviously makes the code less readable. Fortunately, JDK 1.7 provides try-with-resources to solve this problem. The modified code is as follows:

public class TryWithResource {
    public static void main(String[] args) {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            int b;
            while((b = bin.read()) ! = -1) { bout.write(b); }}catch(IOException e) { e.printStackTrace(); }}}Copy the code

We need to put the resource declaration code in parentheses after a try, and then put the resource handling code in {} after a try. Exception handling still happens in the catch block, and we don’t need to write a finally block.

So why does try-with-resources avoid a lot of resource-free code? The answer is that the Java compiler adds the finally code block for us. Note that only the finally code block is added by the compiler, and the process of resource release needs to be provided by the resource provider.

In JDK 1.7, all IO classes implement the AutoCloseable interface and need to implement the close() function in which the resource release process is performed.

Then, at compile time, the compiler automatically adds a finally block and adds the resource-releasing code from the close() function to the finally block. This improves code readability.

Exception masking problem

In a try-catch-finally block, if an exception is thrown from a try block, a catch block, and a finally block, then only the exception in the finally block is eventually thrown, and the exceptions in the try block and the catch block are masked. This is the exception masking problem. As shown in the following code:

public class Connection implements AutoCloseable {
    public void sendData(a) throws Exception {
        throw new Exception("send data");
    }
    @Override
    public void close(a) throws Exception {
        throw new MyException("close"); }}Copy the code

Start by defining a Connection class that provides the sendData() and close() methods, both of which throw an exception directly without any business logic for the purposes of the experiment. Next we use this class.

public class TryWithResource {
    public static void main(String[] args) {
        try {
            test();
        }
        catch(Exception e) { e.printStackTrace(); }}private static void test(a) throws Exception {
        Connection conn = null;
        try {
            conn = new Connection();
            conn.sendData();
        }
        finally {
            if(conn ! =null) { conn.close(); }}}}Copy the code

When conn.sendData() is executed, the method throws the exception to the caller main(), but first executes the finally block. When the conn.close() method in the finally block is executed, an exception is also thrown to the caller. Exceptions thrown by the try block are overwritten, and only exceptions in the finally block are printed in the main method. The result is as follows:

basic.exception.MyException: close
	at basic.exception.Connection.close(Connection.java:10)
	at basic.exception.TryWithResource.test(TryWithResource.java:82)
	at basic.exception.TryWithResource.main(TryWithResource.java:7)...Copy the code

This is the problem of try-catch-finally exception masking, and try-with-resources is a great way to solve this problem. So how does it solve this problem?

Let’s first rewrite this code with try-with-resources:

public class TryWithResource {
    public static void main(String[] args) {
        try {
            test();
        }
        catch(Exception e) { e.printStackTrace(); }}private static void test(a) throws Exception {
        Connection conn = null;
        try (conn = newConnection();) { conn.sendData(); }}}Copy the code

To get a clear idea of what the Java compiler does with try-with-Resources, we decomcompile this code and get something like this:

public class TryWithResource {
    public TryWithResource(a) {}public static void main(String[] args) {
        try {
            // Resource declaration code
            Connection e = new Connection();
            Throwable var2 = null;
            try {
                // Resource usage code
                e.sendData();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                // Resource release code
                if(e ! =null) {
                    if(var2 ! =null) {
                        try {
                            e.close();
                        } catch(Throwable var11) { var2.addSuppressed(var11); }}else{ e.close(); }}}}catch(Exception var14) { var14.printStackTrace(); }}}Copy the code

The core operation is line 22 var2.addsuppressed (var11); . The compiler first stores the exceptions in the try and catch blocks into a local variable, and when an exception is thrown again in the finally block, it adds the current exception to its exception stack by using the addSuppressed() method of the previous exception, thus ensuring that the exceptions in the try and catch blocks are not lost. When try-with-resources is used, the output looks like this:

java.lang.Exception: send data
	at basic.exception.Connection.sendData(Connection.java:5)
	at basic.exception.TryWithResource.main(TryWithResource.java:14)... Suppressed: basic.exception.MyException: close at basic.exception.Connection.close(Connection.java:10)
		at basic.exception.TryWithResource.main(TryWithResource.java:15)...5 more
Copy the code

3. Try-catch-finally execution flow

As you know, the code in try is executed first, and if no exception occurs, the code in finally is executed directly. If an exception occurs, the code in catch is followed by the code in finally.

We’re all familiar with this process, but what if a return occurs in a try or catch block? What about throw? The order of execution changes at this point.

However, it is important to remember that the return and throw in fianlly override the return and throw in try and catch. What do you mean? Read on.

To explain this, let’s take a look at an example. What does the test() function in the following code return?

public int test(a) {
    try {
        int a = 1;
        a = a / 0;
        return a;
    } catch (Exception e) {
        return -1;
    } finally{
        return -2; }}Copy the code

The answer is minus 2.

When executing code a = a / 0; When an exception occurs, the code after the try block is not executed. Instead, the code in the catch block is executed directly. In a catch block, a finally block is executed before return-1; Since the finally block contains a return statement, the return in catch will be overwritten, and the program will end by executing return-2 in fianlly. So the output is -2.

Similarly, replacing a return with a throw is the same. Finally overrides the return and throw ina try or catch block.

Special reminder: Do not use return statements in finally blocks! The examples here are just to show you that this feature of Java is prohibited in actual development!

4. Optional elegant solution to NPE problems

Null-pointer exceptions are run-time exceptions for which the best practice is to let the program die early if there is no clear handling strategy, but in many cases the developer is not aware of the null-pointer exception, rather than having a specific handling strategy. When an exception does occur, it’s easy to just add an if statement where the exception exists. However, such a strategy leads to more and more NULL checks in our program. We know that good programming should keep the null keyword in the code as little as possible. Java8 provides Optional classes that reduce nullpointexceptions while improving code aesthetics. But first, it’s important to be clear that this is not an alternative to the NULL keyword, but rather provides a more elegant implementation of null criteria that avoids nullPointExceptions.

Suppose the following Person class exists:

class Person{
    private long id;
    private String name;
    private int age;
    
    // omit the setter and getter
}
Copy the code

When we call an interface and get a Person object, we can process it as follows:

  • ofNullable(person)
    • Convert the Person object to an Optional object
    • Allow person to be empty
Optional<Person> personOpt = Optional.ofNullable(person);
Copy the code
  • T orElse(T other)
    • If it is empty, the default value is given
personOpt.orElse(new Person(Chaimaomao));
Copy the code
  • T orElseGet(Supplier<? extends T> other)
    • If null, the corresponding code is executed and the default value is returned
personOpt.orElseGet(()->{
    Person person = new Person();
    person.setName(Chaimaomao);
    person.setAge(20);
    return person;
});
Copy the code
  • <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
    • If it is empty, an exception is thrown
personOpt.orElseThrow(CommonBizException::new);
Copy the code
  • <U>Optional<U> map(Function<? super T,? extends U> mapper)
    • Mapping (get the name in Person)
String name = personOpt
                .map(Person::getName)
                .orElseThrow(CommonBizException::new)
                .map(Optional::get);
Copy the code

5. Exception handling protocol

  • Java libraries that are defined in the class of RuntimeException can be avoid by check in advance, and should not catch to processing, such as: IndexOutOfBoundsException, NullPointerException, etc.
    • Is:if (obj ! = null) {... }
    • Example:try { obj.method() } catch (NullPointerException e) {... }
  • Exceptions should not be used for process control or condition control, because exceptions are less efficient than condition branches.
  • It is irresponsible to try and catch large chunks of code. For catch, distinguish between stable code and unstable code. Stable code is code that will not fail no matter what. For the catch of unstable code, distinguish the exception type as far as possible, and then do the corresponding exception processing.
  • Catch an exception to handle it, don’t catch it and throw it away without doing anything. If you don’t want to handle it, throw the exception to its caller. The outermost business consumer must handle the exception and turn it into something that the user can understand.
  • If you need to roll back a transaction after a catch exception, be sure to roll back the transaction manually.
  • You cannot use a return ina finally block. A return ina finally block terminates the method and does not execute the return statement in the try block.
  • Finally blocks must close resource objects, stream objects, and try-catch exceptions. Note: If JDK7 or higher, try-with-resources can be used.
  • If you need to roll back a transaction after a catch exception, be sure to roll back the transaction manually.
  • Catch and throw must be an exact match, or the catch must be a parent of the throw. That is, the exception thrown must be the caught exception or a subclass of it. That’s how you make the big big small.
  • This statute makes it clear that preventing NPE is the responsibility of the caller. Even if the called method returns an empty collection or an empty object, it is not easy for the caller to worry about null returns for remote call failures, runtime exceptions, and other scenarios.
  • Distinguish between unchecked and unchecked exceptions. Avoid throwing RuntimeException or Throwable directly. Use custom exceptions with business significance. Custom exceptions defined in the industry are recommended, for example, DAOException/ServiceException.
  • Use “throw exception” or “return error code” in code:
    • Error codes must be used for external HTTP/API open interfaces;
    • In-app recommendation exceptions are thrown;
    • In inter-application RPC calls, the Result mode is preferred, encapsulating isSuccess, Error code, and Error brief message.