Try-catch-finally may seem simple and harmless in the Java language, but it is not easy to truly “master” it. Nothing else, let’s take Fianlly for example, although its function is single, but it is “hidden killing machine” when used, if you don’t believe, let’s look at the following examples…

Pit 1: Use return in finally

If a return is used in finally, the result is not immediately returned even if there is a try-catch. Instead, the result is not immediately returned until the statement in finally is executed. This is where the problem arises: if a return statement exists in finally, the result in finally will be returned directly, and the return value in the try will be ruthlessly discarded.

① Counterexample code

public static void main(String[] args) throws FileNotFoundException {
    System.out.println("Execution Result :" + test());
}

private static int test(a) {
    int num = 0;
    try {
        // num=1
        num++;
        return num;
    } catch (Exception e) {
        // do something
    } finally {
        // num=2, return this value
        num++;
        returnnum; }}Copy the code

The result of the above code is as follows:

② Cause analysis

If there is a return statement in finally, the try-catch value will be overwritten. If the programmer does not notice this when writing the code, the program will execute incorrectly.

③ Solutions

If there is a return value ina try-catch-finally, make sure that the return statement occurs only once at the end of the method.

④ Example code

public static void main(String[] args) throws FileNotFoundException {
    System.out.println("Execution Result :" + testAmend());
}
private static int testAmend(a) {
    int num = 0;
    try {
        num = 1;
    } catch (Exception e) {
        // do something
    } finally {
        // do something
    }
    // Make sure that the return statement appears only once here
    return num;
}
Copy the code

Pit 2: Code in finally “does not execute”

While the above example is simple, the following example will give you a different feel by looking directly at the code.

① Counterexample code

public static void main(String[] args) throws FileNotFoundException {
    System.out.println("Execution Result :" + getValue());
}
private static int getValue(a) {
    int num = 1;
    try {
        return num;
    } finally{ num++; }}Copy the code

The result of the above code is as follows:

② Cause analysis

** thought the result would be 2, but never thought it would be 1 **. In the words of Master Ma, “I was careless, there was no flash”.

One might ask: If the code were ++num, would the result be 2?

I’m sorry to tell you that it doesn’t, but the result is still 1. So why is that? To really understand it, we need to start with the bytecode of this code.

The resulting bytecode from the above code is as follows:

// class version 52.0 (52)
// access flags 0x21
public class com/example/basic/FinallyExample {

  // compiled from: FinallyExample.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/example/basic/FinallyExample; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V throws java/io/FileNotFoundException 
   L0
    LINENUMBER 13 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "\u6267\u884c\u7ed3\u679c:"INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;) Ljava/lang/StringBuilder; INVOKESTATIC com/example/basic/FinallyExample.getValue ()I INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;) V L1 LINENUMBER14 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0xA
  private static getValue(a)I
    TRYCATCHBLOCK L0 L1 L2 nullL3 LINENUMBER 18 L3 ICONST_1 ISTORE 0 L0 LINENUMBER 20 L0 ILOAD 0 ISTORE 1 L1 LINENUMBER 22 L1 IINC 0 1 L4 LINENUMBER 20  L4 ILOAD 1 IRETURN L2 LINENUMBER 22 L2 FRAME FULL [I] [java/lang/Throwable] ASTORE 2 IINC 0 1 L5 LINENUMBER 23 L5 ALOAD  2 ATHROW L6 LOCALVARIABLE num I L0 L6 0 MAXSTACK= 1
    MAXLOCALS = 3
}
Copy the code

A simplified version of these bytecodes is shown below:To understand these bytecodes, you first need to understand what these bytecodes represent, which can be found on Oracle’s website:Docs.oracle.com/javase/spec…

Here’s a simple translation of the bytecode:

Iconst pushes values of type int onto the operand stack. Istore stores ints in local variables. Iload loads int values from local variables. Iinc increments local variables by subscript. Ireturn returns a value of type int from the operand stack. Astore stores references in local variables.

With this information in hand, let’s translate the bytecode content above:

 0Iconst_1 stores values in the operand stack1
 1Istore_0 stores the data in the operand stack at the location of local variables0
 2Iload_0 reads values from local variables to the operand stack3Istore_1 stores the operand stack1Stored at the location of local variables1
 4 iinc 0 by 1Place local variables0The element is incremented (+1) operation7Iload_1 will be locally located1Is loaded into the operand stack8Ireturn returns the operand stackintCopy the code

Through the above information you may not be able to intuitively see the internal execution process of this method, it does not matter lei Brother to prepare a method execution flow chart for you:

Before the finally statement (iInc 0, 1) is executed, two bits of information are stored in the local table. Position 0 and position 1 both store an int value of 1. Finally (iInc 0, 1) only sums the value of position 0 before executing finally (iInc 0, 1), and then returns the value of position 1 (1) to the operand stack. Therefore, when executing the return operation (ireturn), it reads the result of the return value of 1 from the operand stack, so the final execution is 1 instead of 2.

③ Solutions

On the question of how the Java virtual machine compiles finally blocks, For those interested, see Compiling Finally, Section 7.13, in The JavaTM Virtual Machine Specification, Second Edition. There you can see in detail how the Java virtual machine compiles the finally statement block.

In fact, the Java virtual machine inserts a finally block as a subroutine directly before a control-transfer statement ina try or catch block. However, there is another factor that cannot be ignored. Before a subroutine (finally block) is executed, a try or catch block retains its return value in the Local Variable Table. After the subroutine is executed, the reserved return value is restored to the operand stack, and then returned to the invoker using a return or throw statement.

So if there is a return operation ina try-catch-finally, ** be sure to return only once at the end of the method! ** This guarantees that all operations in the try-catch-finally will take effect.

④ Example code

private static int getValueByAmend(a) {
    int num = 1;
    try {
        // do something
    } catch (Exception e) {
        // do something
    } finally {
        num++;
    }
    return num;
}
Copy the code

Pit 3: Code in finally executes “not last”

① Counterexample code

public static void main(String[] args) throws FileNotFoundException {
    execErr();
}
private static void execErr(a) {
    try {
        throw new RuntimeException();
    } catch (RuntimeException e) {
        e.printStackTrace();
    } finally {
        System.out.println("Executive finally."); }}Copy the code

The result of the above code is as follows:As can be seen from the above resultsThe code in finally is not executed last, but before the catch prints an exceptionWhy is that?

② Cause analysis

The real cause of the above problem is not try-catch-finally. When we open the source code of E.printStackTrace, we can see some clues.As you can see from the figure above, the e.prinintstackTrace () and finally outputs are not the same object.Finally uses the standard output stream: System.out, while e.prinintStackTrace () uses the standard error output stream: System.err.println, they perform the same effect as:

public static void main(String[] args) {
    System.out.println(I'm the standard output stream);
    System.err.println(I'm the standard error output stream);
}
Copy the code

The order of the above code execution results is also random, and the reason for all this can be seen in the comments and instructions of the standard Error output stream (System.err) : Let’s simply do a simple translation of the above notes:

Standard error output stream. The stream is open and ready to receive output data. Typically, this stream corresponds to the host environment or user-specified display output or another output target. By convention, even if the main output stream (out output stream) has been redirected to a file or other target location, the output stream (ERR output stream) can be used to display error messages or other information that should be brought to the immediate attention of the user.

From the comment information of the source code, it can be seen that the standard error output stream (system.err) and the standard output stream (system.out) use different stream objects, even if the standard output stream and locate to other files, it does not affect the standard error output stream. It is then a safe guess that the two are executed independently and in parallel for more efficient output stream information, so we see that the print order is always random.

To verify this, we redirect the standard output stream to a file and then check to see if system. err prints properly.

public static void main(String[] args) throws FileNotFoundException {
    // Locate the standard output stream information in log.txt
    System.setOut(new PrintStream(new FileOutputStream("log.txt")));
    System.out.println(I'm the standard output stream);
    System.err.println(I'm the standard error output stream);
}
Copy the code

The result of the above code is as follows:When the program is finished, we find a new log.txt file in the root directory of the project. Open this file and see the following result:As you can see from the above results, the standard output and standard error output streams are executed independently of each other, and the JVM runs them in parallel for efficient execution, so we end up with finally executing before catch.

③ Solutions

Once we know why, we can fix the problem by simply changing the output object in the try-catch-finally to a uniform output stream object.

④ Example code

private static void execErr(a) {
    try {
        throw new RuntimeException();
    } catch (RuntimeException e) {
        System.out.println(e);
    } finally {
        System.out.println("Executive finally."); }}Copy the code

After changing to a unified output stream object, I manually executed it n times without finding any problems.

Pit 4: Code in finally “does not execute”

Does the code in finally always execute? Before, I would have said “yes” without hesitation, but now that I’ve been beaten by society, I might say something like: Normally, the code in FINALLY must execute, but not necessarily when there are special situations in finally, such as the following:

  • System. Exit is executed in a try-catch statement;
  • An infinite loop occurs in a try-catch statement;
  • A power failure or JVM crash occurs before finally execution.

If any of these things happen, the code in finally will not execute. Although this one feels a bit cynical, Murphy’s Law tells us that if something can happen, it will happen. So from a serious point of view, the idea is still valid, especially for beginners, it is very easy to write an undetected loop, isn’t it?

① Counterexample code

public static void main(String[] args) {
    noFinally();
}
private static void noFinally(a) {
    try {
        System.out.println("I'm try!");
        System.exit(0);
    } catch (Exception e) {
        // do something
    } finally {
        System.out.println("I am fially ~"); }}Copy the code

The result of the above code is as follows:You can see from the above results that the code in finally did not execute.

② Solution

Exclude system. exit code from your code unless business needs it, but also note that if system. exit code is present in try-cacth, then the code in finally will not be executed.

conclusion

In this article we’ve shown some of the problems with Finally, some useful dry goods, and some examples of what might seem like “hackiness”, but it’s one thing to know that try-catch-finally is not an easy thing to master. Finally, if there is a try-catch-finally operation that returns a value, be sure to return only once at the end of the method!

Reference & acknowledgements

Java Development Manual of Alibaba

developer.ibm.com/zh/articles/j-lo-finally

Follow the public account “Java Chinese community” found more dry goods.

Check out Github for more highlights: github.com/vipstone/al…