Java的异常机制

简介

异常:异常并不是指语法错误,而是指程序运行过程中可能出现的错误,有一部分错误是在编写代码时就可以预料到有可能出现的,所以在编译时必须处理,例如,要读取一个外部文件,就有可能遇到文件不存在的情况,在代码中就需要处理这种情况,文件不存在,就是一个可能出现的异常,在Java中就是FileNotFoundException。

异常机制:异常机制就是当程序出现错误,程序安全退出的机制。异常处理是衡量一门语言是否成熟的标准之一

异常处理机制的好处:增加Java语言的健壮性;把异常流程的代码和正常业务代码分开,让程序更简洁。

异常的处理方式:Java 通过面向对象的方法来处理异常,具体分为两个步骤

  • 抛出异常:在一个方法的运行过程中,如果发生了异常,则这个方法会产生代表该异常的一个对象,并把它交给运行时的系统,这个动作称为抛出异常。
  • 捕获异常:运行时系统在方法的调用栈中查找,直到找到能够处理该类型异常的代码,这一个过程称为捕获(catch)异常,如果找不到,就把异常交给虚拟机处理。

基本使用

异常分类

编译期异常:由外部错误引起的异常。正确的程序在运行中,很容易出现的、情理可容的异常状况,在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。Java编译器会检查这种异常,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过,所以它又称受检查的异常

运行期异常:由程序内部错误引起的异常。在编译期不会检查这些异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

JVM产生的异常:这种异常Java程序无法处理,只能打印异常信息并退出程序

异常类的体系

Throwable类:所有异常类型的祖先类,只有继承了 Throwable 的类才能被 throw 或 catch

Error类:Throwable类的直接子类,通常表示jvm异常,如堆栈溢出,

Exception类:Throwable类的直接子类,用于Java程序可能出现的异常情况,如果用户要创建自定义异常类,也是继承Exception类。Exception 下又分为运行时异常和非运行时异常。

  • RuntimeException:运行时异常类,在程序运行过程中,由程序本身的错误引起的异常,如NullPointerException、IndexOutOfBoundsException,在编译期不会检查这些异常。一个异常类如果继承自RuntimeException,在方法中抛出后不需要在方法签名上进行声明,
  • 非运行时异常:指的是RuntimeException类及其子类以外的异常,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能通过编译。

相关源码:除了核心父类Throwable,存储了异常的堆栈信息,其它相关子类都比较简单,只是调用父类的构造方法,传入自己独有的异常描述信息,

处理异常的语法

异常处理涉及到的关键字:

  • try:可能出现异常的语句,放在try块中
  • catch:捕获并处理异常,
  • finally:finally块中的语句是在任何情况下都必须执行的代码,除非jvm退出,
  • throw:在方法体中抛出异常实例,
  • throws:在方法签名上声明可能会出现的异常
  • assert:断言。严格来讲assert应该是用在单元测试中的,正常代码中如果想要assert关键字生效,需要在程序启动时特殊配置,鉴于有时候会在代码中遇到通过assert来进行断言,处理异常情况的案例,这里做个介绍,但是正常代码中不应该出现assert关键字

抛出异常 throws、throw

throws:当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法外部进行处理,使用 throws 在方法声明处声明一个异常。

使用 throws 声明异常的思路是:

  • 当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;
  • 如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。
  • JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是程序在遇到异常后自动结束的原因。

throw 抛出异常:语法 throw new ExceptionType,在方法体中抛出一个异常对象,只有继承了RuntimeException的类才可以被throw抛出。

案例:throw和throws,在下面的程序中,要求用户在运行程序时,向程序传入一个参数,程序会把这个参数作为一个文件名来解析,如果用户没有传入参数,那么就抛出一个运行时异常,如果根据文件名无法找到文件,程序不会处理这个异常,而是会按照默认方式抛给虚拟机。

java 复制代码
public class Test3 {
    public static void main(String[] args) throws FileNotFoundException {
        if (args.length == 0) {
            throw new RuntimeException("请输入文件名");
        }

        String fileName = args[0];
        FileInputStream fileInputStream = new FileInputStream(fileName);
    }
}

这里演示了throw和throws的基本使用,throw,用于抛出一个运行时异常,在本案例中,程序启动时,用户可能输入了参数,也可能没有输入参数,如果没有输入参数,程序无法继续,就要抛出一个运行时异常,提醒用户,然后终止程序。throws,用于在方法上声明一个编译时异常,在本案例中,用户输入的文件名, 根据它可能会找到一个文件也可能不会,这是编译时可以预料到的,所以是一个编译时异常,如果无法处理,需要在方法上声明该异常。

这里详细解释一下,为什么在这里没有输入参数属于运行时异常:因为这属于程序使用逻辑错误,是开发者或用户违反了程序的预期使用方式,程序的设计意图是 "必须传入文件名参数才能运行",如果没有传入,本质上是 "调用方式错误",属于开发者或用户的疏漏。这类错误理论上是可以提前避免的,比如在文档中明确要求传入参数,或者在测试阶段覆盖这种情况,因此Java不强制要求编译时处理,而是归类为运行时异常,由开发者在逻辑上保证避免。

为什么 "文件不存在" 是编译时异常?文件不存在会抛出编译时异常,因为这属于程序外部环境导致的可预见情况,与程序逻辑无关,且无法通过代码完全避免,即使开发者严格要求传入文件名,也无法保证用户输入的文件一定存在,可能用户输错文件名、文件被删除、路径错误等,这是程序运行时依赖的外部资源的状态决定的,属于不可控但可预见的场景。Java 设计编译时异常的目的就是强制开发者提前考虑并处理这类情况,比如提示用户 "文件不存在,请检查路径",否则程序可能在运行时毫无征兆地崩溃。因此编译器会强制要求处理。

总结:编译时异常和运行时异常的区别,运行时异常是程序内部的逻辑错误,可以避免,编译时异常是程序外部的错误,在程序内无法避免,需要在编译时考虑这种情况。

捕获异常 try catch finally

上面的代码演示了如何抛出异常,抛出异常是交给虚拟机处理,用户也可以自己处理这些异常,把异常信息进行友好输出,非致命异常,可以吞掉,让程序继续进行。使用try、catch、finally来实现异常处理。

格式:

text 复制代码
try{
   // 可能发生异常的语句,try 块中声明的变量仅仅是局部变量,外部无法访问
} catch(ExceptionType | ExceptionType2 [, ....] e) {
   // 处理异常语句
}[.....
][finally{
   // 无论有没有异常都会执行的语句。
}]

在 try 块后可以跟多个 catch 块,每一个 catch 块所对应的异常范围应该是越来越大。

执行顺序:

  • 如果在catch块中抛出异常:先执行finally中的语句,然后再抛异常
  • 在 finally 块中抛出的任何异常都会覆盖掉在其前面由 try 或者 catch 块抛出异常。包含 return 语句的情形相似。鉴于此,除必要情况下,应该尽量避免在 finally 块中抛异常或者包含 return 语句。

案例1:继续上一个案例,使用try catch处理编译时异常,捕获异常,输出对用户友好的异常信息,然后程序继续或退出。

java 复制代码
public class Test4 {
    public static void main(String[] args) {
        if (args.length == 0) {
            throw new RuntimeException("请输入文件名");
        }

        String fileName = args[0];
        try {
            FileInputStream fileInputStream = new FileInputStream(fileName);
        } catch (FileNotFoundException e) {
            System.out.println("文件 " + fileName +  " 不存在,程序退出");
        }
    }
}

案例2:finally的使用。网络编程中的一个案例,在finally块中关闭socket、输入流等资源

java 复制代码
public static void processBusiness(Socket socket) {
    InputStream inputStream = null;
    try {
        // 业务处理,读取输入
        inputStream = socket.getInputStream();
        int len;
        while ((len = inputStream.read(bytes)) != -1) {
            String input = new String(bytes, 0, len);
            System.out.println("[" +
                    Thread.currentThread().getName()
                    + "]客户端发来的数据:"
                    + input);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 关闭资源
        if (inputStream != null){
            try {
                socket.shutdownInput();
                inputStream.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

案例3:多个catch块

java 复制代码
public static Long convertDateStrToTimestamp(String date) {
    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
    try {
        Date dateObj = formatter.parse(date);
        return dateObj.getTime();
    } catch (ParseException e) {
        return 0L;
    } catch (Exception e) {
        return -1L;
    }
}

从上往下,异常范围应该越来越大。

案例4:在一个catch块中捕获多个异常

java 复制代码
public static Long convertDateStrToTimestamp(String date) {
    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
    try {
        FileInputStream fileInputStream = new FileInputStream(date);
        Date dateObj = formatter.parse(date);
        return dateObj.getTime();
    } catch (ParseException | FileNotFoundException e) {
        return 0L;
    } catch (Exception e) {
        return -1L;
    }
}

如果一个try块中涉及到多个编译时异常,可以把采用这种写法,但是通常不推荐。

案例5:在finally块中return返回值。

java 复制代码
public class Test5 {
    public static void main(String[] args) {
        Long l = convertDateStrToTimestamp("2025-02-28");
        System.out.println("l = " + l);
    }

    public static Long convertDateStrToTimestamp(String date) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
        try {
            Date dateObj = formatter.parse(date);
            return dateObj.getTime();
        } catch (ParseException e) {
            return 0L;
        } finally {
            return -1L;
        }
    }
}

结果:finally块中的return会覆盖掉正确的返回值,所以尽量不要在finally中return返回值

assert 断言

断言:assert,如果希望在不满足某些条件时阻止代码的执行,就可以考虑用断言来阻止它。断言在软件开发中是一种常用的调试方式,断言用于保证程序最基本、关键的正确性。

断言语法:assert <boolean expression> [: "<message>"]

java 1.4 增加了'断言'的特性,断言是为了调试程序,并不是发布程序的一部分,默认情况下 jvm 是关闭断言的,如果想要使用断言调试程序,需要手动打开断言程序

不要在正式代码中使用断言,只在单元测试中使用,如果需要启用,程序运行时添加虚拟机参数 -ea

案例:这是在junit框架中使用的

java 复制代码
@Test
public void test2() {
    Properties properties = System.getProperties();
    assert properties != null;
}

如果properties为null,抛出断言异常

text 复制代码
java.lang.AssertionError
	at org.wyj.LearnAssertTest.test2(LearnAssertTest.java:28)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)

自动资源管理

java 7引入了自动资源管理的特性,用于自动关闭实现了AutoCloseable接口的资源,无需手动在finally块中关闭,这大大简化了资源管理的代码,并减少了资源泄露的风险。

格式:

text 复制代码
try (声明或初始化资源语句) {
   // 可能会出现异常的语句
}catch(ExceptionType var){
   // 异常处理语句
}[....]

内部机制:

  • try 语句中声明的资源被隐式声明为 final,资源的作用局限于 try 语句,可以在 try 语句中声明或初始化多个资源,用 ';' 隔开即可,需要关闭的资源必须实现了AutoClosable 或 Closable 接口。
  • 声明资源变量的括号中,不可以为已有的成员变量赋值
  • 资源按照声明相反的顺序关闭,最后声明的资源最先关闭。

源码:

java 复制代码
// AutoCloseable接口
public interface AutoCloseable {
    void close() throws Exception;
}

// Closeable接口
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

使用案例:

java 复制代码
public static void main(String[] args) {
    // 自动关闭 BufferedReader 和 FileReader
    try (FileReader fr = new FileReader("test.txt");
         BufferedReader br = new BufferedReader(fr)) {
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    // 资源会在 try 块结束后自动关闭
}

传统写法:

java 复制代码
public static void main(String[] args) {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader("test.txt"));
        // 使用资源...
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close(); // 需要嵌套 try-catch
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

自定义异常

在实际开发中,自定义异常可以更加清晰地表达业务,团队通常会针对不同的情况,定义专属的异常类。自定义异常可以更精准地描述业务逻辑中的错误场景,提高代码的可读性和可维护性。

定义不同类型的异常:

  • 自定义一个编译时异常:需要继承于java.lang.Exception
  • 自定义一个运行时异常:需要继承于java.lang.RuntimeException

异常类的编写:在自定义异常中,调用父类的构造方法,传入一个字符串,描述异常信息,同时异常类还可以承载其它的异常信息,例如异常编码等。

案例1:自定义运行时异常,代表一个业务异常,所有的业务异常都使用当前异常类

java 复制代码
public class BizException extends RuntimeException {
    private String code;

    public BizException() {
    }

    public BizException(String message) {
        super(message);
        this.code = "1";  // 默认错误码是系统错误
    }

    public BizException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BizException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public BizException(String code, Throwable cause) {
        super(cause);
        this.code = code;
    }

    public String toString() {
        return "code:" + this.code + ", msg:" + this.getMessage();
    }

    /**
     * 业务异常不需要收集线程的整个异常栈信息,重写fillInStackTrace方法,,可以减少业务异常抛出导致的开销
     */
    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }
}

当前异常类中自定义了异常码,不同的异常码代表不同的异常,同时,还会抑制堆栈信息的打印,因为业务异常通常不是系统问题,不需要堆栈信息,但是这种情况下要注意,每个业务异常都必须是独一无二的,否则可能会混淆。

当前案例中,在不打印业务异常的堆栈信息的情况下,如果是业务异常的背后是一个系统异常,例如日期字符串解析错误导致需要抛出业务异常,此时,可以把系统异常传递给业务异常,就会正常打印堆栈信息;如果业务异常的背后是用户使用错误,例如用户没有传递某个应该传递的参数,此时,就需要一个独一无二的异常信息,避免混淆

处理异常的原则

为一个基本操作定义一个 try-catch 块:不要将几百行代码放到一个 try-catch 块中,异常只能用于非正常情况,try-catch的存在也会影响性能,尽量缩小try-catch的代码范围;

尽量在循环之外使用try-catch

尽量避免运行时异常的出现,如NullPointerException,提前做非空判断

如果方法需要被上层API调用,方法中的编译时异常最好抛出,如果方法是最外层的方法,必须处理异常

明确异常的级别,异常是否需要阻塞主流程,例如,用户在页面修改数据,后端要做两个改动,一是保存数据库,二是记录日志,如果保存数据库时发生异常,那么必须阻塞流程,向用户抛出友好的异常信息,如果是记录日志时发生异常,可以不用阻塞流程,而是选择把异常吞掉,仅仅打印异常日志,然后查看日志来解决问题即可。这就是异常的级别,异常是主流程相关还是和主流程无关,是否需要阻塞主流程。

需要为异常提供说明文档:可以参考Java doc,如果自定义了异常或某一个方法抛出了异常,应该在文档注释中详细说明;

常见的异常类

编译时异常:

  • IOException :IO异常
  • ParseException:日期解析失败等
  • ClassNotFoundException:类不存在

运行时异常:

  • NullPointerException:空指针异常,对空对象进行除了赋值以外的任何操作都会报空指针异常,比如调用空对象的方法,对空对象中的字段进行赋值
  • ArrayIndexOutOfBoundsException:数组的索引越界异常,操作数组时使用的索引超出了数组的数据范围会出现;
  • NumberFormatException:数字格式化异常,把非数字的数据类型转换为数字类型时使用了非法的转换对象;
  • IllegalArgumentException:传入的参数不正确
  • ArithmeticException:算术异常
  • ClassCastException:类转换异常
  • IllegalArgumentException:非法参数异常
  • IndexOutOfBoundsException:下标越界异常
  • SecurityException:安全异常

编码过程中的常见告警

  • 'InputStream' used without 'try-with-resources' statement: 'InputStream'不带'try-with-resources'语句使用,这是编译器的一个警告,对于InputStream的使用应该放在 try with resources 语句中
  • dangling javadoc document:悬空的Java文档。文档注释只有添加到类上或方法上,才可以生成文档,添加到其它地方和普通注释的效果是一样的。当把一个文档注释添加到方法内的时候,就会报这个告警
相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔6 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说12 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json