15.Java异常处理:从束手无策到从容应对

目录

写在前面

一、什么是异常?

[1.1 异常的概念](#1.1 异常的概念)

[1.2 异常的体系结构](#1.2 异常的体系结构)

[1.3 异常的分类](#1.3 异常的分类)

二、异常的处理方式

[2.1 防御式编程的两种思路](#2.1 防御式编程的两种思路)

[2.2 抛出异常:throw](#2.2 抛出异常:throw)

[2.3 声明异常:throws](#2.3 声明异常:throws)

[2.4 捕获异常:try-catch](#2.4 捕获异常:try-catch)

[2.5 finally:无论是否异常都要执行](#2.5 finally:无论是否异常都要执行)

[2.6 异常的处理流程](#2.6 异常的处理流程)

三、自定义异常类

[3.1 实现自定义异常](#3.1 实现自定义异常)

[3.2 注意事项](#3.2 注意事项)

四、课后练习

总结


写在前面

相信每一位Java初学者都曾经历过这样的时刻:满怀信心地写完代码,点击运行,结果控制台突然冒出一片刺眼的红色错误信息。程序崩溃了,而你一脸茫然地看着那个陌生的异常类名,不知道发生了什么,更不知道该如何解决。

异常,是编程路上绕不开的一道坎。但换个角度看,异常其实是程序在"说话"------它在告诉你:"我这里出问题了,快来帮帮我。"

今天这篇文章,我们就来系统地学习Java的异常处理机制。掌握了它,你就能听懂程序在说什么,并且知道该怎么回应。


一、什么是异常?

1.1 异常的概念

异常是指在程序执行过程中发生的一种特殊情况或错误状态。它打断了程序的正常执行流程,需要特别处理。

在日常开发中,即使程序员绞尽脑汁想把代码写得尽善尽美,程序运行过程中还是难免会出现各种问题。有些问题通过代码很难完全控制,比如数据格式不对、网络不通畅、内存不足等。

在Java中,程序执行过程中发生的不正常行为被称为异常。不同类型的异常,都有对应的类来描述。

比如下面这些你一定见过:

复制代码
// 算术异常
System.out.println(10 / 0);
// Exception in thread "main" java.lang.ArithmeticException: / by zero

// 数组越界异常
int[] array = {1, 2, 3};
System.out.println(array[100]);
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException

// 空指针异常
String str = null;
System.out.println(str.length());
// Exception in thread "main" java.lang.NullPointerException

1.2 异常的体系结构

Java内部维护了一套完善的异常体系结构,对各种异常和错误进行分类管理:

复制代码
Throwable(顶层父类)
├── Error(严重问题,程序无法处理)
│   ├── StackOverflowError
│   └── OutOfMemoryError
└── Exception(可处理的异常)
    ├── RuntimeException(运行时异常/非受查异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   └── ArithmeticException
    └── 其他受查异常
        ├── IOException
        ├── SQLException
        └── FileNotFoundException

关键理解:

  • Throwable:异常体系的顶层类,派生出Error和Exception两个子类

  • Error :Java虚拟机无法解决的严重问题,比如JVM内部错误、资源耗尽等。一旦发生,程序无力回天。典型代表:StackOverflowError(栈溢出)和OutOfMemoryError(内存溢出)

  • Exception:程序员可以通过代码进行处理,使程序继续执行。我们平时所说的"异常"主要指的就是Exception

1.3 异常的分类

根据发生时机不同,异常分为两类:

编译时异常(受查异常)

在程序编译期间发生的异常,编译器会强制要求处理。比如:

复制代码
public class Person implements Cloneable {
    private String name;
    
    @Override
    public Person clone() {
        return (Person) super.clone();  // 编译报错!未处理CloneNotSupportedException
    }
}

运行时异常(非受查异常)

在程序执行期间发生的异常,编译器不强制处理。比如NullPointerExceptionArrayIndexOutOfBoundsExceptionArithmeticException等。

注意 :编译时出现的语法性错误不能称之为异常。比如把System.out.println写成了system.out.println,这是"编译错误",而不是异常。


二、异常的处理方式

2.1 防御式编程的两种思路

面对程序中不可避免的错误,主要有两种处理思路:

LBYL(事前防御型)

在操作之前就做充分的检查:

复制代码
public static int getElement(int[] array, int index) {
    if (array == null) {
        System.out.println("数组为null");
        return -1;
    }
    if (index < 0 || index >= array.length) {
        System.out.println("下标越界");
        return -1;
    }
    return array[index];
}

这种方式的缺陷是:正常流程和错误处理流程的代码混在一起,整体显得比较混乱。

EAFP(事后认错型)

先操作,遇到问题再处理:

复制代码
public static int getElement(int[] array, int index) {
    try {
        return array[index];
    } catch (NullPointerException e) {
        System.out.println("数组为null");
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("下标越界");
    }
    return -1;
}

这种方式将正常流程和错误流程分离开来,代码更清晰。Java异常处理的核心思想就是EAFP。

2.2 抛出异常:throw

当程序中出现错误时,可以使用throw关键字抛出一个异常对象,将错误信息告知调用者:

复制代码
public static int getElement(int[] array, int index) {
    if (null == array) {
        throw new NullPointerException("传递的数组为null");
    }
    if (index < 0 || index >= array.length) {
        throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
    }
    return array[index];
}

使用throw的注意事项:

  1. throw必须写在方法体内部

  2. 抛出的对象必须是Exception或其子类对象

  3. 如果抛出的是RuntimeException或其子类,可以不处理,直接交给JVM

  4. 如果抛出的是编译时异常,必须处理,否则无法通过编译

  5. 异常一旦抛出,其后的代码就不会执行

2.3 声明异常:throws

throws关键字用于在方法声明中列出该方法可能抛出的异常,将异常的处理责任转移给调用者:

复制代码
public void readConfig(String fileName) throws FileNotFoundException {
    if (!fileName.equals("config.ini")) {
        throw new FileNotFoundException("配置文件不是config.ini");
    }
    // 读取文件的逻辑...
}

使用throws的注意事项:

  1. throws必须跟在方法的参数列表之后

  2. 声明的异常必须是Exception或其子类

  3. 如果方法内部可能抛出多个异常,throws后跟多个异常类型,用逗号隔开

  4. 如果多个异常类型有父子关系,直接声明父类即可

  5. 调用声明抛出异常的方法时,如果该异常是编译时异常,调用者必须处理或继续抛出

2.4 捕获异常:try-catch

throws并没有真正处理异常,只是将异常上报给调用者。如果要真正处理异常,需要使用try-catch

复制代码
public static void main(String[] args) {
    try {
        readConfig("other.ini");
    } catch (FileNotFoundException e) {
        // 异常的处理方式
        System.out.println(e.getMessage());     // 只打印异常信息
        System.out.println(e);                   // 打印异常类型:异常信息
        e.printStackTrace();                     // 打印信息最全面(推荐)
    }
    
    // 异常被处理后,这里的代码会继续执行
    System.out.println("异常如果被处理了,这里的代码也可以执行");
}

关于异常的处理方式:

异常种类繁多,要根据不同的业务场景来决定:

  • 对于比较严重的问题(比如和金钱相关的场景),应该让程序直接崩溃,防止造成更严重后果

  • 对于不太严重的问题(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序员

  • 对于可能会恢复的问题(比如网络相关的场景),可以尝试重试

使用try-catch的注意事项:

  1. try块内抛出异常位置之后的代码不会被执行

  2. 如果抛出异常类型与catch时异常类型不匹配,异常不会被成功捕获,继续往外抛

  3. try中可能抛出多个不同的异常,需要用多个catch来捕获

  4. 如果多个异常的处理方式完全相同,可以写成:catch(异常类型1 | 异常类型2 e)

  5. 如果异常之间具有父子关系,子类异常必须在父类异常之前捕获

    try {
    System.out.println("before");
    System.out.println(array[100]);
    System.out.println("after");
    } catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("数组下标越界异常");
    e.printStackTrace();
    } catch (NullPointerException e) {
    System.out.println("空指针异常");
    e.printStackTrace();
    }

2.5 finally:无论是否异常都要执行

有些代码,不论程序是否发生异常,都需要执行。比如程序中打开的资源------网络连接、数据库连接、IO流等,在程序正常或异常退出时,必须进行回收。

finally就是用来解决这个问题的:

复制代码
try {
    // 可能会发生异常的代码
} catch (异常类型 e) {
    // 对捕获到的异常进行处理
} finally {
    // 此处的语句无论是否发生异常,都会被执行到
}

为什么需要finally?

考虑这样一个场景:方法中打开了输入流,在方法返回之前必须关闭它。如果不用finally,可能会出现资源泄漏:

复制代码
public int getData() {
    Scanner scanner = new Scanner(System.in);
    try {
        int num = scanner.nextInt();
        return num;
    } catch (InputMismatchException e) {
        e.printStackTrace();
    } finally {
        scanner.close();  // 无论是否发生异常,都会执行
        System.out.println("finally中代码");
    }
    return -1;
}

重要特性finally中的代码一定会在方法返回之前执行。但如果finally中也有return语句,它会覆盖trycatch中的return。因此,不建议在finally中写return语句

2.6 异常的处理流程

当代码中出现异常时,会沿着"调用栈"向上传递:

  1. 程序先执行try中的代码

  2. 如果try中的代码出现异常,结束try中的代码,查看与catch中的异常类型是否匹配

  3. 如果找到匹配的异常类型,执行catch中的代码

  4. 如果没有找到匹配的异常类型,将异常向上传递到上层调用者

  5. 无论是否找到匹配的异常类型,finally中的代码都会被执行

  6. 如果上层调用者也没有处理异常,继续向上传递

  7. 一直到main方法也没有处理,交给JVM处理,程序异常终止


三、自定义异常类

Java内置的异常类虽然丰富,但不能完全覆盖实际开发中的所有场景。比如实现一个用户登录功能,可能需要区分"用户名错误"和"密码错误"两种异常。

3.1 实现自定义异常

自定义异常通常继承自ExceptionRuntimeException

复制代码
// 自定义异常:用户名错误
class UserNameException extends RuntimeException {
    public UserNameException(String message) {
        super(message);
    }
}

// 自定义异常:密码错误
class PasswordException extends RuntimeException {
    public PasswordException(String message) {
        super(message);
    }
}

然后在业务代码中使用:

复制代码
public class LoginService {
    private static final String USER_NAME = "admin";
    private static final String PASSWORD = "123456";
    
    public static void login(String userName, String password) {
        if (!USER_NAME.equals(userName)) {
            throw new UserNameException("用户名错误!");
        }
        if (!PASSWORD.equals(password)) {
            throw new PasswordException("密码错误!");
        }
        System.out.println("登录成功!");
    }
    
    public static void main(String[] args) {
        try {
            login("admin", "654321");
        } catch (UserNameException e) {
            e.printStackTrace();
        } catch (PasswordException e) {
            e.printStackTrace();
        }
    }
}

3.2 注意事项

  • 继承自Exception的异常默认是受查异常(编译时异常),必须处理

  • 继承自RuntimeException的异常默认是非受查异常(运行时异常),可以不处理


四、课后练习

**题目1:**​ 以下哪个不是Java中的受检异常(checked exception)?

A. IOException

B. SQLException

C. NullPointerException

D. ClassNotFoundException

**答案:**​ C

**题目2:**​ 关于try-catch-finally语句,以下哪个说法是正确的?

A. finally块总是会执行,即使try块中有return语句

B. 如果catch块中抛出异常,finally块就不会执行

C. 一个try块后面必须跟着至少一个catch块

D. finally块中的return语句会覆盖try或catch块中的return语句

**答案:**​ A、D

**题目3:**​ 关于异常处理,以下哪个说法是错误的?

A. 子类方法抛出的异常范围不能大于父类方法声明的异常范围

B. 可以在catch块中捕获多个异常类型

C. RuntimeException及其子类都是非受查异常

D. 使用throw关键字可以手动抛出异常,但不能抛出Error

**答案:**​ D


总结

今天我们系统地学习了Java的异常处理机制:

  1. 异常的概念:程序执行过程中的不正常行为

  2. 异常体系:Throwable → Error(不可处理)和 Exception(可处理)

  3. 异常分类:编译时异常(受查异常)和运行时异常(非受查异常)

  4. 处理方式:throw(抛出)、throws(声明)、try-catch(捕获)、finally(收尾)

  5. 处理流程:沿调用栈向上传递,直到被处理或交给JVM

  6. 自定义异常:继承Exception或RuntimeException

异常处理不是可有可无的装饰,而是高质量软件的必备能力。一个好的程序不是不出错,而是在出错时能够优雅地处理,给用户友好的反馈,给开发者足够的信息来定位问题。

掌握异常处理,你的代码将更加健壮、更加专业。

相关推荐
geovindu8 小时前
go: Recursion Algorithm
开发语言·后端·算法·golang·递归算法
Cry丶8 小时前
一次 FTP 上传引发 504 Gateway Timeout 的排查复盘
java·nginx·ftp·openresty·被动模式·504 gateway·生产问题排查
geovindu9 小时前
CSharp: Composite Pattern
开发语言·后端·c#·组合模式·结构型模式
码农进化录9 小时前
Java 程序员的 AI 进化论 | AI 辅助 CI/CD,让 AI 帮你做代码审查和部署决策
java·spring boot·openai
zyplayer-doc9 小时前
企业文档的进化:网盘和在线文档的下一个升级方向—知识库系统
java·大数据·数据库·人工智能·ocr
消失的旧时光-19439 小时前
KMP Web 开发实战(二):js、wasmJs、browser、executable 到底是什么意思?
开发语言·前端·javascript·跨平台·kmp
吃饱了得干活9 小时前
从零设计一个高并发点赞系统:缓存+MQ异步持久化实战
java·架构
奇牙coding9 小时前
gpt-5.6-luna 调用报 400 bad_request 怎么办?同样的 messages 在 gpt-5.5 没问题——两处校验差异和修复写法
java·windows·gpt·ai
sycmancia9 小时前
Qt——线程的生命期问题
开发语言·qt
影寂ldy9 小时前
C# 多线程进阶知识点(线程优先级、多委托传参、线程锁、死锁)
开发语言·数据库·c#