Java异常学习笔记

目录

异常族谱

Error

Exception

checked

unchecked

例子:NoClassDefFoundError和ClassNotFoundException

Java 的异常处理机制

try catch finally

异常处理的两个基本原则

Throw early和catch late

自定义异常

 


异常族谱

 

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

Error

是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。通俗的说就是系统错误,虚拟机出错,我们处理不了,也不需要我们来处理。

Exception

Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。

checked

checked异常可以进一步细分为两类 :

  • 无能为力、引起注意型。针对此类异常,程序无法处理,如字段超长等导致SQLException,即使做再多重试对解决异常也没有任何帮助 一般处理此类异常的做法是完整地保存异常现场,供开发工程师介入解决。
  • 力所能及、坦然处置型。如发生未授权异常 UnAuthorizedException 序可跳转至权限申请页面。

unchecked

不检查异常就是所谓的运行时异常,它们都继承自RuntimeException不需要程进行显式捕捉和处理。unchecked 异常可以进 一步细分为三类:

  • 可预测异常( Predicted Exception ) 常见的可预测异常包括IndexOutOtBoundsException、NullPointerException 基于对代码的性能和稳定性要求此类异常不应该被产生或者抛出而应该提前做好 界检查、空指针判断等处理。显式的声明或者捕获此类异常会对程序的可读性和运行效率产生很大影响。
  • 需捕捉异常( Caution Exception ),例如在使用Dubbo框架进行RPC调用时产生的远程服务超时异常DubboTimeoutException,此类异常是客户端必须显式处理的异常,不能因服务端的异常导致客户端不可用,此时处理方案可以是重试或者降级处理等。
  • 可透出异常(Ignored Exception),主要是指框架或系统产生的且会自行处理 的异常,而程序无须关心。例如针对Spring框架中抛出的 NoSuchRequestHa ndlingMethodException异常,Spring 框架会自己完成异常的处理,默认将自身抛出的异常自动映射到合适的状态码,比如启动防护机制跳转到 404 页面。

例子:NoClassDefFoundError和ClassNotFoundException

ClassNotFoundException是一个运行时异常。从类继承层次上来看,ClassNotFoundException是从Exception继承的,所以ClassNotFoundException是一个检查异常。

当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。

  最简单的,当我们使用JDBC去连接数据库的时候,我们一般会使用Class.forName()的方式去加载JDBC的驱动,如果我们没有将对应的jar包放到应用的classpath下,那么会导致运行时找不到类,所以运行Class.forName()会抛出ClassNotFoundException。

NoClassDefFoundError异常,看命名后缀是一个Error。从类继承层次上看,NoClassDefFoundError是从Error继承的。和ClassNotFoundException相比,明显的一个区别是,NoClassDefFoundError并不需要应用程序去关心catch的问题。

当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。比如当我们在new一个类的实例的时候,如果在运行是类找不到,则会抛出一个NoClassDefFoundError的错误。

假如我们建立一个Demo.java,然后编译之后将Demo.class文件删除,那么运行时就会输出NoClassDefFoundError。

由此我们能看出两者最大的区别就是NoClassDefFoundError是运行时发生了某些错误,而ClassNotFoundException是在编译时期就检查出了错误。

Java 的异常处理机制

try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。

Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的 Exception 也是一种思路。

一个函数尽管抛出了多个异常,但是只有一个异常可被传播到调用端。最后被抛出的异常时唯一被调用端接收的异常,其他异常都会被吞没掩盖。如果调用端要知道造成失败的最初原因,程序之中就绝不能掩盖任何异常。

try catch finally

( I ) try 代码块:监视代码执行过程,一旦发现异常则直接跳转至 catch,如果没有catch,则直接跳转至 finally。

( 2 ) catch 代码块:可选执行的代码块,如果没有任何异常发生则不会执行,如果发现异常则进行处理或向上抛出。这一切都在 catch 代码块中执行。

( 3 ) finally 代码块:必选执行的代码块,不管是否有异常产生,即使发生 OutOfMemoryError也会执行,通常用于处理善后清理工作。

如果 finally 代码块没有执行,那么有三种可能:

  • 没有进入 try 代码块。
  • 进入代码块,但是代码运行中出现了死循环或死锁状态。
  • 进入 try 代码块,但是执行了 System.exit()操作。

注意, finally是在 return 表达式运行后执行的,此时将要return的结果已经被暂存起来,finally代码块执行结束后再将之前暂存的结果返回。

但是如果在finally中发生了return,那么最终获得的返回值是finally中return的。但是如果没有发生异常的话,try中的return表达式已经被计算过了,这样会导致返回值变得非常不可控,所以避免在finally中写任何return语句。

finally代码块的职责不在于对变量进行赋值等操作,而是清理资源、释放连接、关闭管道流等操作,此时如果有异常也要做 try-catch。

函数返回值有两种类型:值类型与对象引用。对于对象引用,要特别小心,如果在finally代码块中对函数返回的对象成员属性进行了修改,即使不在finally块中显式调用return语句,这个修改也会作用于返回值上。

异常处理的两个基本原则

尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。

 

try {  
// 业务代码 
// …  Thread.sleep(1000L); 
} catch (Exception e) {//在这里是 Thread.sleep() 抛出的 InterruptedException。
  // Ignore it 
}

这是因为在日常的开发和合作中,我们读代码的机会往往超过写代码,软件工程是门协作的艺术,所以我们有义务让自己的代码能够直观地体现出尽量多的信息,而泛泛的 Exception之类,恰恰隐藏了我们的目的。另外,我们也要保证程序不会捕获到我们不希望捕获的异常。比如,你可能更希望 RuntimeException 被扩散出来,而不是被捕获。

进一步讲,除非深思熟虑了,否则不要捕获 Throwable 或者 Error,这样很难保证我们能够正 确程序处理 OutOfMemoryError。

不要生吞(swallow)异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。

生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不 要在产品代码做这种假设!

如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不 可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。

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...

}

至于“catch late”,其实是我们经常苦恼的问题,捕获异常后,需要怎么处理呢?最差的处理方式,就是我前面提到的“生吞异常”,本质上其实是掩盖问题。如果实在不知道如何处理,可以选择保留原有异常的 cause 信息,直接再抛出或者构建新的异常抛出去。在更高层面,因为有了清晰的(业务)逻辑,往往会更清楚合适的处理方式是什么。

自定义异常

  • 是否需要定义成 Checked Exception,因为这种类型设计的初衷更是为了从异常情况恢复,作为异常设计者,我们往往有充足信息进行分类。
  • 在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。如果我们看 Java 的标准类库,你可能注意到类似 java.net.ConnectException,出错信息是类似“ Connection refused (Connection refused)”,而不包含具体的机器名、IP、端口等,一个重要考量就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面的。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值