Rumah  >  Artikel  >  Java  >  Bagaimana untuk menggunakan mekanisme pengendalian pengecualian Java?

Bagaimana untuk menggunakan mekanisme pengendalian pengecualian Java?

PHPz
PHPzke hadapan
2023-05-09 16:07:071345semak imbas

Konsep

Konsep pengendalian pengecualian berasal dari bahasa pengaturcaraan awal seperti LISP, PL/I dan CLU. Buat pertama kalinya, bahasa pengaturcaraan ini memperkenalkan mekanisme pengendalian pengecualian untuk mengesan dan mengendalikan keadaan ralat semasa pelaksanaan program. Mekanisme pengendalian pengecualian kemudiannya telah diterima pakai secara meluas dan dibangunkan dalam bahasa pengaturcaraan seperti Ada, Modula-3, C++, Python, dan Java. Di Java, pengendalian pengecualian menyediakan cara untuk mengendalikan ralat dan pengecualian semasa program sedang berjalan. Mekanisme pengendalian pengecualian membenarkan atur cara untuk terus melaksanakan apabila ralat ditemui, dan bukannya ranap serta-merta. Mekanisme ini menjadikan program lebih teguh dan tahan terhadap kesalahan. Pengecualian dibahagikan kepada dua kategori: Pengecualian yang Disemak dan Pengecualian Tidak Ditanda

Pengecualian yang Disemak:

Pengecualian yang Disemak ialah yang mesti dikendalikan pada masa penyusunan. Ia biasanya disebabkan oleh ralat pengaturcara atau masalah dengan sumber luaran. Contohnya, IOException, FileNotFoundException, dsb. Pengecualian yang ditandai mesti diisytiharkan menggunakan kata kunci throws dalam tandatangan kaedah, atau ditangkap dan dikendalikan dalam badan kaedah menggunakan blok try-catch.

Pengecualian Tidak Ditanda:

Pengecualian yang tidak ditanda merujuk kepada pengecualian yang tidak wajib dikendalikan pada masa penyusunan. Ia biasanya disebabkan oleh ralat pengaturcaraan, seperti pengecualian penuding nol (NullPointerException), tatasusunan di luar sempadan (ArrayIndexOutOfBoundsException), dsb. Pengecualian yang tidak ditandai mewarisi daripada kelas java.lang.RuntimeException dan tidak perlu diisytiharkan dalam tandatangan kaedah, dan juga tidak perlu dipaksa untuk ditangkap dan dikendalikan.

Hubungan mereka adalah seperti berikut:

Bagaimana untuk menggunakan mekanisme pengendalian pengecualian Java?

Pengendalian pengecualian

Java menggunakan kata kunci try/catch untuk tangkapan pengecualian dan menggunakan throw pernyataan Melemparkan pengecualian, kod sampel adalah seperti berikut:

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();
        }
    }
}

Dalam contoh ini, kami cuba mendapatkan panjang rentetan null. Apabila nullString.length() dipanggil, NullPointerException dilemparkan. Kami menggunakan pernyataan try-catch untuk menangkap pengecualian dan mengendalikannya

Pengecualian tersuai

Pengecualian rasmi Java tidak dapat meramalkan semua kemungkinan ralat Kadangkala anda perlu menggabungkannya dengan senario perniagaan anda sendiri, seperti seperti Senario berikut:

  • Apabila kelas pengecualian Java terbina dalam tidak dapat menerangkan dengan tepat situasi pengecualian yang anda hadapi.

  • Set pengecualian khusus perlu dibuat untuk domain atau logik perniagaan tertentu.

  • Apabila anda ingin memberikan lebih banyak maklumat kontekstual atau kod ralat khusus kepada pemanggil melalui kelas pengecualian tersuai.

Membina pengecualian khusus juga sangat mudah Warisi kelas pengecualian yang sedia ada (sebaik-baiknya mewarisi satu dengan makna yang sama Seperti berikut, kami mencipta pengecualian yang menunjukkan baki akaun tidak mencukupi.

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;
    }
}

Seterusnya, kami menggunakan pengecualian tersuai ini dalam kod logik perniagaan:

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;
    }
}

Pemanggil boleh menangkap dan mengendalikan pengecualian tersuai ini:

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);
        }
    }
}

boleh melihat pengecualian tersuai Menentukan pengecualian membolehkan kami menyatakan dengan lebih jelas kemungkinan pengecualian dalam logik perniagaan sambil memberikan pemanggil maklumat yang lebih kontekstual tentang pengecualian tersebut. Kami juga menggunakan alat java.util.logging untuk merekod output ke log

Berbilang tangkapan

Dalam versi awal Java, untuk mengendalikan berbilang pengecualian tanpa kelas asas biasa, anda perlu menulis pengecualian untuk setiap jenis pengecualian Pemprosesan pernyataan tangkapan adalah seperti berikut:

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());
        }
    }
}

Kod sedemikian bukan sahaja sukar dibaca, tetapi juga tidak cukup ringkas.

Mekanisme penangkapan pengecualian berbilang, yang membolehkan penangkapan berbilang jenis pengecualian dalam satu pernyataan catch. Pendekatan ini mengelakkan pertindihan kod dan menjadikan pengendalian pengecualian lebih ringkas. Berikut ialah contoh menggunakan berbilang mekanisme penangkapan pengecualian:

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());
        }
    }
}

Pengecualian melontar semula

Dalam sesetengah kes, anda mungkin mahu memberikan pengecualian kepada pemanggil untuk dikendalikan dan bukannya dalam kaedah semasa dalam pemprosesan. Atau anda perlu melakukan beberapa operasi pemprosesan apabila menangkap pengecualian, seperti mengelog, membersihkan sumber atau menambah maklumat konteks tambahan. Dalam kes ini, anda boleh mengendalikan pengecualian dalam blok catch dan kemudian melontar semula pengecualian asal atau membuang pengecualian baharu dengan maklumat tambahan, seperti ini:

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

NPE yang Lebih Baik

NPE ( NullPointerExceptions) adalah pengecualian yang sangat biasa Sebelum JDK 14, apabila pengecualian NPE ditemui, maklumat yang tersedia adalah terhad. Dalam versi JDK sebelumnya, apabila NullPointerException berlaku, maklumat pengecualian selalunya tidak menyediakan konteks yang mencukupi untuk membantu pembangun mencari lokasi khusus masalah.

Kod sampel:

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);
            }
        }
    }
}

Dalam JDK 14 dan versi terdahulu, hasil output:

# Anda tidak dapat melihat apa yang salah sama sekali
null
java.lang.NullPointerException
java.lang.NullPointerException

Dalam JDK 15 dan versi yang lebih baru, hasil output ialah:

# 得到更详细的 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

Atas ialah kandungan terperinci Bagaimana untuk menggunakan mekanisme pengendalian pengecualian Java?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam