Java's exception handling mechanism

Exceptions are something that everyone "keeps away from" in daily development, but in fact almost every high-level programming language has its own exception handling mechanism, because no matter how good a programmer you are, mistakes are inevitable, in other words In other words: No matter how good you are, you also have time to write bugs.

The so-called "exception handling mechanism" is to be able to return the error message and the approximate location of the error code as much as possible when you have a logic error, so as to facilitate you to troubleshoot the error.

At the same time, you don't have to think too much about the exception. It's just a piece of error message. It's just that some logical errors in your program are checked out by the virtual machine. It encapsulates the error information and "reports" it to you. That's it, and exactly how you handle it is up to you.

Exceptional inheritance architecture

In Java, the class Throwable is the highest parent class of the entire exception handling mechanism. It has two subclasses, Error and Exception, which represent "error" and "exception" respectively.

The exceptions we usually talk about actually refer to Exceptions, because errors are beyond our programmer’s control, and are often caused by problems inside the virtual machine, such as stack space overflow due to insufficient memory, virtual machine running failures, etc. .

In this case, the virtual machine will directly terminate the thread and call back error information through Error and its subclass objects. Therefore, we only focus on the exceptions of Exception and its subclasses that can be controlled by us.

image

Our Exception exceptions are mainly divided into two categories, one is IOException (I/O input and output exception), and the other is RuntimeException (runtime exception). IOException and its subclasses are also called "checked exceptions", and RuntimeException is called "unchecked exceptions".

The so-called checked exceptions are those exceptions that the compiler requires during compilation to be handled. For example: you write a piece of code to read and write a file, and the compiler thinks that reading and writing a file is likely to encounter a situation where the file does not exist, so it forces you to write a piece of code to handle the exception that the file does not exist.

The file does not exist exception here is a checked exception, you must deal with it at compile time.

The reason why our RuntimeException is called a runtime exception is because the compiler does not know what problems will occur in your code, so it does not force you to handle the exception. During the runtime, if an exception occurs, the virtual machine will call back the error message.

Of course, if you predict that your code will have an exception, you can also catch it yourself, but then again, if you know there may be a problem somewhere, why don't you just solve it directly? .

So, runtime exceptions are unknowable exceptions and don't force you to handle them.

custom exception type

All exceptions defined in Java's exception mechanism cannot foresee all possible errors. In some specific situations, we need to customize the exception type to report some error information.

The custom exception type is also quite simple, you can choose to inherit Throwable, Exception or their subclasses, and even you don't need to implement and override any methods of the parent class to complete the definition of an exception type.

E.g:

public class MyException extends RuntimeException{

}
public class MyException extends Exception{

}

Of course, you can also override multiple overloaded constructors if you want to provide more information for your exceptions, for example:

public class MyException extends RuntimeException{
    public MyException(){}

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

    public MyException(String mess,Throwable cause){
        super(mess,cause);
    }
}

We know that any exception type, whether it is in the Java API or our own, must directly or indirectly inherit the Throwable class.

And this Throwable class defines a String type of detailMessage field stored by the subclass to pass in the details of the subclass exception. E.g:

public static void main(String[] args) {
    throw new MyException("hello wrold failed");
}

Output result:

Exception in thread "main" test.exception.MyException: hello wrold failed
    at test.exception.Test.main(Test.java:7)

Whenever the program encounters an exception, Java creates an exception type object like other objects and stores it in the heap, and then the exception mechanism takes over the program, first retrieving whether the exception table of the current method can match the exception (exception). The table holds a collection of all exceptions that have been handled by the current method).

If it matches an exception in the exception table, it will jump to the bytecode position where the exception is handled according to the relevant information about exception handling stored in the exception table to continue execution.

Otherwise, the virtual machine will terminate the invocation of the current method and pop the stack frame of the method, return to the calling place of the method, and continue to retrieve the caller's exception table to match the processing of the exception.

If it has been unable to match, all the methods involved in the entire method call chain will eventually pop up the stack and will not run normally, and finally the virtual machine will print the error message of this exception.

This is roughly a process from the appearance of an exception to the eventual handling. It is enough to see that if an exception is handled, the program will be recovered and can continue to execute. Otherwise, all methods involving the exception will be terminated.

As for the content of this exception information, let's look at the specific implementation of the printStackTrace method:

image

image

There are a total of three parts of information, the first part consists of the name of the exception and its detailMessage, the second part is the call chain information of the exception, from top to bottom is the location of the exception to the calling point of the outer method, and the third part is is the source exception that caused the exception.

exception handling

Regarding the handling of exceptions, the most familiar one is try-catch. The basic syntax of try-catch is as follows:

try{
    //你的程序
}catch(xxxException e){
    //异常处理代码
}catch(xxxException e){
    //异常处理代码
}

The code in the try code block is also called the "monitoring area", and the catch code block is called the "exception handling area". Among them, each catch code block corresponds to an exception handling, and the exception will be stored in the exception table of the method. Once any exception occurs in the try code block, the exception handling mechanism will first retrieve from the exception table whether the exception has been handled. code block.

To be precise, the handled exception block held by the exception table can only be used to process the code in our try block, and the same exception elsewhere will not be matched for processing.

Of course, in addition to this, we have another way to handle exceptions, throwing exceptions . E.g:

public static void main(String[] args){
    try{
        calculate(22,0);
    }catch (Exception e){
        System.out.println("捕获一个异常");
        e.printStackTrace();
    }
}

public static void calculate(int x,int y){
    if (y == 0) 
        throw new MyException("除数为 0");
    int z = x/y;
}

Output result:

捕获一个异常
test.exception.MyException: 除数为 0
    at test.exception.Test_throw.calculate(Test_throw.java:14)
    at test.exception.Test_throw.main(Test_throw.java:6)

We can use the throw keyword to manually throw an exception. In this case, the callee is often unable to handle an exception and needs to be thrown to the caller to handle it.

Obviously, this way of throwing exceptions is meticulous, and requires programmers to have a certain pre-judgment. There is another way of throwing exceptions in Java, see:

public static void calculate2(int x,int y) throws ArithmeticException{
    int z = x/y;
}

This method is more "rude". I don't care where you are, there will be an exception. As long as you encounter an exception of the ArithmeticException type, you will throw it to me.

In fact, the second is essentially the same as the first. When the virtual machine is performing x/y, when it finds that y is equal to zero, it will also create a new ArithmeticException object, and then the program will be handed over to the exception mechanism.

But the latter is more trouble-free than the former. You don't need to care about where an exception occurs, and you don't need to manually make judgments. Everything is left to the virtual machine. However, the obvious shortcoming is that the control of the exception is not in their own hands, and some customized exception virtual machines cannot be judged when they are running.

For example, if our calculate2 method here does not allow y to be equal to 1, it will throw a MyException if it is equal to 1. In this case, the latter can never be achieved, because the division by 1 does not have any problem in the eyes of the virtual machine, how do you call it to throw an exception. Using the former to manually throw an exception is much simpler.

However, you have to be clear that whether you use throws to manually throw an exception upwards, or use throws to let the virtual machine dynamically throw an exception for us, you always need to handle the exception somewhere, which requires clear.

It's not that if you don't want to clean up your garbage, you just throw it to your classmates at the front desk, and if you don't want to clean up the front desk, just keep throwing it forward, but the person at the front has to deal with it, otherwise you will wait for your class teacher. I'll clean up after you've cleaned up.

Execution order of try-catch-finally

Issues related to the order of execution of try-catch-finally can be said to be "regular" in all kinds of interviews, especially when the finally block has a return statement. Let's look directly at a few interview questions:

Interview question one:

public static void main(String[] args){
    int result = test1();
    System.out.println(result);
}

public static int test1(){
    int i = 1;
    try{
        i++;
        System.out.println("try block, i = "+i);
    }catch(Exception e){
        i--;
        System.out.println("catch block i = "+i);
    }finally{
        i = 10;
        System.out.println("finally block i = "+i);
    }
    return i;
}

We might as well calculate what the final result of the programmer's operation is.

The output is as follows:

try block, i = 2
finally block i = 10
10

This is a fairly simple problem, there is no pit, let's change it slightly:

public static int test2(){
    int i = 1;
    try{
        i++;
        throw new Exception();
    }catch(Exception e){
        i--;
        System.out.println("catch block i = "+i);
    }finally{
        i = 10;
        System.out.println("finally block i = "+i);
    }
    return i;
}

The output is as follows:

catch block i = 1
finally block i = 10
10

The result of the operation must be expected, the program throws an exception, which is then caught and handled by the catch block of this method.

Interview question two:

public static void main(String[] args){
    int result = test3();
    System.out.println(result);
}

public static int test3(){
    //try 语句块中有 return 语句时的整体执行顺序
    int i = 1;
    try{
        i++;
        System.out.println("try block, i = "+i);
        return i;
    }catch(Exception e){
        i ++;
        System.out.println("catch block i = "+i);
        return i;
    }finally{
        i = 10;
        System.out.println("finally block i = "+i);
    }
}

The output is as follows:

try block, i = 2
finally block i = 10
2

Do you have any doubts? Obviously I have a return statement in the try block, but why is the code in the finally block executed?

Let's decompile this class and see the implementation of the compiled bytecode of this test3 method:

0: iconst_1         //将 1 加载进操作数栈
1: istore_0         //将操作数栈 0 位置的元素存进局部变量表
2: iinc          0, 1   //将局部变量表 0 位置的元素直接加一(i=2)
5: getstatic     #3     // 5-27 行执行的 println 方法                
8: new           #5                  
11: dup
12: invokespecial #6                                                     
15: ldc           #7 
17: invokevirtual #8                                                     
20: iload_0         
21: invokevirtual #9                                                     24: invokevirtual #10                
27: invokevirtual #11                 
30: iload_0         //将局部变量表 0 位置的元素加载进操作栈(2)
31: istore_1        //把操作栈顶的元素存入局部变量表位置 1 处
32: bipush        10 //加载一个常量到操作栈(10)
34: istore_0        //将 10 存入局部变量表 0 处
35: getstatic     #3  //35-57 行执行 finally中的println方法             
38: new           #5                  
41: dup
42: invokespecial #6                  
45: ldc           #12                 
47: invokevirtual #8                  
50: iload_0
51: invokevirtual #9                
54: invokevirtual #10                 
57: invokevirtual #11                 
60: iload_1         //将局部变量表 1 位置的元素加载进操作栈(2)
61: ireturn         //将操作栈顶元素返回(2)
-------------------try + finally 结束 ------------
------------------下面是 catch + finally,类似的 ------------
62: astore_1
63: iinc          0, 1
.......
.......

It can be seen from our analysis that the content in the finally block will always be executed, regardless of whether the program is abnormal or not, the compiler will copy the code in the finally block twice and add them after try and catch respectively. .

Some people may be confused. Originally, our i was stored in the local variable table 0, and the code in the finally finally filled the slot 0 with the value 10. Why does the program still return the value 2?

Looking at the bytecode carefully, you will find that before the return statement returns, the virtual machine pushes the value to be returned into the operand stack and waits for the return, even if the finally statement block modifies i, the value to be returned already exists. in the operand stack, so it will not affect the program return result.

Interview question three:

public static int test4(){
    //finally 语句块中有 return 语句
    int i = 1;
    try{
        i++;
        System.out.println("try block, i = "+i);
        return i;
    }catch(Exception e){
        i++;
        System.out.println("catch block i = "+i);
        return i;
    }finally{
        i++;
        System.out.println("finally block i = "+i);
        return i;
    }
}

operation result:

try block, i = 2
finally block i = 3
3

In fact, you look at the entire process from its bytecode instructions, not just remember its execution process.

image

You will find that the program will eventually use the return statement in the finally block to return, and directly ignore the return instruction in the try block.

Finally, there is an unwritten convention for the use of exceptions: try to handle them uniformly in a centralized location, and do not use try-catch everywhere, otherwise the code structure will be chaotic .


All the code, images and files in the article are stored in the cloud on my GitHub:

(https://github.com/SingleYam/overview_java)

Welcome to the WeChat public account: Gorky on the code, all articles will be synchronized on the public account.

image

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324911821&siteId=291194637