杰瑞科技汇

Java异常处理有哪些最佳实践?

将从以下几个方面展开,希望能帮助你彻底理解 Java 异常机制:

Java异常处理有哪些最佳实践?-图1
(图片来源网络,侵删)
  1. 什么是异常?
  2. 异常的体系结构
  3. 异常的分类
  4. 异常的处理关键字
    • try-catch-finally
    • throw
    • throws
  5. 自定义异常
  6. 最佳实践和常见误区

什么是异常?

在 Java 中,异常是程序在运行过程中出现的不正常事件,它会中断正常的指令流程。

  • 算术异常10 / 0
  • 空指针异常String str = null; str.length();
  • 数组越界异常int[] arr = new int[3]; arr[3];
  • 文件未找到异常:读取一个不存在的文件。

Java 通过异常处理机制来管理这些错误,使得程序可以从错误中恢复,或者至少以一种优雅的方式终止,而不是直接崩溃。


异常的体系结构

所有异常类都继承自 java.lang.Throwable 类。Throwable 有两个重要的子类:

  • Error (错误):表示严重的问题,通常是 JVM 层面的错误,OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出),这类错误不应该被捕获,因为它们是程序无法处理的,一旦发生,JVM 通常会选择终止线程。
  • Exception (异常):表示程序本身可以处理的异常,这是我们日常编码中主要关注的部分。

Exception 体系又可以分为两大类:

Java异常处理有哪些最佳实践?-图2
(图片来源网络,侵删)
  • 受检异常
  • 非受检异常

异常的分类

这是理解异常的核心。

A. 受检异常

  • 特点:编译器会检查它,如果一个方法可能会抛出受检异常,那么该方法必须使用 throws 关键字声明,或者使用 try-catch 块来捕获它,否则,代码将无法通过编译。
  • 来源:通常是由外部环境引起的,例如文件操作、网络连接、数据库访问等,这些操作的成功与否不完全由程序控制。
  • 示例
    • IOException:当发生 I/O 错误时抛出。
    • SQLException:与数据库访问相关的错误。
    • FileNotFoundException:当尝试打开一个不存在的文件时抛出。

示例代码:

import java.io.FileInputStream;
import java.io.IOException;
public class CheckedExceptionExample {
    public static void main(String[] args) {
        // readFile() 方法声明了可能抛出 IOException,所以我们必须处理它
        try {
            readFile("non_existent_file.txt");
        } catch (IOException e) {
            System.out.println("文件读取失败,请检查文件路径: " + e.getMessage());
        }
    }
    public static void readFile(String path) throws IOException {
        // 这行代码可能会抛出受检异常 FileNotFoundException (IOException的子类)
        FileInputStream fis = new FileInputStream(path);
        // ... 读取文件
    }
}

如果不使用 try-catch 或在 main 方法上加上 throws IOException,编译器会报错。

B. 非受检异常

  • 特点:编译器不会检查它,它通常是程序逻辑错误导致的,理论上程序员应该通过修正代码来避免。
  • 来源:通常是程序员的错误,例如空指针引用、数组越界、非法的参数类型等。
  • 示例
    • RuntimeException:所有运行时异常的基类。
    • NullPointerException:当试图在 null 对象上调用方法或访问字段时抛出。
    • ArrayIndexOutOfBoundsException:当访问数组中不存在的索引时抛出。
    • ArithmeticException:当出现数学运算错误时抛出,如除以零。
    • IllegalArgumentException:当方法接收到非法或不适当的参数时抛出。

示例代码:

public class UncheckedExceptionExample {
    public static void main(String[] args) {
        String str = null;
        // 这行代码会抛出 NullPointerException,但编译器不会强制你处理
        // System.out.println(str.length()); // 取消这行注释会运行时出错
        int[] arr = {1, 2, 3};
        // 这行代码会抛出 ArrayIndexOutOfBoundsException
        // System.out.println(arr[3]); // 取消这行注释会运行时出错
    }
}

编译器不会检查这些异常,你可以选择捕获,也可以选择不捕获,如果未捕获,程序会在运行时中断。


异常的处理关键字

Java 提供了三个关键字来处理异常:try, catch, finally, throw, throws

try-catch-finally

这是捕获和处理异常的核心结构。

  • try:将可能抛出异常的代码块包裹起来。
  • catch:捕获 try 块中发生的特定类型的异常,并进行处理,可以有多个 catch 块来捕获不同类型的异常。
  • finally:无论是否发生异常,finally 块中的代码一定会被执行,通常用于资源释放(如关闭文件、数据库连接等)。

语法结构:

try {
    // 可能会抛出异常的代码
} catch (ExceptionType1 e1) {
    // 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
    // 处理 ExceptionType2 类型的异常
} finally {
    // 无论是否发生异常,都会执行的代码
}

示例:

public class TryCatchFinallyExample {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        int result = 0;
        try {
            // 可能会抛出 ArithmeticException 的代码
            result = a / b;
            System.out.println("计算结果: " + result);
        } catch (ArithmeticException e) {
            // 捕获并处理异常
            System.out.println("捕获到异常: " + e.getMessage());
            System.out.println("不能除以零!");
        } finally {
            // 无论是否发生异常,这里都会执行
            System.out.println("finally 块被执行,用于清理资源。");
        }
        System.out.println("程序继续执行...");
    }
}

输出:

捕获到异常: / by zero
不能除以零!
finally 块被执行,用于清理资源。
程序继续执行...

throw

throw 用于手动抛出一个异常,它通常用在方法体内部,表示程序在某个条件下主动创建并抛出一个异常对象。

语法:

throw new Exception("异常信息");

示例:

public class ThrowExample {
    public static void checkAge(int age) {
        if (age < 18) {
            // 手动抛出一个 IllegalArgumentException
            throw new IllegalArgumentException("年龄必须大于或等于18岁!");
        }
        System.out.println("年龄合法: " + age);
    }
    public static void main(String[] args) {
        try {
            checkAge(15);
        } catch (IllegalArgumentException e) {
            System.out.println("捕获到异常: " + e.getMessage());
        }
    }
}

输出:

捕获到异常: 年龄必须大于或等于18岁!

throws

throws 用于在方法签名中声明该方法可能会抛出的异常(通常是受检异常),它告诉调用者:“我可能会抛出这些异常,请你自己处理”。

语法:

public void methodName() throws ExceptionType1, ExceptionType2 {
    // 方法体
}

throw 的区别:

  • throw:是动作,在代码中主动抛出一个异常对象。
  • throws:是声明,在方法签名上声明该方法可能抛出的异常类型,将处理责任交给调用者。

示例:

public class ThrowsExample {
    // readFile 方法声明它可能会抛出 IOException
    public static void readFile(String path) throws IOException {
        FileInputStream fis = new FileInputStream(path);
        // ...
    }
    public static void main(String[] args) {
        // main 方法是 readFile 的调用者,所以必须处理 IOException
        try {
            readFile("test.txt");
        } catch (IOException e) {
            System.out.println("处理文件异常: " + e.getMessage());
        }
    }
}

自定义异常

当 Java 内置的异常无法满足你的业务需求时,你可以创建自己的异常类,最佳实践是继承 Exception(用于受检异常)或 RuntimeException(用于非受检异常)。

示例:

// 自定义一个受检异常
class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(String message) {
        super(message); // 调用父类 Exception 的构造方法
    }
}
// 自定义一个非受检异常
class InvalidAgeRuntimeException extends RuntimeException {
    public InvalidAgeRuntimeException(String message) {
        super(message);
    }
}
public class CustomExceptionExample {
    public static void withdraw(double balance, double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            // 抛出自定义的受检异常
            throw new InsufficientBalanceException("余额不足!");
        }
        System.out.println("取款成功,剩余余额: " + (balance - amount));
    }
    public static void checkAge(int age) {
        if (age < 0) {
            // 抛出自定义的非受检异常
            throw new InvalidAgeRuntimeException("年龄不能为负数!");
        }
    }
    public static void main(String[] args) {
        try {
            withdraw(100, 150); // 这会触发受检异常
        } catch (InsufficientBalanceException e) {
            System.out.println("捕获到自定义异常: " + e.getMessage());
        }
        checkAge(-5); // 这会触发非受检异常
    }
}

输出:

捕获到自定义异常: 余额不足!
Exception in thread "main" InvalidAgeRuntimeException: 年龄不能为负数!
    at CustomExceptionExample.checkAge(CustomExceptionExample.java:26)
    at CustomExceptionExample.main(CustomExceptionExample.java:33)

最佳实践和常见误区

最佳实践

  1. 具体捕获:尽量捕获具体的异常类型,而不是笼统地捕获 Exception,这样可以更精确地处理问题。

    // 不推荐
    try {
        // ...
    } catch (Exception e) { ... }
    // 推荐
    try {
        // ...
    } catch (IOException e) { ... } // 只处理IO异常
    catch (SQLException e) { ... }  // 只处理数据库异常
  2. 不要吞掉异常:在 catch 块中只打印堆栈信息或日志,但什么都不做,这会隐藏问题,让调试变得困难。

    // 不推荐
    try {
        // ...
    } catch (IOException e) {
        e.printStackTrace(); // 至少要记录下来
    }
    // 推荐
    try {
        // ...
    } catch (IOException e) {
        // 记录详细的日志,包括上下文信息
        logger.error("读取文件失败,路径: " + path, e);
        // 可以选择重新抛出或进行恢复
        throw new BusinessException("文件处理失败", e);
    }
  3. 使用 finallytry-with-resources 释放资源:确保文件、数据库连接、网络流等资源被正确关闭。

    // 传统方式
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("file.txt");
        // ...
    } catch (IOException e) {
        // ...
    } finally {
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                // ...
            }
        }
    }
    // Java 7+ 推荐方式:try-with-resources
    try (FileInputStream fis = new FileInputStream("file.txt")) {
        // ...
    } catch (IOException e) {
        // ...
    } // fis.close() 会自动被调用
  4. 优先使用异常而不是返回码:异常机制提供了更清晰的流程控制,让代码逻辑更易读,不要用 if-else 来模拟异常处理。

常见误区

  1. 将异常用于流程控制:用 try-catch 来处理本可以用 if-else 判断的正常逻辑,这会降低性能,并使代码难以理解。

    // 错误示范
    try {
        int result = 10 / 0; // 想用异常来处理除数为零的情况
    } catch (ArithmeticException e) {
        // ...
    }
    // 正确做法
    if (b != 0) {
        int result = a / b;
    }
  2. 过度使用受检异常:如果异常是程序逻辑错误(如空指针、非法参数),应该使用非受检异常(RuntimeException),这样可以减少不必要的 try-catchthrows,让代码更简洁。

  3. catch 块中只打印 e.getMessage()e.getMessage() 可能会丢失异常的堆栈信息,这会给问题排查带来巨大困难,应该记录完整的堆栈信息,例如使用 logger.error("Error message", e);

希望这份详细的梳理能帮助你全面掌握 Java 异常机制!

分享:
扫描分享到社交APP
上一篇
下一篇