前言

这一篇我们来学习 Java 面试中非常重要的一个基础知识点:异常体系。

在平时写项目时,我们经常会遇到各种异常,比如:

  • 空指针异常
  • 数组越界异常
  • 类型转换异常
  • SQL 异常
  • 文件找不到异常
  • 参数校验异常
  • 业务异常

如果异常处理不好,接口可能直接报 500,前端看到一堆错误信息,用户体验很差。

在 Spring Boot 项目中,我们还经常会配合全局异常处理:

@RestControllerAdvice
public class GlobalExceptionHandler {
}

所以理解 Java 异常体系,不只是为了面试,也直接关系到后端项目开发。

一、Java 异常体系结构

Java 中异常的顶级父类是:

Throwable

Throwable 下面主要分成两大类:

Throwable
  ├── Error
  └── Exception

可以简单理解为:

类型 说明
Error 严重错误,程序一般无法处理
Exception 异常,程序可以捕获和处理

常见结构:

Throwable
  ├── Error
  │     ├── OutOfMemoryError
  │     └── StackOverflowError
  │
  └── Exception
        ├── RuntimeException
        │     ├── NullPointerException
        │     ├── IndexOutOfBoundsException
        │     └── ClassCastException
        │
        └── IOException

二、Error 是什么?

Error 表示严重错误,一般不是业务代码能处理的。

常见 Error 有:

OutOfMemoryError
StackOverflowError

1. OutOfMemoryError

内存溢出。

比如程序创建了太多对象,导致 JVM 内存不够:

List<byte[]> list = new ArrayList<>();

while (true) {
    list.add(new byte[1024 * 1024]);
}

这可能导致:

OutOfMemoryError

2. StackOverflowError

栈溢出。

常见原因是递归没有结束条件:

public class Test {
    public static void main(String[] args) {
        method();
    }

    public static void method() {
        method();
    }
}

不断调用自己,最终可能出现:

StackOverflowError

Error 一般不建议在业务代码中捕获处理,而是要从程序设计、JVM 参数、内存使用等角度解决。

三、Exception 是什么?

Exception 表示异常,是程序可以捕获和处理的问题。

比如:

try {
    int result = 10 / 0;
} catch (Exception e) {
    System.out.println("程序出现异常:" + e.getMessage());
}

这里的除零异常可以被捕获。

Exception 又可以分成两类:

运行时异常
编译时异常

更准确地说:

类型 说明
RuntimeException 及其子类 运行时异常,也叫非受检异常
除 RuntimeException 外的 Exception 编译时异常,也叫受检异常

四、运行时异常 RuntimeException

运行时异常指的是 RuntimeException 及其子类。

常见运行时异常包括:

异常 含义
NullPointerException 空指针异常
IndexOutOfBoundsException 下标越界异常
ClassCastException 类型转换异常
ArithmeticException 算术异常
IllegalArgumentException 参数非法异常
NumberFormatException 数字格式化异常

运行时异常的特点是:

编译器不会强制你处理。

比如:

public class Test {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length());
    }
}

这段代码可以编译通过,但运行时会报:

NullPointerException

五、常见运行时异常示例

1. NullPointerException

String str = null;

System.out.println(str.length());

原因是 strnull,却调用了它的方法。

避免方式:

if (str != null) {
    System.out.println(str.length());
}

或者在比较字符串时写成:

"success".equals(str)

这样可以减少空指针风险。

2. IndexOutOfBoundsException

List<String> list = new ArrayList<>();

list.add("Java");

System.out.println(list.get(1));

列表只有一个元素,下标只有 0。

访问下标 1 就会越界。

3. ClassCastException

Object obj = "hello";

Integer num = (Integer) obj;

obj 实际是字符串,却强转成 Integer,会出现类型转换异常。

4. ArithmeticException

int result = 10 / 0;

整数除以 0 会报算术异常。

5. NumberFormatException

String str = "abc";

Integer num = Integer.parseInt(str);

abc 不是合法数字,所以转换失败。

六、编译时异常是什么?

编译时异常也叫受检异常。

它的特点是:

编译器会强制你处理。

比如文件读取:

FileInputStream fis = new FileInputStream("a.txt");

这段代码可能出现文件找不到异常:

FileNotFoundException

编译器会要求你处理。

处理方式有两种:

  1. 使用 try-catch
  2. 使用 throws 抛出

七、try-catch 处理异常

示例:

try {
    FileInputStream fis = new FileInputStream("a.txt");
} catch (FileNotFoundException e) {
    System.out.println("文件不存在");
}

这里如果文件不存在,就会进入 catch

try-catch 的作用是:

捕获异常,并在当前方法中处理它。

八、throws 抛出异常

如果当前方法不想处理异常,可以继续往外抛。

public void readFile() throws FileNotFoundException {
    FileInputStream fis = new FileInputStream("a.txt");
}

调用这个方法的人,需要继续处理这个异常。

public static void main(String[] args) {
    try {
        readFile();
    } catch (FileNotFoundException e) {
        System.out.println("文件不存在");
    }
}

throws 的意思是:

当前方法可能会抛出这个异常,但当前方法不处理,交给调用者处理。

九、throw 和 throws 的区别

这也是面试中经常问的点。

对比 throw throws
位置 方法体内部 方法声明上
作用 主动抛出一个异常对象 声明方法可能抛出异常
后面跟什么 异常对象 异常类型
数量 一次抛一个 可以声明多个

1. throw 示例

public void checkAge(Integer age) {
    if (age == null || age < 0) {
        throw new IllegalArgumentException("年龄不合法");
    }
}

这里是主动抛出异常对象。

2. throws 示例

public void readFile() throws IOException {
    FileInputStream fis = new FileInputStream("a.txt");
}

这里是声明当前方法可能抛出 IOException

简单记忆:

throw:真正抛异常
throws:声明可能抛异常

十、finally 的作用

finally 中的代码一般无论是否发生异常都会执行。

示例:

try {
    int result = 10 / 0;
} catch (Exception e) {
    System.out.println("出现异常");
} finally {
    System.out.println("finally 执行");
}

输出:

出现异常
finally 执行

finally 常用于释放资源,比如关闭流、释放连接等。

FileInputStream fis = null;

try {
    fis = new FileInputStream("a.txt");
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

不过现在更推荐使用 try-with-resources

十一、try-with-resources

try-with-resources 可以自动关闭资源。

示例:

try (FileInputStream fis = new FileInputStream("a.txt")) {
    int data = fis.read();
    System.out.println(data);
} catch (IOException e) {
    e.printStackTrace();
}

只要资源实现了 AutoCloseable 接口,就可以放在 try() 中自动关闭。

这样比手动在 finally 中关闭资源更简洁。

十二、自定义异常

在项目中,我们经常会自定义业务异常。

比如:

public class BusinessException extends RuntimeException {

    public BusinessException(String message) {
        super(message);
    }
}

使用:

public void saveUser(User user) {
    if (user == null) {
        throw new BusinessException("用户不能为空");
    }

    if (user.getUsername() == null) {
        throw new BusinessException("用户名不能为空");
    }
}

为什么继承 RuntimeException

因为业务异常通常不希望每一层都强制 try-catch,而是交给全局异常处理统一处理。

十三、Spring Boot 中的全局异常处理

在 Spring Boot 项目中,可以使用:

@RestControllerAdvice

统一处理异常。

示例:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        return Result.error(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        e.printStackTrace();
        return Result.error("系统异常,请联系管理员");
    }
}

这样 Controller 中就不需要到处写 try-catch

比如:

@PostMapping("/users")
public Result save(@RequestBody User user) {
    userService.save(user);
    return Result.success();
}

如果 Service 中抛出 BusinessException,会被全局异常处理类统一捕获并返回:

{
  "code": 0,
  "msg": "用户名不能为空",
  "data": null
}

这也是项目中非常常见的写法。

十四、异常处理的实际建议

在项目中处理异常时,可以注意下面几点:

  1. 不要直接把异常堆栈返回给前端
  2. Controller 层不要到处写重复的 try-catch
  3. 业务错误可以使用自定义业务异常
  4. 全局异常处理统一返回结果
  5. 捕获异常后不要什么都不做
  6. 不能处理的异常不要乱吞掉
  7. 涉及资源关闭时优先使用 try-with-resources
  8. 日志中可以记录异常详细信息,但响应前端要友好

十五、面试标准回答

如果面试官问:

Java 异常体系是什么?

可以这样回答:

Java 异常体系的顶级父类是 Throwable,它下面主要分为 ErrorException

Error 表示严重错误,比如 OutOfMemoryErrorStackOverflowError,一般不是业务代码能处理的。

Exception 表示异常,程序可以捕获和处理。Exception 又可以分为运行时异常和编译时异常。RuntimeException 及其子类属于运行时异常,编译器不会强制处理,比如空指针异常、下标越界异常、类型转换异常等。除了 RuntimeException 之外的很多异常属于编译时异常,编译器会强制使用 try-catchthrows 处理,比如 IOException

实际项目中,业务异常通常会自定义异常类,并配合 Spring Boot 的全局异常处理统一返回结果。

十六、常见问题总结

1. Error 和 Exception 有什么区别?

Error 是严重错误,一般程序无法处理。

Exception 是异常,程序可以捕获和处理。

2. 运行时异常和编译时异常有什么区别?

运行时异常编译器不强制处理。

编译时异常编译器会强制处理,要么 try-catch,要么 throws

3. throw 和 throws 有什么区别?

throw 用在方法体中,表示主动抛出异常对象。

throws 用在方法声明上,表示当前方法可能抛出异常。

4. finally 一定会执行吗?

大多数情况下会执行。

但如果 JVM 直接退出,比如执行:

System.exit(0);

finally 可能不会执行。

5. 自定义业务异常一般继承什么?

一般继承:

RuntimeException

这样可以配合全局异常处理统一处理,不需要每层都强制捕获。

十七、总结

这一篇主要学习了 Java 异常体系。

Java 异常顶级父类是 Throwable,下面分为 ErrorExceptionError 表示严重错误,通常不由业务代码处理;Exception 表示异常,可以被程序捕获和处理。

Exception 又可以分为运行时异常和编译时异常。运行时异常编译器不强制处理,编译时异常必须处理。

在实际项目中,我们经常会自定义业务异常,并配合 Spring Boot 全局异常处理,让接口返回更加统一、友好。

下一篇我们继续学习 finalfinallyfinalize 的区别。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐