异常处理

异常处理


异常处理

程序运行过程中,可能会出现错误,Java 使用异常机制来处理这些错误。

异常的概念与分类

异常就是程序运行过程中出现的不正常情况(错误),Java 把异常封装成对象,统一处理。

异常继承体系

flowchart BT
    Throwable --> Error
    Throwable --> Exception
    Exception --> RuntimeException
    Exception --> 非运行时异常(检查异常)
  • Throwable:所有错误和异常的根类
  • Error:错误,严重问题,JVM 层面问题,比如 OutOfMemoryError、StackOverflowError,程序无法处理,只能避免
  • Exception:异常,程序可以处理
  • RuntimeException:运行时异常,不检查,比如 NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException,一般是代码错误导致,需要改进代码避免
  • 编译时异常(Checked Exception):编译时必须处理,否则编译不通过,比如 IOException、SQLException

处理编译时异常的方法有两种:

  1. try-catch捕获这个异常,throw RuntimeException 自己处理了这个异常,程序可以继续进行
  2. throws抛出异常,我不处理这个异常,向外抛出异常,外部没有处理会终止程序
对比维度 编译时异常(受检异常 CheckedException) 运行时异常(非受检异常 UncheckedException)
继承关系 直接 / 间接继承 Exception,不是RuntimeException子类 继承 RuntimeException(RuntimeException本身是Exception的子类)
编译器检查 编译期强制校验抛出异常必须 try-catch 捕获 或 throws 声明 编译器不强制校验抛出异常无需声明 throws,可选择性捕获
出现时机 异常对象运行时抛出;只是编译阶段要求提前处理 异常对象运行时抛出;编译阶段无强制要求
设计初衷 可预期、大概率能够修复的异常(外部环境问题) 程序代码 BUG、逻辑错误,一般无法在代码内恢复
代表场景 文件读写、网络、数据库、反射、日期解析 空指针、数组越界、类型强转失败、除 0 运算
自定义异常写法 class MyException extends Exception{} class MyException extends RuntimeException{}
企业开发推荐 极少使用 业务异常首选(SpringBoot 主流方案)

编译时异常

异常名称 产生场景说明
IOException 文件、流读写、网络 IO 操作异常(父类)
FileNotFoundException 读取文件时,目标文件不存在
SQLException JDBC 操作数据库发生异常
ParseException SimpleDateFormat日期字符串解析失败
ClassNotFoundException 反射 Class.forName() 找不到目标类
NoSuchMethodException 反射找不到指定方法
NoSuchFieldException 反射找不到成员变量
InterruptedException sleep()/wait()线程被中断
CloneNotSupportedException 对象调用 clone () 但未实现 Cloneable 接口
MalformedURLException URL 地址格式不合法

运行时异常:

异常类 说明
NullPointerException 空指针异常,调用了 null 对象的方法或属性
ArrayIndexOutOfBoundsException 数组索引越界异常
ClassCastException 类型转换异常
IllegalArgumentException 参数非法异常
ArithmeticException 算术异常,比如除零
IllegalArgumentException 传入非法参数
NumberFormatException 字符串转数字格式错误(属于 IllegalArgumentException 子类)
IndexOutOfBoundsException 集合、数组索引越界父类

try-catch-finally

捕获异常,处理异常:

语法:

try {
    // 可能出异常的代码
} catch (异常类型 变量) {
    // 捕获到异常后的处理代码
} finally {
    // 一定会执行的代码,用于释放资源
}

执行流程:

  1. try 中代码如果没有异常,跳过 catch,执行 finally
  2. try 中如果出现异常,跳转到 catch,执行 catch 中代码,然后执行 finally

示例:

public class Demo {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        try {
            // 这里会发生索引越界
            System.out.println(arr[10]);
        } catch (ArrayIndexOutOfBoundsException e) {
            // 捕获异常,处理
            System.out.println("索引越界了:" + e.getMessage());
            // 打印异常堆栈信息,方便调试
            e.printStackTrace();
        } finally {
            System.out.println("finally 一定会执行");
        }
        System.out.println("程序继续执行");
    }
}

💡 如果没有 try-catch,异常会向上抛,最终到 JVM,JVM 会打印异常信息然后终止程序。用了 try-catch,程序可以继续执行。

try-catch 多种异常

可以多个 catch 捕获不同异常:

try {
    // ...
} catch (ArrayIndexOutOfBoundsException e) {
    // 处理索引越界
} catch (NullPointerException e) {
    // 处理空指针
} catch (Exception e) {
    // 捕获其他异常,放最后
}

Java 7+ 支持多异常同 catch:

try {
    // ...
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
    // 两种异常同处理
}

finally 的作用

finally 用来释放资源,因为 finally 一定会执行,不管有没有异常:

FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    // 读取文件...
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 关闭流,释放资源,一定会执行
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Java 7+ 提供了 try-with-resources 语法,自动关闭资源,更简洁:

try (FileInputStream fis = new FileInputStream("test.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 读取文件...
} catch (IOException e) {
    e.printStackTrace();
}
// 自动关闭资源,不需要写 finally

💡 推荐使用 try-with-resources,代码更简洁,不容易出错。

throws 与 throw

throws 声明抛出异常

方法内部可能出异常,自己不处理,声明抛出,让调用者处理:

// 方法声明上 throws 异常
public void readFile(String path) throws IOException {
    FileInputStream fis = new FileInputStream(path);
    // ...
}

调用这个方法时,必须处理这个异常:要么继续 throws,要么 try-catch。

throw 手动抛出异常

我们可以手动抛出异常对象:

public void setAge(int age) {
    if (age < 0 || age > 150) {
        // 手动抛出异常
        throw new IllegalArgumentException("年龄不合法:" + age);
    }
    this.age = age;
}

throw 抛出的是一个异常对象。

对比 throws 和 throw:

throw throws
方法内部,抛出一个异常对象 方法声明上,声明抛出哪些异常
throw new 异常对象() throws 异常类型1, 异常类型2

自定义异常

如果需要,我们可以自定义异常类:

步骤:

  1. 继承 Exception 或 RuntimeException
  2. 提供无参构造
  3. 提供有参构造(接收错误信息)

继承 Exception时要受检要主动声明异常 throws MeiQianException

继承RuntimeException时为非受检,可以不用声明

  @Test
    public void test4()  {
      try {
          meiqian(1100);

      } catch (MeiQianException e) {
          e.printStackTrace();
      }
    }
    public void meiqian(int money) throws MeiQianException {
        if(money >= 1000){
            throw new MeiQianException("余额不足");
        }else {
            System.out.println("余额充足");
        }
    }
// 自定义业务异常
public class MeiQianException extends RuntimeException {
    public MeiQianException() {
        super();
    }

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

    public MeiQianException(String message, Throwable cause) {
        super(message, cause);
    }
}

使用:

if (余额不足) {
    throw new BusinessException("余额不足,无法支付");
}

异常链与日志

捕获底层异常后,如果要抛出更符合业务语义的新异常,应保留原始异常作为 cause,形成异常链:

try {
    repository.save(order);
} catch (IOException e) {
    throw new BusinessException("保存订单失败", e);
}

这样上层既能看到业务错误信息,也能通过 cause 追踪到底层原因。

在真实项目中,不建议到处使用 e.printStackTrace();更常见做法是使用日志框架记录异常:

// 示例写法,具体日志框架在后续章节介绍
logger.error("保存订单失败,orderId={}", orderId, e);

异常处理最佳实践

  1. 异常不能被吞掉:catch 块不要空着什么都不做,至少打印异常堆栈:
   } catch (Exception e) {
       // 不好:什么都不做,出了问题找不到
       // 好:至少打印
       e.printStackTrace();
   }
   
  1. 不要用异常控制流程:异常是处理错误情况,不要用异常来做条件判断,性能差
  1. 尽早抛出异常:发现问题就立即抛出
  1. 分层处理:底层抛,上层捕获处理,UI 层提示用户,日志记录
  1. 优先使用 try-with-resources:关闭资源更方便
  1. 保留异常链:包装异常时把原始异常作为 cause 传入,不要丢失根因
  1. 合适的粒度:不要把一大段代码都放在 try 里面,只把可能出异常的代码放进去
上一篇
下一篇