谈谈 Java 中的 Exception 和 Error

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

这个世界上有永远不会出错的程序吗?小伙子,你活在梦里呢。所以对于一门现代的高级语言,一般都会设计一套完善的异常处理机制,Java 正是其中之一,通过异常处理机制,可以大大的降低编写和维护可靠程序的门槛。so 今天的主题,我们就来讨论一下 Java 中 Exception 和 Error 的区别。

面试官:请说说 Exception 和 Error 的区别,另外,运行时异常和一般异常有啥区别?

经典回答

首先,Exception 和 Error 都继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被捕获(catch)或者是抛出 (throw), 它是异常处理机制的基本组成。

Exception 和 Error 体现了 Java 设计者对不同类型的分类。Exception 通常是程序正常运行,可以被预料的异常,可以被捕获并进行相应处理的。而 Error 指的是正常情况下不大可能会出现的异常,绝大多数 Error 都会导致程序处于非正常的,不可恢复的状态。它也是不便于也是不需要被捕获的。比如说比较经典的 OutOfMemoryError 这种 Error 的子类。

还需要特别说明的是,Exception 又分为可检查异常不检查异常两类。

对于可检查异常,我们在代码需要显式的去捕获,并作出相应的处理,它属于编译检查的一部分。而不检查异常呢,类似 NullPointerException, ArrayIndexOutOfBoundsException 之类,我们是可以在程序中进行判断来避免的,所以编译期并不会强制要求你去捕获。

下面给出 Exception, Error 的继承关系图,和常见的一些异常:

知识拓展

对于异常,我们因秉持的两个编码原则

  • 1.尽量捕获特定的异常,而不是捕获 Exception 这样通用的异常:

    try {
      // 业务代码
      // …
      Thread.sleep(1000L);
    } catch (Exception e) {
      // Ignore it
    }
    

上面这段代码就违反了第一条原则,Thread.sleep(1000L) 会抛出 InterruptedException,我们应该捕获它,而不是捕获通用的 Exception 类异常。要知道,日常工作中,我们读代码的时间要超过写代码的时间的,读别人写的代码,如果可维护性很差,你一定觉得特别痛苦,甚至是无从下手。所以,我们自己的编码时,应尽量让我们的代码直观的展现出更多的信息, 而泛泛的 Exception 实际上隐藏了我们原始的目的。

进一步来说,除非你经过深思熟虑了,确实要捕获 Exception 和 Error, 才去做这样的工作。

  • 2.不要生吞异常:

线上系统中,最忌捕获了却啥事不做的情况发生,导致系统发生错误后,无法定位到问题发生在哪里,如下面这段代码:

  try {
    // 业务代码
    // …
  } catch (Exception e) {
    // 什么事都不做
  }

你至少需要打印错误日志,才能保证发生问题了,有迹可寻,能够快速的定位到问题。

对于异常,我们还应秉持 Throw early, catch late 原则

public void readPreferences(String fileName){
   //...perform operations... 
  InputStream in = new FileInputStream(fileName);
   //...read the preferences file...
}

上面这段代码健壮性是不够的,你无法保证别人传递进来的 fileName 是否为空,若是 null, 就会抛出 NullPointerException, 由于没有第一时间抛出,导致打印出的堆栈信息往往很费解,这无疑增加的锁定问题的时间成本。如果我们能够秉持 Throw early 的原则:

public void readPreferences(String filename) {
  Objects. requireNonNull(filename);
  //...perform other operations... 
  InputStream in = new FileInputStream(filename);
   //...read the preferences file...
}

这样我们就能第一时间抛出 NullPointerException,而没有额外的错误堆栈输出,排查问题也能够快速定位到。

从性能角度审视 Java 异常处理机制

Java 的异常处理机制有两个比较昂贵的地方:

  • try-catch 代码段会产生额外的性能开销,就是说它会影响 JVM 对代码的进行性能优化,所以这里仅建议你捕获有必要捕获的代码片段,不要用一个大的 try 包住整个代码段;同时,利用异常控制代码流程也不是一个好的方案,它远比我们使用 if-else, switch 要低效。

  • Java 每实例化一个 Exception, 都会对当时的栈进行快照,这是一个很重的过程。如果程序中发生频繁,那开销就不能被忽略了。

所以,当我们发现服务出现反应变慢,吞吐量下降时,排查问题时,检查最频繁的 Exception 也是一种思路。

总结

今天主要从经典的面试题,Exception 和 Error 的区别为切入点,介绍了两者的区别。以及 Exception 的可检查异常不检查异常。另外还有我们在处理异常时,需要秉持的两条原则。最后,我们又从性能的角度审视了 Java 的异常处理机制,以便我们能够编写出更加高效的代码。