Skip to content

Java异常处理详解

异常概述

异常是程序运行过程中发生的不正常事件,它中断了正常的指令流。Java使用面向对象的方式来处理异常,通过异常机制可以优雅地处理程序中的错误情况,提高程序的健壮性。

异常体系结构

Java的异常体系以Throwable类为根,派生出两大子类:ErrorException

异常层次结构

Throwable
├── Error
│   ├── VirtualMachineError
│   │   ├── OutOfMemoryError
│   │   └── StackOverflowError
│   └── LinkageError
└── Exception
    ├── RuntimeException
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   ├── ArithmeticException
    │   └── IllegalArgumentException
    ├── IOException
    │   ├── FileNotFoundException
    │   └── EOFException
    ├── SQLException
    └── ClassNotFoundException

Error与Exception的区别

  • Error:表示严重错误,程序一般无法恢复,如JVM内存溢出、栈溢出等。不建议捕获和处理。
  • Exception:表示可以处理的异常,是我们在编程中主要关注的部分。
    • Checked Exception(受检异常):必须显式捕获或声明抛出,如IOExceptionSQLException
    • Unchecked Exception(非受检异常):可以不显式处理,通常是程序逻辑错误,如NullPointerExceptionArrayIndexOutOfBoundsException

异常处理机制

try-catch-finally 语句

基本语法

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

示例

java
public class ExceptionExample {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0);
            System.out.println("结果: " + result);
        } catch (ArithmeticException e) {
            System.out.println("捕获到算术异常: " + e.getMessage());
        } finally {
            System.out.println("finally块一定会执行");
        }
    }
    
    public static int divide(int a, int b) {
        return a / b;
    }
}

执行结果

捕获到算术异常: / by zero
finally块一定会执行

多重catch块

当可能抛出多种类型的异常时,可以使用多个catch块来分别处理。

注意事项

  • 异常捕获的顺序很重要,应该先捕获子类异常,再捕获父类异常
  • 否则子类异常的catch块永远不会被执行

示例

java
try {
    String str = null;
    System.out.println(str.length());
    int[] arr = new int[5];
    arr[10] = 100;
} catch (NullPointerException e) {
    System.out.println("空指针异常: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("数组越界异常: " + e.getMessage());
} catch (Exception e) {
    System.out.println("其他异常: " + e.getMessage());
}

try-with-resources 语句

Java 7引入的try-with-resources语句用于自动关闭实现了AutoCloseable接口的资源,无需在finally块中手动关闭。

语法

java
try (ResourceType resource = new ResourceType();
     AnotherResourceType anotherResource = new AnotherResourceType()) {
    // 使用资源的代码
} catch (Exception e) {
    // 异常处理
}

示例

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

public class TryWithResourcesExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("读取文件异常: " + e.getMessage());
        }
    }
}

throws 与 throw 关键字

throws 关键字

用于在方法声明中指定该方法可能抛出的异常类型,将异常责任传递给调用者。

语法

java
public returnType methodName(paramList) throws ExceptionType1, ExceptionType2 {
    // 方法体
}

示例

java
public class ThrowsExample {
    public static void main(String[] args) {
        try {
            readFile("file.txt");
        } catch (IOException e) {
            System.out.println("处理异常: " + e.getMessage());
        }
    }
    
    public static void readFile(String fileName) throws IOException {
        // 可能抛出IOException的代码
    }
}

throw 关键字

用于在方法内部抛出一个具体的异常对象。

语法

java
throw new ExceptionType("异常信息");

示例

java
public class ThrowExample {
    public static void main(String[] args) {
        try {
            checkAge(15);
        } catch (IllegalArgumentException e) {
            System.out.println("年龄检查异常: " + e.getMessage());
        }
    }
    
    public static void checkAge(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("年龄必须大于等于18岁");
        }
        System.out.println("年龄检查通过");
    }
}

自定义异常

在实际开发中,我们可以创建自定义异常来处理特定业务场景下的异常情况。

创建自定义异常类

步骤

  1. 继承Exception类(受检异常)或RuntimeException类(非受检异常)
  2. 提供构造方法

示例

java
// 自定义受检异常
public class InsufficientFundsException extends Exception {
    private double amount;
    
    public InsufficientFundsException(double amount) {
        super("余额不足: 还需要 " + amount + " 元");
        this.amount = amount;
    }
    
    public double getAmount() {
        return amount;
    }
}

// 自定义非受检异常
public class InvalidAccountException extends RuntimeException {
    public InvalidAccountException(String message) {
        super(message);
    }
}

使用自定义异常

java
public class BankAccount {
    private double balance;
    
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }
    
    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(amount - balance);
        }
        balance -= amount;
    }
    
    public void transfer(BankAccount target, double amount, String targetAccountId) {
        if (target == null) {
            throw new InvalidAccountException("目标账户无效: " + targetAccountId);
        }
        
        try {
            this.withdraw(amount);
            target.balance += amount;
        } catch (InsufficientFundsException e) {
            System.out.println("转账失败: " + e.getMessage());
        }
    }
}

异常链

异常链是指在捕获一个异常后,再抛出另一个异常,但同时保留原有异常的信息。这在处理高级异常时非常有用。

使用 initCause() 方法

java
try {
    // 可能抛出SQLException的代码
} catch (SQLException e) {
    // 创建新的业务异常,并设置原始异常为cause
    BusinessException businessException = new BusinessException("数据库操作失败");
    businessException.initCause(e);
    throw businessException;
}

在构造方法中设置 cause

java
// 自定义异常类,支持cause参数
public class BusinessException extends Exception {
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}

// 使用
try {
    // 可能抛出SQLException的代码
} catch (SQLException e) {
    throw new BusinessException("数据库操作失败", e);
}

异常链的传播

java
try {
    methodA();
} catch (BusinessException e) {
    System.out.println("业务异常: " + e.getMessage());
    System.out.println("根本原因: " + e.getCause().getMessage());
}

public static void methodA() throws BusinessException {
    try {
        methodB();
    } catch (DataAccessException e) {
        throw new BusinessException("业务处理失败", e);
    }
}

public static void methodB() throws DataAccessException {
    try {
        // 数据库操作
    } catch (SQLException e) {
        throw new DataAccessException("数据访问失败", e);
    }
}

异常处理最佳实践

1. 只捕获真正能处理的异常

java
// 不推荐
try {
    // 复杂操作
} catch (Exception e) {
    // 过于宽泛的异常捕获
    System.out.println("发生错误");
}

// 推荐
try {
    // 文件操作
} catch (IOException e) {
    // 具体的异常处理
    log.error("文件读取失败", e);
    // 提供恢复机制或合理的默认行为
}

2. 避免空的catch块

空的catch块会隐藏错误,使调试变得困难。

java
// 不推荐
try {
    // 可能抛出异常的代码
} catch (Exception e) {
    // 空的catch块
}

// 推荐
try {
    // 可能抛出异常的代码
} catch (Exception e) {
    // 至少记录异常信息
    log.error("处理失败", e);
}

3. 尽早抛出,延迟捕获

  • 尽早抛出异常:在发现错误时立即抛出
  • 延迟捕获异常:在能有效处理异常的地方再捕获

4. 关闭资源

总是确保资源被正确关闭,使用try-with-resources是最佳选择。

5. 提供有用的异常信息

异常信息应该清晰描述问题,便于调试。

java
// 不推荐
throw new Exception("错误");

// 推荐
throw new IllegalArgumentException("用户名必须包含至少5个字符,当前: " + username.length());

6. 避免过度使用异常

异常处理的开销较大,不要用异常来控制正常的程序流程。

java
// 不推荐
try {
    int index = list.indexOf(element);
    if (index == -1) {
        throw new ElementNotFoundException("元素未找到");
    }
} catch (ElementNotFoundException e) {
    // 处理元素未找到的情况
}

// 推荐
int index = list.indexOf(element);
if (index == -1) {
    // 直接处理元素未找到的情况
    return defaultValue;
}

7. 区分Checked和Unchecked异常

  • Checked异常:用于可恢复的错误,如资源不可用、业务规则违反等
  • Unchecked异常:用于编程错误,如空指针、数组越界等

8. 不要忽略异常

即使知道某些异常不太可能发生,也不要完全忽略它们。

常见异常类型

RuntimeException子类

异常类型说明常见原因
NullPointerException空指针异常尝试访问null对象的属性或方法
ArrayIndexOutOfBoundsException数组索引越界使用超出数组范围的索引
ClassCastException类型转换异常尝试将对象转换为不兼容的类型
ArithmeticException算术异常除以零等算术错误
IllegalArgumentException非法参数异常传入方法的参数不符合要求
IllegalStateException非法状态异常对象处于不适合执行请求操作的状态
IndexOutOfBoundsException索引越界异常集合、字符串等索引超出范围
ConcurrentModificationException并发修改异常在迭代集合时修改集合

Checked Exception子类

异常类型说明常见原因
IOException输入输出异常文件读写、网络通信等IO操作失败
FileNotFoundException文件未找到异常尝试访问不存在的文件
SQLExceptionSQL异常数据库操作失败
ClassNotFoundException类未找到异常尝试加载不存在的类
InterruptedException线程中断异常线程在睡眠或等待时被中断
ParseException解析异常解析字符串为其他类型失败
UnsupportedEncodingException不支持的编码异常使用不支持的字符编码

异常处理与日志记录

异常处理与日志记录密切相关,记录异常信息有助于调试和监控系统。

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExceptionLoggingExample {
    private static final Logger logger = LoggerFactory.getLogger(ExceptionLoggingExample.class);
    
    public void process() {
        try {
            // 业务逻辑
        } catch (IOException e) {
            // 记录异常信息,包括堆栈跟踪
            logger.error("处理文件时发生IO异常", e);
            // 重新抛出或处理异常
            throw new BusinessException("文件处理失败", e);
        } catch (Exception e) {
            // 记录未预期的异常
            logger.error("发生未预期的异常", e);
            // 处理或向上传递
        }
    }
}

总结

异常处理是Java编程中不可或缺的一部分,合理地使用异常处理机制可以使程序更加健壮和可维护。通过本文的学习,我们应该掌握:

  1. Java异常体系的结构
  2. 异常处理的基本语法(try-catch-finally, try-with-resources)
  3. throws和throw关键字的使用
  4. 如何创建和使用自定义异常
  5. 异常链的概念和应用
  6. 异常处理的最佳实践

在实际开发中,应该根据具体场景选择合适的异常处理策略,既要保证程序的健壮性,也要兼顾代码的可读性和性能。