谈谈 Java 中的 Exception 和 Error

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

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 63w+ 字,讲解图 2808+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2200+ 小伙伴加入学习 ,欢迎点击围观

这个世界上有永远不会出错的程序吗?小伙子,你活在梦里呢。所以对于一门现代的高级语言,一般都会设计一套完善的异常处理机制,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 的异常处理机制,以便我们能够编写出更加高效的代码。

相关文章