java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java常见Exception异常

全面解析Java中常见Exception异常的错误排查与代码修正

作者:百锦再@新空间创想科技

这篇文章主要为大家详细介绍了Java开发中最常见的异常类型及其处理方法,包括运行时异常(如NullPointerException,ArrayIndexOutOfBoundsException)和受检异常,下面小编就和大家详细介绍一下吧

前言

适用对象

本课程适合已经掌握Java基础语法,初步了解异常处理概念,但希望系统掌握常见异常排查与修复能力的开发者。无论你是刚入行的新人,还是有一定经验的开发者,这门课程都将帮助你建立系统的异常排查思维,提升代码质量。

学习目标

通过两个课时的系统学习,你将能够:

课程安排

教学方式

每个异常都遵循“现象描述 → 出现场景 → 堆栈分析 → 排查方法 → 代码修正 → 预防措施”的六步教学法,确保理论与实践紧密结合。

第一部分:Java异常体系回顾(约10分钟)

1.1 异常是什么

在深入具体异常之前,我们先理解异常的本质。异常(Exception) 是程序运行过程中出现的打断正常执行流程的事件。它本质上是一个对象,封装了错误类型、错误描述、方法调用堆栈以及可能的底层原因。

1.2 Java异常体系结构

        java.lang.Object
               |
       java.lang.Throwable
               |
      ---------------------
      |                   |
  java.lang.Error     java.lang.Exception
                          |
              -------------------------
              |                       |
   RuntimeException              其他 Exception
   (运行时异常)                  (受检异常)

1.3 异常信息解读

一个典型的异常堆栈包含以下要素:

Exception java.lang.IllegalArgumentException: item quantity must be a number
        at io.jzheaux.pluralsight.DeliController.orderSandwich (DeliController.java:45)
        // …
Caused by java.lang.NumberFormatException: For input string: " 3"
        at NumberFormatException.forInputString (NumberFormatException.java:67)
        at Integer.parseInt (Integer.java:647)
        ...

排查技巧:遇到复杂异常时,不要只看第一行,要顺着堆栈往下找,尤其是“Caused by”部分,那里往往藏着真正的原因。

第二课时(上):运行时异常深度剖析(约30分钟)

运行时异常(RuntimeException)是Java程序中最常见的异常类型,它们通常由代码逻辑错误引起。下面我们将逐个剖析最常见的运行时异常。

2.1 NullPointerException(空指针异常)

现象描述

当应用程序试图在需要对象的地方使用null引用时,抛出此异常。这是Java中最著名的异常,占据了异常总数的很大比例。

出现场景

场景一:直接调用null对象的方法或属性

String text = null;
int length = text.length(); // 抛出NullPointerException

场景二:自动拆箱时包装类型为null

Boolean willVote = null;
if (willVote) { // 自动拆箱时抛出NullPointerException
    System.out.println("可以投票");
}

场景三:方法参数或返回值未做空检查

void parseDocument(Document doc) {
    doc.getElements(); // 如果传入的doc为null,抛出异常
}

String lookupElement(Document doc) {
    Element element = doc.findElement("span");
    return element.getValue(); // 如果element为null,抛出异常
}

场景四:数组元素未初始化

Person[] people = new Person[5];
people[0].getName(); // 数组元素默认为null,抛出异常

堆栈分析示例

Exception in thread "main" java.lang.NullPointerException
    at com.example.UserService.getUserAge(UserService.java:25)
    at com.example.UserController.main(UserController.java:12)

从堆栈可以看出,UserService.java的第25行调用了某个null对象的方法。

排查方法流程图

代码修正与预防

修正方案一:参数校验

// 错误代码
void parseDocument(Document doc) {
    doc.getElements();
}

// 修正代码
void parseDocument(@NonNull Document doc) {
    if (doc == null) {
        throw new IllegalArgumentException("doc cannot be null");
    }
    doc.getElements();
}

修正方案二:使用守卫语句

// 错误代码
String lookupElement(Document doc) {
    Element element = doc.findElement("span");
    return element.getValue();
}

// 修正代码
@Nullable String lookupElement(Document doc) {
    Element element = doc.findElement("span");
    if (element == null) {
        return null; // 或者返回默认值
    }
    return element.getValue();
}

修正方案三:使用Optional(Java 8+)

public Optional<String> lookupElement(Document doc) {
    return Optional.ofNullable(doc.findElement("span"))
                  .map(Element::getValue);
}

修正方案四:数组元素初始化

Person[] people = new Person[5];
for (int i = 0; i < people.length; i++) {
    people[i] = new Person(); // 确保每个元素都被初始化
}

预防措施

2.2 ArrayIndexOutOfBoundsException(数组下标越界异常)

现象描述

当试图使用非法索引访问数组元素时抛出,非法索引包括负数、0到数组长度减1范围外的值。

出现场景

场景一:索引超出数组长度

int[] numbers = {1, 2, 3};
int value = numbers[3]; // 索引3超出范围(有效索引0-2)

场景二:循环条件错误

int[] scores = {85, 90, 78, 92};
for (int i = 0; i <= scores.length; i++) { // 应该是 i < scores.length
    System.out.println(scores[i]); // 最后一次循环i=4,越界
}

场景三:索引为负数

int[] data = new int[10];
int index = -1;
data[index] = 100; // 负索引越界

堆栈分析示例

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
    at com.example.ArrayDemo.processArray(ArrayDemo.java:15)
    at com.example.ArrayDemo.main(ArrayDemo.java:8)

异常信息直接告诉我们:试图访问索引5,但数组长度只有5(有效索引0-4)。

排查方法

代码修正与预防

修正方案一:修正循环边界

// 错误代码
for (int i = 0; i <= scores.length; i++) {
    System.out.println(scores[i]);
}

// 修正代码
for (int i = 0; i < scores.length; i++) {
    System.out.println(scores[i]);
}

// 更好的方式:使用增强for循环
for (int score : scores) {
    System.out.println(score);
}

修正方案二:访问前检查索引

public int getElement(int[] array, int index) {
    if (array == null) {
        throw new IllegalArgumentException("array cannot be null");
    }
    if (index < 0 || index >= array.length) {
        throw new IndexOutOfBoundsException(
            "Index " + index + " out of bounds for length " + array.length);
    }
    return array[index];
}

预防措施

2.3 ClassCastException(类型转换异常)

现象描述

当试图将一个对象强制转换为它不是实例的子类时抛出。这是使用继承和多态时的常见问题。

出现场景

场景一:将父类对象强制转换为子类类型

Object obj = new Object();
Integer num = (Integer) obj; // Object不能转换为Integer

场景二:集合中元素类型不一致

List list = new ArrayList();
list.add("Hello");
list.add(123); // 混合类型

String first = (String) list.get(0); // 正常
String second = (String) list.get(1); // 抛出ClassCastException,123不能转String

场景三:不正确的向下转型

Animal animal = new Dog();
Cat cat = (Cat) animal; // Dog不能转换为Cat

堆栈分析示例

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
    at com.example.GenericDemo.processList(GenericDemo.java:22)
    at com.example.GenericDemo.main(GenericDemo.java:15)

异常信息明确告诉我们:试图将Integer转换为String,类型不兼容。

排查方法

代码修正与预防

修正方案一:使用泛型

// 错误代码
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0);

// 修正代码:使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 无需强制转换

修正方案二:使用instanceof检查

Object obj = getSomeObject();
if (obj instanceof String) {
    String str = (String) obj; // 安全转换
    // 处理字符串
} else if (obj instanceof Integer) {
    Integer num = (Integer) obj; // 安全转换
    // 处理整数
}

修正方案三:Java 16+的Pattern Matching for instanceof

Object obj = getSomeObject();
if (obj instanceof String str) {
    // 这里可以直接使用str变量
    System.out.println(str.length());
} else if (obj instanceof Integer num) {
    System.out.println(num + 10);
} else {
    // 处理其他情况
}

预防措施

2.4 ArithmeticException(算术异常)

现象描述

当发生异常的算术条件时抛出,最常见的是整数除零。

出现场景

场景一:整数除零

int result = 10 / 0; // 抛出ArithmeticException

场景二:取模运算除零

int remainder = 10 % 0; // 抛出ArithmeticException

注意:浮点数除零不会抛出异常,会返回Infinity或NaN

double result = 10.0 / 0.0; // 返回 Infinity,不会抛出异常

堆栈分析示例

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.example.Calculator.divide(Calculator.java:10)
    at com.example.Calculator.main(Calculator.java:5)

异常信息直接告诉我们问题:除零。

排查方法

代码修正与预防

修正方案一:检查除数

public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("除数不能为0");
    }
    return a / b;
}

修正方案二:使用Optional处理可能为0的情况

public Optional<Integer> safeDivide(int a, int b) {
    if (b == 0) {
        return Optional.empty();
    }
    return Optional.of(a / b);
}

修正方案三:使用浮点数运算(如果业务允许)

double result = 10.0 / 0.0; // 返回 Infinity,不会抛出异常
if (Double.isInfinite(result)) {
    // 处理无穷大情况
}

预防措施

第二课时(中):运行时异常(续)与常见受检异常(约20分钟)

2.5 NumberFormatException(数字格式异常)

现象描述

当尝试将字符串转换为数字类型,但字符串格式不合法时抛出。

出现场景

场景一:字符串包含非数字字符

int num = Integer.parseInt("123abc"); // 抛出NumberFormatException

场景二:字符串包含空格或特殊符号

int num = Integer.parseInt(" 123 "); // 抛出NumberFormatException,空格未处理

场景三:数字超出类型范围

int num = Integer.parseInt("2147483648"); // 超出int最大值,抛出异常

场景四:空字符串或null

int num = Integer.parseInt(""); // 抛出NumberFormatException
Integer.parseInt(null); // 抛出NullPointerException,注意这里是NPE

代码修正与预防

修正方案一:数据清洗

// 错误代码
String input = " 123 ";
int value = Integer.parseInt(input); // 抛出异常

// 修正代码
String input = " 123 ";
input = input.trim(); // 去除前后空格
if (!input.isEmpty()) {
    try {
        int value = Integer.parseInt(input);
    } catch (NumberFormatException e) {
        // 处理异常
    }
}

修正方案二:使用正则表达式预验证

public int parsePostalCode(String input) {
    // 预验证:必须是5位数字
    if (input == null || !input.matches("\\d{5}")) {
        throw new IllegalArgumentException("邮政编码必须是5位数字");
    }
    return Integer.parseInt(input); // 此时已保证安全
}

修正方案三:使用Apache Commons Lang的NumberUtils

import org.apache.commons.lang3.math.NumberUtils;

String input = "123";
int value = NumberUtils.toInt(input, 0); // 失败时返回默认值0,不抛出异常

修正方案四:Java 8+的Optional + 异常处理

public Optional<Integer> tryParseInt(String input) {
    try {
        return Optional.of(Integer.parseInt(input.trim()));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

预防措施

2.6 IllegalArgumentException(非法参数异常)

现象描述

当向方法传递了不合法或不适当的参数时抛出。这通常表示调用者的责任。

出现场景

场景一:参数值超出允许范围

public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄必须在0-150之间");
    }
    this.age = age;
}

场景二:参数格式错误

public void setEmail(String email) {
    if (email == null || !email.contains("@")) {
        throw new IllegalArgumentException("邮箱格式不正确");
    }
    this.email = email;
}

场景三:参数为null但方法不允许

public void processData(@NonNull Data data) {
    if (data == null) {
        throw new IllegalArgumentException("data cannot be null");
    }
    // 处理数据
}

排查方法

代码修正

// 在方法开头进行参数校验
public void registerUser(String username, String email, int age) {
    // 参数校验集中处理
    if (username == null || username.trim().isEmpty()) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
        throw new IllegalArgumentException("邮箱格式不正确");
    }
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄无效");
    }
    
    // 业务逻辑
}

2.7 IllegalStateException(非法状态异常)

现象描述

当方法在非法或不适当的时间被调用时抛出。通常表示被调用者的状态不适合执行请求的操作。

出现场景

场景一:对象未正确初始化

public class ConnectionPool {
    private boolean initialized = false;
    
    public void connect() {
        if (!initialized) {
            throw new IllegalStateException("连接池未初始化");
        }
        // 建立连接
    }
}

场景二:迭代器越界

List<String> list = Arrays.asList("A", "B");
Iterator<String> it = list.iterator();
it.next(); // A
it.next(); // B
it.next(); // 抛出NoSuchElementException,但有时会被包装为IllegalStateException

排查方法

代码修正

public class FileProcessor {
    private boolean opened = false;
    
    public void open() {
        // 打开文件
        opened = true;
    }
    
    public void readData() {
        if (!opened) {
            throw new IllegalStateException("必须先调用open()方法打开文件");
        }
        // 读取数据
    }
}

2.8 IOException(输入输出异常)

现象描述

当输入输出操作失败或中断时抛出。这是最典型的受检异常,处理文件、网络、流操作时经常遇到。

出现场景

场景一:文件不存在(FileNotFoundException)

FileReader fr = new FileReader("nonexistent.txt"); // 抛出FileNotFoundException

场景二:读取流时连接断开

InputStream in = socket.getInputStream();
int data = in.read(); // 如果连接已关闭,可能抛出IOException

场景三:写入磁盘空间不足

FileOutputStream fos = new FileOutputStream("largefile.bin");
byte[] data = new byte[1024];
fos.write(data); // 如果磁盘空间不足,抛出IOException

堆栈分析示例

java.io.FileNotFoundException: nonexistent.txt (系统找不到指定的文件)
    at java.base/java.io.FileInputStream.open0(Native Method)
    at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
    at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
    at com.example.FileReaderDemo.main(FileReaderDemo.java:8)

排查方法

代码修正与预防

修正方案一:使用try-with-resources确保资源关闭

// 错误代码:可能忘记关闭资源
public String readFile(String path) throws IOException {
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    return br.readLine();
    // 没有关闭资源,可能造成内存泄漏
}

// 修正代码:使用try-with-resources
public String readFile(String path) throws IOException {
    try (FileReader fr = new FileReader(path);
         BufferedReader br = new BufferedReader(fr)) {
        return br.readLine();
    } // 自动关闭
}

修正方案二:检查文件存在性

public void processFile(String path) {
    File file = new File(path);
    if (!file.exists()) {
        System.err.println("文件不存在: " + path);
        return; // 或者抛出更友好的异常
    }
    
    try (BufferedReader br = new BufferedReader(new FileReader(file))) {
        // 处理文件
    } catch (IOException e) {
        System.err.println("读取文件时发生错误: " + e.getMessage());
        e.printStackTrace();
    }
}

修正方案三:多层异常处理

public void copyFile(String src, String dest) {
    try (FileInputStream in = new FileInputStream(src);
         FileOutputStream out = new FileOutputStream(dest)) {
        byte[] buffer = new byte[1024];
        int length;
        while ((length = in.read(buffer)) > 0) {
            out.write(buffer, 0, length);
        }
    } catch (FileNotFoundException e) {
        System.err.println("源文件不存在或目标目录无法写入: " + e.getMessage());
    } catch (IOException e) {
        System.err.println("复制过程中发生IO错误: " + e.getMessage());
    }
}

预防措施

2.9 ClassNotFoundException(类未找到异常)

现象描述

当应用程序试图通过字符串名加载类,但在类路径中找不到该类的定义时抛出。

出现场景

场景一:Class.forName()加载类

Class.forName("com.mysql.jdbc.Driver"); // 如果驱动jar不在类路径中,抛出异常

场景二:类加载器加载类

ClassLoader.getSystemClassLoader().loadClass("com.example.MissingClass");

场景三:使用反射创建实例

Object obj = Class.forName("com.example.DynamicClass").newInstance();

堆栈分析示例

java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
    at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:476)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:398)

排查方法

代码修正与预防

修正方案一:确保JAR包在类路径中

修正方案二:捕获并处理异常

try {
    Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
    // 提供友好的错误信息
    throw new RuntimeException("MySQL驱动未找到,请检查是否添加了mysql-connector-java依赖", e);
}

修正方案三:使用ServiceLoader模式(Java 6+)

ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver driver : drivers) {
    // 自动发现所有驱动实现
}

第二课时(下):综合实战与最佳实践(约10分钟)

3.1 复杂异常排查案例

案例:银行转账系统中的异常链

public class BankingService {
    public void transfer(String fromAccount, String toAccount, double amount) 
            throws BusinessException {
        try {
            Account from = accountRepository.findByNumber(fromAccount);
            Account to = accountRepository.findByNumber(toAccount);
            
            if (from == null || to == null) {
                throw new IllegalArgumentException("账户不存在");
            }
            
            from.withdraw(amount);
            to.deposit(amount);
            
            transactionLog.log(fromAccount, toAccount, amount);
            
        } catch (IllegalArgumentException e) {
            throw new BusinessException("转账参数错误", e);
        } catch (InsufficientBalanceException e) {
            throw new BusinessException("余额不足", e);
        } catch (Exception e) {
            throw new BusinessException("转账失败,请稍后重试", e);
        }
    }
}

异常排查思路

当看到类似下面的异常堆栈时:

com.example.BusinessException: 转账失败,请稍后重试
    at com.example.BankingService.transfer(BankingService.java:45)
    at com.example.BankingController.main(BankingController.java:18)
Caused by: java.sql.SQLException: Connection timed out
    at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2189)
    at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:795)
    at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:329)
    at java.sql.DriverManager.getConnection(DriverManager.java:664)
    at com.example.AccountRepository.findByNumber(AccountRepository.java:22)
    ... 5 more

排查步骤

解决方案

教训:包装异常时不要丢失原始信息,提供具体的错误消息有助于排查。

3.2 异常处理最佳实践总结

捕获特定异常,而不是通用异常

// 不好的做法
try {
    // 业务代码
} catch (Exception e) {
    // 捕获所有异常,掩盖了真正的问题
}

// 好的做法
try {
    // 业务代码
} catch (FileNotFoundException e) {
    // 处理文件不存在
} catch (IOException e) {
    // 处理其他IO错误
}

避免空的catch块

// 绝对不要这样做
try {
    riskyOperation();
} catch (Exception e) {
    // 空的catch块,异常被吞噬
}

// 至少记录异常
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("操作失败", e); // 记录日志
    throw e; // 或者重新抛出
}

使用try-with-resources自动关闭资源

// Java 7之前的方式
FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt");
    // 处理文件
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// Java 7+ 推荐的方式
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 处理文件
} // 自动关闭

使用自定义异常增强业务语义

// 自定义业务异常
public class InsufficientBalanceException extends Exception {
    private double currentBalance;
    private double requiredAmount;
    
    public InsufficientBalanceException(double current, double required) {
        super(String.format("余额不足:当前余额%.2f,需要%.2f", current, required));
        this.currentBalance = current;
        this.requiredAmount = required;
    }
    
    // getters...
}

// 使用
public void withdraw(double amount) throws InsufficientBalanceException {
    if (balance < amount) {
        throw new InsufficientBalanceException(balance, amount);
    }
    balance -= amount;
}

方法重写时遵守异常声明规则

class Parent {
    public void process() throws IOException { }
}

class Child extends Parent {
    @Override
    public void process() throws FileNotFoundException { } // 允许,FileNotFoundException是IOException的子类
    
    // @Override
    // public void process() throws Exception { } // 不允许,Exception比IOException更宽泛
}

记录异常时包含上下文信息

try {
    processOrder(orderId, userId);
} catch (OrderException e) {
    // 记录有用的上下文信息
    logger.error("处理订单失败: orderId={}, userId={}", orderId, userId, e);
    throw e;
}

不要用异常控制正常的程序流程

// 不好的做法:用异常控制流程
try {
    Integer.parseInt(userInput);
    // 是数字,继续处理
} catch (NumberFormatException e) {
    // 不是数字,执行其他逻辑
}

// 好的做法:使用条件判断
if (userInput.matches("\\d+")) {
    int value = Integer.parseInt(userInput);
    // 是数字,继续处理
} else {
    // 不是数字,执行其他逻辑
}

异常处理的黄金法则总结

原则说明
精准捕获捕获具体的异常类型,而不是笼统的Exception
绝不吞噬空的catch块是万恶之源,至少要记录日志
及时释放使用try-with-resources或finally确保资源释放
保留原始异常包装异常时要把原异常作为cause传入
提供上下文异常消息要包含有助于排查的信息
区分异常类型可恢复用受检异常,程序错误用运行时异常
文档化用javadoc的@throws说明方法可能抛出的异常

3.3 Java 7+ 多异常捕获

从Java 7开始,可以使用|在一个catch块中捕获多个异常类型,减少代码重复:

try {
    // 可能抛出多种异常的代码
} catch (IOException | SQLException e) {
    // 统一处理IO和SQL异常
    logger.error("数据访问错误", e);
    throw e; // Java 7+ 支持更精确的重抛类型检查
}

注意:多异常捕获时,catch参数隐式为final,不能修改。

3.4 异常处理与事务管理

在企业级应用中,异常处理与事务管理密切相关。通常:

@Service
public class AccountService {
    @Transactional(rollbackFor = {BusinessException.class, RuntimeException.class})
    public void transferMoney(String from, String to, double amount) 
            throws BusinessException {
        try {
            // 转账逻辑
        } catch (InsufficientBalanceException e) {
            // 业务异常,触发事务回滚
            throw new BusinessException("转账失败", e);
        }
    }
}

课程总结(约5分钟)

知识体系回顾

通过两个课时的学习,我们全面覆盖了:

异常排查思维导图

遇到异常时,按以下顺序思考:
┌─────────────────────────────────────┐
│ 1. 看类型:是什么异常?属于哪一类?    │
├─────────────────────────────────────┤
│ 2. 看消息:异常说了什么?有什么线索?  │
├─────────────────────────────────────┤
│ 3. 看堆栈:第一行自己的代码在哪?      │
├─────────────────────────────────────┤
│ 4. 看原因:有Caused by吗?底层是什么? │
├─────────────────────────────────────┤
│ 5. 想来源:这个值从哪来的?谁传的?    │
├─────────────────────────────────────┤
│ 6. 想方案:怎么修复?如何预防?       │
└─────────────────────────────────────┘

以上就是全面解析Java中常见Exception异常的错误排查与代码修正的详细内容,更多关于Java常见Exception异常的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文