09 Java 异常处理

Java 异常处理入门:看懂这篇,程序再也不 "崩溃" 了

刚学 Java 的你,是不是也遇到过这种情况:写了一个看似完美的程序,输入 10 和 2 能正常出结果,可一旦输入 10 和 0,程序就突然报错终止,屏幕上一堆看不懂的英文?其实这就是 Java 里的 "异常" 在搞鬼。今天就用通俗的比喻 + 实战代码,带你彻底搞懂异常处理,让你的程序从此更稳健~

一、先搞懂:异常和错误不是一回事

我们可以把程序运行比作一次探险旅程,清晰区分两种 "意外状况":

1. 异常(Exception):能解决的小麻烦

旅途中难免遇到小意外,但只要提前准备,就能顺利化解,旅程还能继续:

  • 想过河发现桥断了(对应IOException,比如读取文件失败)

  • 打开地图发现是空白(对应NullPointerException,空指针异常)

  • 计算路程时不小心除以零(对应ArithmeticException,算术异常)

    这些情况都是可以预见和处理的,就像绕路、重新找地图一样,处理后程序能正常运行。

2. 错误(Error):挡不住的大灾难

如果遇到毁灭性事件,再怎么准备也没用,只能终止旅程:

  • 突然地震,地面裂开鸿沟(对应OutOfMemoryError,内存溢出)

  • 探险队全员病倒(对应NoClassDefFoundError,类定义未找到)

    这类问题是 JVM 或系统层面的严重故障,程序无法捕获和处理,只能提前预防。

一张表分清异常和错误

特性 异常 (Exception) 错误 (Error)
本质 程序运行中的意外情况 JVM / 系统级严重故障
处理方式 可以用代码捕获处理 无法处理,只能预防
影响范围 不处理会导致当前线程终止 通常导致整个程序崩溃
常见例子 空指针、除以零、文件找不到等 内存溢出、栈溢出等

二、异常与错误的继承关系(类层次结构)

理解它们的继承关系是掌握 Java 异常处理机制的关键。

php 复制代码
java.lang.Throwable
├─ java.lang.Error(系统错误,不可处理)
│  ├─ OutOfMemoryError
│  ├─ StackOverflowError
│  ├─ NoClassDefFoundError
│  └─ ...(其他系统级错误)
└─ java.lang.Exception(程序异常,可处理)
   ├─ 检查型异常(必须处理)
   │  ├─ IOException
   │  │  ├─ FileNotFoundException
   │  │  └─ ...
   │  ├─ SQLException
   │  ├─ ClassNotFoundException
   │  └─ ...
   └─ 非检查型异常(无需强制处理,继承自 RuntimeException)
      ├─ RuntimeException
      │  ├─ NullPointerException
      │  ├─ IndexOutOfBoundsException
      │  │  ├─ ArrayIndexOutOfBoundsException
      │  │  └─ StringIndexOutOfBoundsException
      │  ├─ IllegalArgumentException
      │  ├─ ClassCastException
      │  └─ ...
      └─ ...(其他非检查型异常,如 `UnsupportedOperationException`)
  • java.lang.Object:Java 中所有类的根父类。

  • java.lang.Throwable :这是所有错误和异常的顶层父类 。只有 Throwable 类型的对象才能被 throw 语句抛出,并且被 catch 语句捕获。它有两个重要的子类:

    • java.lang.Error:用于表示严重的、无法处理的错误。

    • java.lang.Exception:用于表示可以被处理的异常。

      • java.lang.RuntimeExceptionException 的一个重要子类,所有非受检异常都继承自它。

三、异常的两大分类:受检和非受检

Java 里所有异常都继承自java.lang.Exception类,根据处理要求不同,分为两类,重点记清楚区别:

1. 受检异常(Checked Exception)

  • 特点:编译时就会 "提醒" 你必须处理,不处理编译器不让通过
  • 原因:大多是外部环境导致的,比如文件不存在、网络断开
  • 常见例子:IOException(文件读写异常)、SQLException(数据库操作异常)
  • 处理要求:要么用try-catch捕获,要么用throws声明交给调用者处理

2. 非受检异常(Unchecked Exception)

  • 特点:编译时不报错,运行时才可能出现
  • 原因:大多是程序员的逻辑错误导致的,比如数组越界、用了空对象
  • 常见例子:NullPointerException(空指针)、ArrayIndexOutOfBoundsException(数组越界)
  • 处理要求:不用强制处理,但要在编码时避免(比如判断数组索引是否合法)

提示:所有非受检异常都继承自RuntimeException类,记住这个核心类就好~

四、异常处理核心:try-catch-finally 三板斧

这是 Java 处理异常的核心机制,就像给风险代码装了 "安全网",我们用计算器案例一步步拆解:

1. 原始问题代码(会崩溃)

ini 复制代码
import java.util.Scanner;

public class SimpleCalculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入第一个数字: ");
        int num1 = scanner.nextInt();
        System.out.print("请输入第二个数字: ");
        int num2 = scanner.nextInt();

        int result = num1 / num2; // 输入0就会崩溃
        System.out.println("结果是: " + result);
        scanner.close();
    }
}

2. try 块:包裹风险代码

把可能出错的代码放进try里,让 Java "尝试" 执行:

csharp 复制代码
try {
    int result = num1 / num2; // 可能抛出异常的代码
    System.out.println("结果是: " + result);
}

3. catch 块:捕获并处理异常

如果try里的代码真的出错,catch就会像安全网一样接住异常,执行备用逻辑:

csharp 复制代码
catch (ArithmeticException e) {
    // 只捕获"算术异常"(比如除以零)
    System.out.println("【错误提示】");
    System.out.println("出错原因:" + e.getMessage()); // 获取具体错误信息
    System.out.println("除数不能为零,请重新输入!");
}

4. finally 块:必执行的收尾工作

不管有没有异常,finally里的代码都会执行,适合做资源清理(比如关文件、关连接):

csharp 复制代码
finally {
    System.out.println("\n--- 计算结束,资源已释放 ---");
    scanner.close(); // 确保关闭输入流
}

完整健壮版代码(不崩溃)

csharp 复制代码
import java.util.Scanner;

public class RobustCalculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("请输入第一个数字: ");
        int num1 = scanner.nextInt();
        System.out.print("请输入第二个数字: ");
        int num2 = scanner.nextInt();

        try {
            int result = num1 / num2;
            System.out.println("结果是: " + result);
        } catch (ArithmeticException e) {
            System.out.println("\n【错误提示】");
            System.out.println("出错原因:" + e.getMessage());
            System.out.println("除数不能为零,请重新输入!");
        } finally {
            System.out.println("\n--- 计算结束,资源已释放 ---");
            scanner.close();
        }
    }
}

运行效果:输入 10 和 0 后,程序不会崩溃,而是友好提示错误,完美收尾~

五、进阶技巧:throw 和 throws 关键字

除了被动捕获异常,我们还能主动处理异常,这两个关键字要记牢:

1. throw:手动抛出异常

当检测到非法条件时,主动创建异常并抛出,比如判断除数为 0 时直接报错:

arduino 复制代码
if (num2 == 0) {
    // 手动抛出异常,自定义错误信息
    throw new ArithmeticException("除数不能为零");
}

2. throws:声明异常,转移责任

如果一个方法不想处理异常,可以用throws声明,告诉调用者 "这里可能出错,你要处理":

csharp 复制代码
// 声明该方法可能抛出算术异常
public static int divide(int num1, int num2) throws ArithmeticException {
    if (num2 == 0) {
        throw new ArithmeticException("除数不能为零");
    }
    return num1 / num2;
}

// 调用者必须处理异常
public static void main(String[] args) {
    try {
        int result = divide(10, 0);
        System.out.println("结果是: " + result);
    } catch (ArithmeticException e) {
        System.out.println("捕获到异常: " + e.getMessage());
    }
}

六、高阶玩法:多异常捕获 + 自定义异常

掌握基础后,解锁两个实用高阶技巧,让异常处理更灵活精准:

1. 多异常捕获:一次处理多种错误

实际开发中,try块可能抛出多种异常(比如同时有算术异常、空指针异常),不用写多个独立catch,有两种高效处理方式:

方式 1:多异常并列捕获(Java 7+ 支持)

多个异常用竖线|分隔,适用于异常处理逻辑相同的场景:

ini 复制代码
try {
    String str = null;
    int num1 = Integer.parseInt(str); // 可能抛NumberFormatException
    int num2 = 0;
    int result = num1 / num2; // 可能抛ArithmeticException
} catch (NumberFormatException | ArithmeticException e) {
    // 同一逻辑处理两种异常
    System.out.println("输入非法或计算错误:" + e.getMessage());
}
方式 2:分层捕获(父异常在后)

如果不同异常需要不同处理逻辑,按 "子类异常在前、父类异常在后" 的顺序捕获:

csharp 复制代码
try {
    String[] arr = {"10", "2", null};
    int num1 = Integer.parseInt(arr[0]);
    int num2 = Integer.parseInt(arr[2]); // 可能抛NullPointerException
    int result = num1 / num2; // 可能抛ArithmeticException
} catch (NullPointerException e) {
    // 专门处理空指针异常
    System.out.println("数组元素为空,请检查输入:" + e.getMessage());
} catch (ArithmeticException e) {
    // 专门处理算术异常
    System.out.println("计算错误,除数不能为零:" + e.getMessage());
} catch (Exception e) {
    // 父类异常兜底,捕获其他未预料的异常
    System.out.println("未知错误:" + e.getMessage());
}

⚠️ 注意:不能把父异常(如Exception)放在子类异常前面,否则子类异常的catch块会永远执行不到,编译器会报错。

2. 自定义异常:贴合业务场景的错误提示

Java 内置异常只能覆盖通用场景,实际开发中需要贴合业务的异常(比如 "用户年龄不合法""余额不足"),这时可以自定义异常类:

自定义异常步骤:
  1. 继承Exception(受检异常)或RuntimeException(非受检异常);
  2. 提供无参构造和带错误信息的构造方法(方便传递异常原因)。
实战:自定义 "年龄不合法异常"
scala 复制代码
// 自定义受检异常(继承Exception)
public class IllegalAgeException extends Exception {
    // 无参构造
    public IllegalAgeException() {}

    // 带错误信息的构造方法
    public IllegalAgeException(String message) {
        super(message); // 调用父类构造,传递错误信息
    }
}

// 自定义非受检异常(继承RuntimeException)
public class InsufficientBalanceException extends RuntimeException {
    public InsufficientBalanceException(String message) {
        super(message);
    }
}
使用自定义异常:
csharp 复制代码
public class UserService {
    // 注册用户:年龄必须在18-60岁之间(受检异常,需throws声明)
    public void register(String name, int age) throws IllegalAgeException {
        if (age < 18 || age > 60) {
            // 手动抛出自定义异常
            throw new IllegalAgeException("用户" + name + "年龄不合法:" + age + "岁,需在18-60岁之间");
        }
        System.out.println("用户" + name + "注册成功!");
    }

    // 转账:余额不足时抛出非受检异常
    public void transfer(String userName, double balance, double amount) {
        if (amount > balance) {
            throw new InsufficientBalanceException("用户" + userName + "余额不足:当前余额" + balance + ",转账金额" + amount);
        }
        System.out.println("转账成功!剩余余额:" + (balance - amount));
    }

    // 测试自定义异常
    public static void main(String[] args) {
        UserService service = new UserService();

        // 处理受检异常(必须try-catch或throws)
        try {
            service.register("张三", 17);
        } catch (IllegalAgeException e) {
            System.out.println("注册失败:" + e.getMessage());
        }

        // 处理非受检异常(可选try-catch)
        try {
            service.transfer("李四", 100, 200);
        } catch (InsufficientBalanceException e) {
            System.out.println("转账失败:" + e.getMessage());
        }
    }
}

运行结果:

复制代码
注册失败:用户张三年龄不合法:17岁,需在18-60岁之间
转账失败:用户李四余额不足:当前余额100.0,转账金额200.0

六、核心总结:异常处理的 3 个原则

  1. 能处理的异常一定要处理,别让程序粗暴崩溃;
  2. try包裹风险代码,catch精准捕获(多异常按 "子类在前、父类在后" 顺序),finally清理资源;
  3. 受检异常必须处理(try-catchthrows),非受检异常尽量避免(靠逻辑判断),业务异常用自定义异常优化提示。

异常处理是 Java 程序健壮性的基石,刚开始不用追求复杂,先把try-catch-finally用熟,能解决除以零、空指针这些常见问题就够了。随着编程经验增加,再慢慢学习自定义异常、多异常捕获等进阶用法~

相关推荐
玖剹2 小时前
多线程编程:从日志到单例模式全解析
java·linux·c语言·c++·ubuntu·单例模式·策略模式
Penge6662 小时前
Go 通道引用与 close 操作
后端
一 乐2 小时前
社区养老保障|智慧养老|基于springboot+小程序社区养老保障系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·小程序
程序员爱钓鱼2 小时前
Python编程实战 - Python实用工具与库 - 操作Word:python-docx
后端·python
随机昵称_1234562 小时前
Linux如何从docker hub下载arm镜像
java·linux·arm开发·docker
程序员爱钓鱼2 小时前
Python编程实战 - Python实用工具与库 - 操作PDF:pdfplumber、PyPDF2
后端·python
Python私教2 小时前
什么是爬虫
后端
Python私教2 小时前
Python爬虫怎么学
后端
欧阳码农2 小时前
盘点这两年我接触过的副业赚钱赛道,对于你来说可能是信息差
前端·人工智能·后端