Home  >  Article  >  Java  >  How to use Java exception handling mechanism?

How to use Java exception handling mechanism?

PHPz
PHPzforward
2023-05-09 16:07:071357browse

Concept

The concept of exception handling originated in early programming languages ​​such as LISP, PL/I and CLU. For the first time, these programming languages ​​introduced exception handling mechanisms to detect and handle error conditions during program execution. The exception handling mechanism has subsequently been widely adopted and developed in programming languages ​​such as Ada, Modula-3, C, Python, and Java. In Java, exception handling provides a way to handle errors and exceptions while a program is running. The exception handling mechanism allows the program to continue executing when an error is encountered, rather than crashing immediately. This mechanism makes the program more robust and fault-tolerant. Exceptions are divided into two categories: Checked Exceptions and Unchecked Exceptions

Checked Exceptions:

Checked Exceptions are those that must be handled at compile time. They are usually caused by programmer error or problems with external resources. For example, IOException, FileNotFoundException, etc. Checked exceptions must be declared using the throws keyword in the method signature, or captured and handled within the method body using a try-catch block.

Unchecked Exceptions:

Unchecked exceptions refer to exceptions that are not required to be handled at compile time. They are usually caused by programming errors, such as null pointer exception (NullPointerException), array out-of-bounds (ArrayIndexOutOfBoundsException), etc. Unchecked exceptions inherit from the java.lang.RuntimeException class and do not need to be declared in the method signature, nor do they need to be forced to be captured and processed.

Their relationship is as follows:

How to use Java exception handling mechanism?

Exception handling

Java uses the try/catch keyword to catch exceptions , use throw to declare an exception, the sample code is as follows:

public class NullPointerExceptionExample {
    public static void main(String[] args) {
        String nullString = null;
        try {
            int length = nullString.length();
        } catch (NullPointerException e) {
            System.out.println("Caught NullPointerException!");
            e.printStackTrace();
        }
    }
}

In this example, we try to get the length of a null string. When nullString.length() is called, NullPointerException will be thrown. We use the try-catch statement to catch the exception and handle it

Custom exception

Java official exception cannot foresee all possible errors. Sometimes you need to combine your own Business scenarios, such as the following scenarios:

  • When the built-in Java exception class cannot accurately describe the abnormal situation you encounter.

  • A specific set of exceptions needs to be created for a specific domain or business logic.

  • When you want to provide more contextual information or specific error codes to the caller through a custom exception class.

Constructing a specific exception is also very simple. Inherit the existing exception class (it is best to inherit the same meaning). As follows, we create an exception indicating insufficient account balance:

public class InsufficientBalanceException extends RuntimeException {
    private double balance;
    private double amount;

    public InsufficientBalanceException(double balance, double amount) {
        super("Insufficient balance: " + balance + ", required amount: " + amount);
        this.balance = balance;
        this.amount = amount;
    }

    public double getBalance() {
        return balance;
    }

    public double getAmount() {
        return amount;
    }
}

Next, we use this custom exception in the business logic code:

public class BankAccount {
    private double balance;

    public BankAccount(double balance) {
        this.balance = balance;
    }

    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException(balance, amount);
        }
        balance -= amount;
    }
}

The caller can catch and handle this custom exception:

public class BankAccountTest {
    private static final Logger logger = Logger.getLogger(BankAccountTest.class.getName());

    public static void main(String[] args) {
        BankAccount account = new BankAccount(1000.00);
        try {
            account.withdraw(2000.00);
        } catch (InsufficientBalanceException e) {
            System.out.println("Error: " + e.getMessage());
            System.out.println("Current balance: " + e.getBalance());
            System.out.println("Required amount: " + e.getAmount());
            
            logger.log(Level.SEVERE, "An exception occurred", e);
        }
    }
}

You can see the custom exception Defining exceptions allows us to more clearly express possible exceptions in business logic while providing the caller with more contextual information about the exception. We also use the java.util.logging tool to record the output to the log

Multiple capture

In early versions of Java, handle multiple exceptions that have no common base class, You need to write a catch statement for each exception type, as follows:

class CustomException1 extends Exception {
    public CustomException1(String message) {
        super(message);
    }
}

class CustomException2 extends Exception {
    public CustomException2(String message) {
        super(message);
    }
}

public class SingleCatchException {

    public static void main(String[] args) {
        try {
            // 根据参数选择抛出哪种异常
            if (args.length > 0 && "type1".equals(args[0])) {
                throw new CustomException1("This is a custom exception type 1.");
            } else {
                throw new CustomException2("This is a custom exception type 2.");
            }
        } catch (CustomException1 e) {
            // 当 CustomException1 发生时,执行此代码块
            System.err.println("Error occurred: " + e.getMessage());
        } catch (CustomException2 e) {
            // 当 CustomException2 发生时,执行此代码块
            System.err.println("Error occurred: " + e.getMessage());
        }
    }
}

Such code is not only difficult to read, but also not concise enough.

Multiple exception catching mechanism, which allows catching multiple exception types in one catch statement. This approach avoids duplication of code and makes exception handling more concise. The following is an example of using multiple exception catching mechanisms:

public class MultiCatchExample {

    public static void main(String[] args) {
        try {
            // 根据参数选择抛出哪种异常
            if (args.length > 0 && "type1".equals(args[0])) {
                throw new CustomException1("This is a custom exception type 1.");
            } else {
                throw new CustomException2("This is a custom exception type 2.");
            }
        } catch (CustomException1 | CustomException2 e) {
            // 当 CustomExceptionType1 或 CustomExceptionType2 发生时,执行此代码块
            System.err.println("Error occurred: " + e.getMessage());
        }
    }
}

Rethrowing exceptions

In some cases, you may want to pass the exception to the caller for handling instead of in the current method in processing. Or you need to perform some processing operations when catching exceptions, such as logging, cleaning up resources, or adding additional context information. In this case, you can handle the exception in a catch block and then rethrow the original exception or throw a new exception with additional information, as follows:

public class RethrowExceptionExample {
    public static void main(String[] args) {
        try {
            doSomething();		// 可能会抛出异常的方法
        } catch (IOException e) {
            // 异常处理逻辑
            System.err.println("An error occurred: " + e.getMessage());            
            // 重新抛出异常
            throw e;
        }
    }
}

Update Good NPE

NPE (NullPointerExceptions) are very common exceptions. Before JDK 14, when encountering NPE exceptions, the information available was limited. JDK 15 introduced a new feature called "Helpful NullPointerExceptions". This Feature improved NullPointerException (NPE) diagnostics. In previous JDK versions, when a NullPointerException occurred, the exception information often did not provide enough context to help developers locate the specific location of the problem.

Sample code:

class A {
    String s;
    public A(String s) {
        this.s = s;
    }
}

class B {
    A a;
    public B(A a) {
        this.a = a;
    }
}

class C {
    B b;
    public C(B b) {
        this.b = b;
    }
}

public class BetterNullPointerReports {

    public static void main(String[] args) {
        C[] ca = {
                new C(new B(new A(null))),
                new C(new B(null)),
                new C(null)
        };

        for (C c : ca) {
            try {
                System.out.println(c.b.a.s);
            } catch (NullPointerException npe) {
                System.out.println(npe);
            }
        }
    }
}

In JDK 14 and earlier versions, the output result is:

# You can’t see what went wrong at all
null
java.lang.NullPointerException
java.lang.NullPointerException

In JDK 15 and later versions, the output result is:

# 得到更详细的 NPE 信息
null
java.lang.NullPointerException: Cannot read field "s" because "c.b.a" is null
java.lang.NullPointerException: Cannot read field "a" because "c.b" is null

清道夫:finally

当程序发生了非预期的异常,那么程序会终止运行,但是对于很多需要执行清理操作,这是不可接受的,例如:

  • 关闭资源:在 try 块中打开的资源,如文件、数据库连接、网络连接等,需要在完成操作后确保被正确关闭

  • 释放锁:在并发编程中,可能会使用锁来同步代码。在释放锁之前,如果发生异常,可能会导致其他线程无法获取锁

  • 回滚事务:在数据库编程中,可能需要在事务中执行一系列操作。如果这些操作中的任何一个失败,事务需要回滚

  • 恢复状态:在执行某些操作时,可能需要更改对象或系统的状态。在操作完成后,可能需要恢复原始状态

对于以上的程序来说,finally 就非常重要了,它可以解决以上程序的清理操作。

示例代码:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class FinallyExample {

    public static void main(String[] args) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader("example.txt"));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.err.println("Error closing file: " + e.getMessage());
                }
            }
        }
    }
}

在以上代码中,无论程序是否出错,finally 都可以确保文件被正确关闭

异常的约束

Java 在面向对象中对异常存在颇多约束和限制,其主要目的如下:

  • 保持子类型可替换性:当子类覆盖父类的方法或实现接口的方法时,子类的方法应该满足父类或接口方法的约定

  • 避免意外的异常:如果子类方法可以抛出任意异常,那么调用者在处理异常时可能遇到意外的异常类型,导致程序出错

  • 提高代码可读性:通过限制异常的继承和实现规则,可以使得代码更加清晰和易于理解

  • 促进良好的设计实践:如果子类方法可以抛出任意异常,那么程序员可能会过度依赖异常来处理错误情况,导致代码难以维护

在接口和继承中使用异常,需要遵循以下规则:

  • 子类可以抛出与接口或者父类方法相同的异常。

  • 子类可以不抛出任何异常,即使接口或父类方法声明了异常。这意味着实现类的方法已经处理了这些异常。

  • 子类可以抛出接口方法或父类声明异常的相同类型的异常,因为子类异常依然符合接口方法的约定。

代码示例:

class CustomException extends RuntimeException {}
class CustomExceptionChild extends CustomException {}
interface MyInterface {
    void myMethod() throws CustomException;
}

class MyClass1 implements MyInterface {
    // 1:抛出与接口方法相同异常
    @Override
//    public void myMethod() throws FileNotFoundException {     // 编译错误,不能抛出不同类型的异常
    public void myMethod() throws CustomException {
        // ...
    }
}

class MyClass2 implements MyInterface {
    // 2:即使不抛出任何异常,也没有问题
    @Override
    public void myMethod() {
    }
}

class MyClass3 implements MyInterface {
    // 3: 抛出接口方法声明异常的子类异常(或者父类,既相同类型即可)
    @Override
    public void myMethod() throws CustomExceptionChild {
        // ...
    }
}

try-with-resources

在 Java 7 中,关于自动管理资源。在处理需要关闭的资源(如文件、数据库连接、网络连接等)有了更好的选择,那就是使用 try-catch-finally 进行处理,它对比 finally 具有以下优势:

  • 简化代码:相比 finally 显示关闭资源,使用 Try-With-Resources,可以自动关闭资源,从而使代码更简洁、易读。

  • 避免资源泄漏:使用 Try-With-Resources 能够确保在退出 try 代码块时自动关闭资源,降低资源泄漏的风险。

  • 减少错误:Try-With-Resources 能够正确地处理资源关闭过程中的异常,并提供完整的异常信息,有助于减少错误。

示例代码:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryWithResourcesExample {

    public static void main(String[] args) {
        String fileName = "example.txt";

        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
}

在这个示例中,我们使用 Try-With-Resources 语句创建了一个 BufferedReader 实例。BufferedReader 实现了 Closeable 接口,因此在退出 try 代码块时,reader 会自动调用 close() 方法以释放资源。

为了一探 Try-With-Resources 的究竟,我们可以创建自定义的 AutoCloseable 类:

class Reporter implements AutoCloseable {
    String name = getClass().getSimpleName();
    public Reporter() {
        System.out.println("Creating " + name);
    }
    @Override
    public void close() throws Exception {
        System.out.println("Closing " + name);
    }
}
class First extends Reporter {}
class Second extends Reporter {}

public class AutoCloseableDetails {
    public static void main(String[] args) {
        try (First f = new First(); Second s = new Second()) {
            System.out.println("In body");
        } catch (Exception e) {
            System.out.println("Exception caught");
        }
    }
}

输出结果:

Creating First
Creating Second
In body
Closing Second
Closing First

退出 try 块会调用两个对象的 close() 方法,并以与创建顺序相反的顺序关闭它们。(顺序很重要)。

使用 Try-With-Resource 是很安全的,假设你随意在 Try 头使用对象,会出现编译错误:

class Anything {}

public class TryAnything {
    public static void main(String[] args) {
        // 假设我们定义的类,不是 AutoCloseable 的对象,会出现编译错误
        try (Anything a = new Anything()) {     // compile error
            System.out.println("In body");
        } catch (Exception e) {
            System.out.println("Exception caught");
        }
    }
}

异常类型匹配

Java 在抛出异常时,会根据异常类型进行匹配。异常处理程序会从上到下依次检查 catch 子句,看它们是否与抛出的异常类型兼容。当发现兼容的 catch 子句时,Java 就会执行该子句的代码来处理异常。请注意,Java 只会执行与抛出异常类型兼容的第一个 catch 子句。

示例代码:

public class ExceptionMatchingExample {
    public static void main(String[] args) {
        try {
            // ... Some code that may throw exceptions ...
            throw new FileNotFoundException("File not found");
        } catch (FileNotFoundException e) {
            System.out.println("Handling FileNotFoundException: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("Handling IOException: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("Handling general exception: " + e.getMessage());
        }
    }
}

在这个示例中,我们抛出了一个 FileNotFoundException,Java 会从上到下检查 catch 子句,看它们是否与 FileNotFoundException 兼容。因为 FileNotFoundException 是 IOException 的子类,它与 FileNotFoundException 和 IOException 的 catch 子句兼容。但是,Java 只会执行第一个兼容的 catch 子句,即 FileNotFoundException 子句。如果没有找到兼容的 catch 子句,Java 会继续在调用栈中查找异常处理程序,直到找到一个合适的处理程序或者程序终止。

输出结果:

Handling FileNotFoundException: File not found

使用指南

异常看似简单易懂,但在处理过程中还需要遵循许多最佳实践,例如:

  • 除非你知道如何处理,否则不要捕获异常(错误处理代码太多,容易干扰主线代码的逻辑和可读性)

  • 不要生吞异常:捕获异常不进行处理会导致异常消失,从而对于线上问题排查,无从下手

  • 捕获具体异常:尽量捕获具体的异常类,而不是捕获泛化的 Exception 类

  • 尽可能的使用多重异常捕获来简化重复代码,并且提高代码的可读性

  • 尽可能的使用 Try-With-Resources 清理资源

  • 自定义异常:在需要时,为特定于你的应用程序的异常情况创建自定义异常

检查型异常是 shit

检查型异常(checked exceptions)在 Java 中引发了很多争议。有些人认为它们是一种有益的设计,可以提高代码的可靠性,而另一些人则认为它们是一种糟糕的设计,会导致代码冗余和难以维护,例如 Martin Fowler (《UML 精粹》、《重构》)作者,也曾在博客发表称:

总的来说,我认为异常很不错,但是 Java 的检查型异常要比好处多

那么检查型异常究竟带来了什么问题 ? 常见的槽点有:

  • 强制错误处理:检查型异常强制开发者处理异常情况,导致主线代码中充斥着大量和业务逻辑无关的代码

  • 代码冗余:检查型异常可能导致大量的 try-catch 代码块,增加代码冗余,影响代码可读性

  • 异常传递:对于一些需要在多层方法调用中传递异常的情况,检查型异常可能导致开发者不得不为每个方法添加异常声明

Go 也没有异常啊

最几年 Go 语言的成功让很多人加深了这一观点,Go 语言没有检查型异常的概念,但它们的代码依然可以具有很高的可靠性。这表明检查型异常并非是提高代码可靠性的唯一方法。Go 语言的设计者们有意避免了引入检查型异常,主要有以下原因:

  • 代码简洁性:Go 语言的设计者们希望保持代码简洁,避免因为异常处理而产生的大量冗余代码。

  • 显示错误处理:Go 语言鼓励开发者显式地处理错误,而不是通过异常机制隐式地处理。

  • 降低复杂性:异常机制会增加程序的复杂性。Go 语言的设计者们希望通过避免引入异常机制,让程序更简单易懂。

  • 性能开销:异常处理机制可能会带来一定的性能开销,通过返回 error 类型,可以避免这种性能开销。

示例代码:

package main

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }

    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

在这个示例中,我们定义了一个名为divide的函数,它接受两个整数参数ab,计算它们的商。如果b为零,函数将返回一个非空的error类型值,以指示发生了错误。否则,函数将返回商和一个空error值。在 main 函数中,我们调用divide两次,一次使用一个非零除数,另一次使用零作为除数。对于第一次调用,divide将返回一个空的error值,我们就可以打印出计算结果。对于第二次调用,divide将返回一个非空的error值,我们使用if err != nil来检查这个值是否为nil,如果不是,就打印错误信息。

输出结果:

Result: 5
Error: division by zero

The above is the detailed content of How to use Java exception handling mechanism?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete