我学习了很多语言,但是目前,代码还是java写的最多,也是java写的最舒服。原因有很多,其中之一就是java的异常检查机制。
我们都知道在java中有一个RuntimeException类,这个类经常在面试java的时候被问,面试官会问你啥是运行时异常,和普通异常的区别在哪里?
这时平常不细心的同学可能就讲不到重点。实际上这是java非常重要的一个机制,也是java最独一无二的特性之一:异常检查。
其实,java做的并不是把异常分为了运行时和非运行时两种,而是分为了受检(checked),不受检(unchecked)两种,前二者只是后二者在类继承关系上的体现而已,并非这个机制的本质。
简单说来呢,java的RuntimeException就和java的老师:C++中的异常一样,不强制catch或者throw,即不受编译器检查。而非RuntimeException的处理机制则是java自己的发明(更像是实验),它提供了除返回值以外的一个通道,能让程序员在开发时,事先对调用栈底层可能出现的异常状态进行处理,因为编译器或者IDE会告诉你这里有个可能的异常,请catch或者throw,这也就是所谓的受编译器检查了。
这个理念的出发点其实很好,我们的程序在运行时总会出现各种意外的情况,很多时候是我们的代码出了问题,需要去debug。但也有很多时候,并不是我们代码的问题。举个例子,A调B的API读文件,出了问题就说B的接口有bug,结果后来发现A读取的路径下压根就没这文件,B的代码没有问题,是A忘记了要写文件不存在时的处理代码。如果能有一个办法,简单的将锅分清楚,那岂不美哉。所以java是这么做的,B的API开发者在代码中写个逻辑,并申明:当读取的文件不存在,会抛出FileNotFoundException。意思是:这个问题需要由API的调用方去处理。因为这是个受检异常,所以编译器会强制要求调用者处理这个可能发生的异常状态,也就让调用者提前注意到了这个问题,比如这里,A就可以catch一哈,如果FileNotFound,就去建一个新文件。
一切看起来似乎都很完美,调用栈底层的异常状态通过一个高维度的信息通道反馈给了表层。但是问题也随之而来,如果在一段代码里调用了很多接口,这些接口各有各的异常状态会怎么样?
所以这个时候有的开发者就嫌麻烦,直接搞个带括号,头尾一包,catch一哈,敲句:throw new RuntimeException(e);就完事了。甚至这都嫌麻烦,搞个Lombok,加个@SneakyThrow注解,悄悄地就把啥可能出现的异常都给抛了。
结果Leader是带厂出来的,对代码质量有求道一般的执着,一审代码,怒斥此人简直胆大包天,居然敢如此敷衍草率。给我全部catch的干活。
最后10行功能代码,catch了5个异常,写了50行代码去处理,不但降低了开发效率,也导致程序可读性变差,并且代码质量几乎没有提高(只是稍微处理了一下那些偶然会出现或实际上完全不会出现的异常)。这个现象在一些大型项目中尤为严重。就比如之前的例子,很多时候,如果B抛出FileNotFoundException,很可能A也处理不了,要往调用栈的更上层去抛,一层层抛上去,到最后还是只能打在日志里,让程序员去debug,查查这里到底为什么会出现FileNotFoundException。
所以后来的语言都不再做异常检查,对异常处理不做任何强制要求。比如java的小弟kotlin、ruby,以及java的模仿者C#,他们都舍弃了java的这一机制。
kotlin的官方文档中对这一抉择做了详细解释