Kotlin中的Checked Exception机制浅析

前言

现在使用Kotlin的Android开发者已经越来越多了。

这门语言从一开始的无人问津,到后来成为Android开发的一级语言,再到后来Google官宣的Kotlin First。Kotlin正在被越来越多的开发者接受和认可。

许多学习Kotlin的开发者之前都是学习过Java的,并且本身Kotlin就是一款基于JVM语言,因此不可避免地需要经常和Java进行比较。

Kotlin的诸多特性,在熟悉Java的开发者看来,有些人很喜欢,有些人不喜欢。但即使是不喜欢的那些人,一旦用熟了Kotlin进行程序开发之后,也难逃真香定律。

今天我想跟大家聊一聊的话题,是Kotlin在早期的时候争议比较大的一个特性:Checked Exception机制。

由于Kotlin取消了Checked Exception,这在很多Java开发者看来是完全不可接受的,可能也是许多Java支持者拒绝使用Kotlin的原因。但目前Kotlin已经被Google转正两年多了,开发了成千上万的Android应用。你会发现,即使没有Checked Exception,Kotlin编写出的程序也并没有出现比Java更多的问题,因此编程语言中对于Checked Exception的必要性可能并没有许多人想象中的那么高。

当然,本篇文章中我并不能给出一个结论来证明谁对谁错,更多的是跟大家谈一谈我自己的观点和个人心得,另外引用一些大佬的权威观点。

另外,这个问题永远是没有正确答案的,因为世界上没有最好的编程语言(PHP除外)。每个编程语言选择不同的处理方式都有着自己的一套理论和逻辑,所以与其去争论Java中的Checked Exception机制是不是多余的,不如去论证Kotlin中没有Checked Exception机制为什么是合理的。

那么,我们首先从什么是Checked Exception开始说起。

什么是Checked Exception?

Checked Exception,简称CE。它是编程语言为了保证程序能够更好的处理和捕获异常而引入的一种机制。

具体而言,就是当一个方法调用了另外一个可能会抛出异常的接口时,要么将这个异常进行捕获,要么将这个异常抛出,交给上一层进行捕获。

熟悉Java语言的朋友对这一机制一定不会陌生,因为我们几乎每天都在这个机制的影响下编写程序。

观察如下代码:

public void readFromFile(File file) {
 FileInputStream in = null;
 BufferedReader reader = null;
 StringBuilder content = new StringBuilder();
 try {
  in = new FileInputStream(file);
  reader = new BufferedReader(new InputStreamReader(in));
  String line = "";
  while ((line = reader.readLine()) != null) {
   content.append(line);
  }
 } catch (IOException e) {
  e.printStackTrace();
 } finally {
  if (reader != null) {
   try {
    reader.close();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }
}

这段代码每位Java程序员应该都非常熟悉,这是一段Java文件流操作的代码。

我们在进行文件流操作时有各种各样潜在的异常可能会发生,因此这些异常必须被捕获或者抛出,否则程序将无法编译通过,这就是Java的Checked Exception机制。

有了Checked Exception,就可以保证我们的程序不会存在一些隐藏很深的潜在异常,不然的话,这些异常会像定时炸弹一样,随时可能会引爆我们的程序。

由此看来,Checked Exception是一种非常有必要的机制。

为什么Kotlin中没有CE?

Kotlin中是没有Checked Exception机制的,这意味着我们使用Kotlin进行上述文件流操作时,即使不捕获或者抛出异常,也可以正常编译通过。

熟悉Java的开发者们是不是觉得这样严重没有安全感?

那么我们就来尝试分析和思考一下,为什么Kotlin中没有Checked Exception。

我在学习Kotlin时,发现这门语言在很多设计方面都参考了一些业内的最佳编程实践。

举个例子,《Effective Java》这本书中有提到过,如果一个类并非是专门为继承而设计的,那么我们就应该将它声明成final,使其不可被继承。

而在Kotlin当中,一个类默认就是不可被继承的,除非我们主动将它声明成open。

类似的例子还有很多很多。

因此,Kotlin取消Checked Exception也肯定不是随随便便拍脑瓜决定的,而是有很多的理论依据为其支持。

比如说,《Thinking in Java》的作者 Bruce Eckel就曾经公开表示,Java语言中的Checked Exception是一个错误的决定,Java应该移除它。C#之父Anders Hejlsberg也认同这个观点,因此C#中是没有Checked Exception的。

那么我们大多数Java开发者都认为非常有必要的Checked Exception机制到底存在什么问题呢?

这些大佬们例举了很多方面的原因,但是我个人认为最主要的原因其实就是一个:麻烦。

Checked Exception机制虽然提升了编程语言的安全性,但是有时却让我们在书写代码时相当抓狂。

由于Checked Exception机制的存在,对于一些可能发生潜在异常的代码,我们必须要对其进行处理才行。处理方式只有两种:要么使用try catch代码块将异常捕获住,要么使用throws关键字将异常抛出。

以刚才的文件流操作举例,我们使用了两次try catch代码块来进行潜在的异常捕获,但其实更多只是为了能让编译器满意:

public void readFromFile(File file) {
 BufferedReader reader = null;
 try {
  ...
 } catch (IOException e) {
  e.printStackTrace();
 } finally {
  if (reader != null) {
   try {
    reader.close();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }
}

这段代码在Java当中是最标准和规范的写法,然而你会发现,我们几乎没有人能在catch中写出什么有意义的逻辑处理,通常都只是打印一下异常信息,告知流发生异常了。那么流发生异常应该怎么办呢?没人知道应该怎么办,理论上流应该总是能正常工作的。

思考一下,是不是你在close文件流时所加的try catch都只是为了能够让编译通过而已?你有在close的异常捕获中进行过什么有意义的逻辑处理吗?

而Checked Exception机制的存在强制要求我们对这些未捕获的异常进行处理,即使我们明确不想对它进行处理都不可以。

这种机制的设计思路本身是好的,但是却也间接造就了很多填鸭式的代码,只是为了满足编译器去编程,导致编写了很多无意义的try catch语句,让项目代码看来得变得更加臃肿。

那么如果我们选择不对异常进行捕获,而是将异常向上抛出呢?事实证明,这可能也并不是什么特别好的主意。

绝大多数Java程序员应该都使用过反射的API,编写反射代码时有一点特别讨厌,就是它的API会抛出一大堆的异常:

Object reflect(Object object, String className, String methodName, Object[] parameters, Class<?>[] parameterTypes)
	  throws SecurityException, IllegalArgumentException,
		IllegalAccessException, InvocationTargetException,
		NoSuchMethodException, ClassNotFoundException {
	Class<?> objectClass = Class.forName(className);
	Method method = objectClass.getMethod(methodName, parameterTypes);
	return method.invoke(object, parameters);
}

这里我只是编写了一段最简单的反射代码,竟然有6个异常要等着我去处理。其中每个异常代表什么意思我也没能完全搞明白,与其我自己去写一大堆的try catch代码,还不如直接将所有异常都抛出到上一层得了,这样代码看起来还能清爽一点。

你是这么想的,上一层的人也是这么想的,更过分的是,他可能还会在你抛出异常的基础之上,再增加一点其他的异常继续往上抛出。

根据我查阅到的资料,有些项目经过这样的层层累加之后,调用一个接口甚至需要捕获80多个异常。想必调用这个接口的人心里一定在骂娘吧。你觉得在这种情况下,他还能耐心地对每一种异常类型都细心进行处理吗?绝对不可能,大概率可能他只会catch一个顶层的Exception,把所有异常都囊括进去,从而彻底地让Checked Exception机制失去意义。又或者,他可能会在当前异常抛出链上再加一把火,为抛出100个异常做出贡献。。。

最终我们可以看出,Java的Checked Exception机制,本身的设计初衷确实是好的,而且是先进的,但是却对程序员有着较高的编码规范要求。每一层方法的设计者都应该能清楚地辨别哪些异常是应该自己内部捕获的,哪些异常是应该向上抛出的,从而让整个方法调用栈的异常链都在一个合理和可控的范围内。

然而比较遗憾的现实是,绝大多数的程序员其实都是做不到这一点的,滥用和惰性使用CE机制的情况广泛存在,完全达不到Java本身设计这个机制所预期的效果,这也是Kotlin取消Checked Exception的原因。

没有CE不会出现问题吗?

许多Java程序员会比较担心这一点,Kotlin取消了Checked Exception机制,这样不会导致我的程序变得很危险吗?每当我调用一个方法时,都完全不知道这个方法可能会抛出什么异常。

首先这个问题在开头已经给出了答案,经过两年多的实践发现,即使没有Checked Exception,Kotlin开发出的程序也并没有比Java开发的程序出现更多的异常。恰恰相反,Kotlin程序反倒是减少了很多异常,因为Kotlin增加了编译期处理空指针异常的功能(空指针在各类语言的崩溃率排行榜中都一直排在第一位)。

那么至于为什么取消Checked Exception并不会成为导致程序出现更多异常的原因,我想分成以下几个点讨论。

第一,Kotlin并没有阻止你去捕获潜在的异常,只是不强制要求你去捕获而已。

经验丰富的程序员在编写程序时,哪些地方最有可能发生异常其实大多是心中有数的。比如我正在编写网络请求代码,由于网络存在不稳定性,请求失败是极有可能发生的事情,所以即使没有Checked Exception,大多数程序员也都知道应该在这里加上一个try catch,防止因为网络请求失败导致程序崩溃。

另外,当你不确定调用一个方法会不会有潜在的异常抛出时,你永远可以通过打开这个方法,观察它的抛出声明来进行确定。不管你有没有这个类的源码都可以看到它的每个方法抛出了哪些异常:

public class FileInputStream extends InputStream {

  public FileInputStream(File file) throws FileNotFoundException {
    throw new RuntimeException("Stub!");
  }

  public int read(byte[] b, int off, int len) throws IOException {
    throw new RuntimeException("Stub!");
  }

  public void close() throws IOException {
    throw new RuntimeException("Stub!");
  }
  ...
}

然后当你觉得需要对这个异常进行捕获时,再对它进行捕获即可,相当于你仍然可以按照之前在Java中捕获异常的方式去编写Kotlin代码,只是没有了强制的要求,你可以自由选择要不要进行捕获和抛出。

第二,绝大多数的方法其实都是没有抛出异常的。

这是一个事实,不然你绝对不会爱上Checked Exception机制,而是会天天咒骂它。

试想一下,假如你编写的每一行代码,调用的每一个方法,都必须要对它try catch捕获一下才行,你是不是想摔键盘的心都有了?

我说的这种情况在Java中真的有一个非常典型的例子,就是Thread.sleep()方法。由于Thread.sleep()方法会抛出一个InterruptedException,所以每次我们调用这个方法时,都必须要用try catch捕获一下:

public class Main {

  public void test() {
    // do something before
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    // do something after
  }

}

这也是我极其不喜欢这个方法的原因,用起来就是一个字:烦。

事实上,可能绝大多数Java程序员甚至都不知道为什么要捕获这个异常,只知道编译器提醒我必须捕获。

之所以我们在调用Thread.sleep()方法时需要捕获InterruptedException,是因为如果在当前线程睡眠的过程中,我们在另外一个线程对中这个睡眠中的线程进行中断(调用thrad.interrupt()方法),那么sleep()方法会结束休眠,并抛出一个InterruptedException。这种操作是非常少见的,但是由于Checked Exception的存在,我们每个人都需要为这一个少见的操作买单:即每次调用Thread.sleep()方法时,都要写一段长长的try catch代码。

而到了Kotlin当中,你会不再讨厌使用Thread.sleep()方法,因为没有了Checked Exception,代码也变得清爽了:

class Main {

  fun test() {
    // do something before
    Thread.sleep(1000)
    // do something after
  }

}

第三,拥有Checked Exception的Java也并不是那么安全。

有些人认为,Java中拥有Checked Exception机制,调用的每个方法你都会感到放心,因为知道它会抛出什么异常。而没有Checked Exception的话,调用任何方法心里都感觉没底。

那么这种说法有道理吗?显然这不是真的。不然,你的Java程序应该永远都不会崩溃才对。

事实上,Java将所有的异常类型分成了两类:受检查异常和不受检查异常。只有受检查异常才会受到Checked Exception机制的约束,不受检查异常是不会强制要求你对异常进行捕获或抛出的。

比如说,像NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException这些都是不受检查的异常,所以你调用的方法中即使存在空指针、数组越界等异常风险,Checked Exception机制也并不会要求你进行捕获或抛出。

由此可见,即使Java拥有Checked Exception机制,也并不能向你保证你调用的每个方法都是安全的,而且我认为空指针和数组越界等异常要远比InterruptedException之类的异常更加常见,但Java并没有对此进行保护。

至于Java是如何划分哪些异常属于受检查异常,哪些属于不受检查异常,这个我也不太清楚。Java的设计团队一定有自己的一套理论依据,只不过这套理论依据看上去并没有被其他语言的设计者所认可。

因此,你大概可以理解成,Kotlin就是把异常类型进一步进行了简化,将所有异常都归为了不受检查异常,仅此而已。

结论

所以,最终的结论是什么呢?

很遗憾,没有结论。正如任何事物都有其多样性一样,关于Checked Exception这个问题上面,也没有一个统一的定论。

Java拥有Checked Exception机制并不是错误的,Kotlin中取消Checked Exception机制也不是错误的。我想这大概就是你阅读完本文之后能够得出的结论吧。

但是,希望你自此往后,在使用Kotlin编程程序时,不要再为有没有Checked Exception的问题所纠结了。

总结

到此这篇关于Kotlin中的Checked Exception机制浅析的文章就介绍到这了,更多相关Kotlin的Checked Exception机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2020-10-01

详解Java中Checked Exception与Runtime Exception 的区别

详解Java中Checked Exception与Runtime Exception 的区别 Java里有个很重要的特色是Exception ,也就是说允许程序产生例外状况.而在学Java 的时候,我们也只知道Exception 的写法,却未必真能了解不同种类的Exception 的区别. 首先,您应该知道的是Java 提供了两种Exception 的模式,一种是执行的时候所产生的Exception (Runtime Exception),另外一种则是受控制的Exception (Checked

Kotlin中的反射机制深入讲解

前言 Java中的反射机制,使得我们可以在运行期获取Java类的字节码文件中的构造函数,成员变量,成员函数等信息.这一特性使得反射机制被常常用在框架中,想要比较系统的了解Kotlin中的反射,先从Java的反射说起. Java中的反射 通常我们写好的.java源码文件,经过javac的编译,最终生成了.class字节码文件.这些字节码文件是与平台无关的,使用时通过Classloader去加载这些.class字节码文件,从而让程序按照我们编写好的业务逻辑运行.Java的反射主要是从这些.class

C++11中std::declval的实现机制浅析

本文主要给大家介绍了关于C++11中std::declval实现机制的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 在vs2013中,declval定义如下 template <_Ty> typenamea dd_rvalue_reference<_Ty>::type declval() _noexcept; 其中,add_rvalue_reference为一个traits,定义为 template <_Ty> struct add_rvalue_ref

浅析Python3中的对象垃圾收集机制

###概述 GC作为现代编程语言的自动内存管理机制,专注于两件事:1. 找到内存中无用的垃圾资源 2. 清除这些垃圾并把内存让出来给其他对象使用. 在Python中,它在每个对象中保持了一个计数器,用于记录指向该对象的的引用的个数.一旦这个计数器为0时,则立即回收该对象,对象占用的内存空间将被释放. 引用计数 我们可以利用简单的变量引用和销毁窥见引用计数过程. 增加引用计数 增加引用计数的方式多种,即对象进行引用,那么计数器都会+1 # 创建第一个引用 a = 3 # 用其他变量名引用 b =

Kotlin中单例模式和Java的对比浅析

前言 单例模式,一直以来是我们在日常开发中最常用的一种设计模式,更是面试中非常重要,也非常容易被问到的问题.在日常开发中,大家常用的语言还是Java,但今天我给大家带来的是在Kotlin语言中,单例模式是怎么编写的,并且会对比Java方式,下面话不多说了,来一起看看详细的介绍吧 一.懒人写法(恶汉式) java中 public class Singleton{ public static final Singleton instance = new Singleton(); public Sin

详解Java中的checked异常和unchecked异常区别

(一)Java的异常层次结构 要想明白Java中checked Exception和unchecked Exception的区别,我们首先来看一下Java的异常层次结构. 这是一个简化的Java异常层次结构示意图,需要注意的是所有的类都是从Throwable继承而来,下一层则分为两个结构,Error和Exception.其中Error类层次描述了Java运行时系统的内部错误和资源耗尽错误,这种错误除了简单的报告给用户,并尽力阻止程序安全终止之外,一般也米有别的解决办法了. (二)unchecke

深入分析javascript中的错误处理机制

前面的话 错误处理对于web应用程序开发至关重要,不能提前预测到可能发生的错误,不能提前采取恢复策略,可能导致较差的用户体验.由于任何javascript错误都可能导致网页无法使用,因此作为开发人员,必须要知道何时可能出错,为什么会出错,以及会出什么错.本文将详细介绍javascript中的错误处理机制 error对象 error对象是包含错误信息的对象,是javascript的原生对象.当代码解析或运行时发生错误,javascript引擎就会自动产生并抛出一个error对象的实例,然后整个程序

全面了解javascript中的错误处理机制

前面的话 错误处理对于web应用程序开发至关重要,不能提前预测到可能发生的错误,不能提前采取恢复策略,可能导致较差的用户体验.由于任何javascript错误都可能导致网页无法使用,因此作为开发人员,必须要知道何时可能出错,为什么会出错,以及会出什么错.本文将详细介绍javascript中的错误处理机制 error对象 error对象是包含错误信息的对象,是javascript的原生对象.当代码解析或运行时发生错误,javascript引擎就会自动产生并抛出一个error对象的实例,然后整个程序

Java不可变类机制浅析

不可变类(Immutable Class):所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值.如JDK内部自带的很多不可变类:Interger.Long和String等. 可变类(Mutable Class):相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类. 不可变类的特性对JAVA来说带来怎样的好处? 1)线程安全:不可变对象是线程安全的,在线程之间可以相互共享,不需要利用特殊机制来保证同步问题,因为对象的值无法改变.可以降低并发错误

URL中“#” “?” &“”号的作用浅析

1. # 10年9月,twitter改版.一个显著变化,就是URL加入了"#!"符号.比如,改版前的用户主页网址为http://twitter.com/username改版后,就变成了http://twitter.com/#!/username 这是主流网站第一次将"#"大规模用于重要URL中.这表明井号(Hash)的作用正在被重新认识.本文根据HttpWatch的文章,整理与井号有关的所有重要知识点. 一.#的涵义 #代表网页中的一个位置.其右面的字符,就是该位置