简评:我们继续上一部分的讨论。
6. 默认 final 类
《Effective Java》第 17 条建议每个类如果没有必要都不应该是子类化的,或者至少应该是经过精心设计和注释,以支持继承。在 Java 中,每个类都能子类化除非你明确声明类是 final 类型。如果你忘记了声明 final, 并且没有做好设计和注释,那么当客户使用的时候就会有麻烦,他们会认为他们可以创建子类,重写一些方法,并且假定一切都能照常运行。
在 Kotlin 中,每个类默认都是 final 类型。你必须明确使用关键字 open(恰恰和 Java 相反)允许类能够被继承。这样可以避免不想被继承的类被允许继承。
在 Kotlin 社区并非每个人都乐于见到 默认 final 类的设计。关于这个争论话题在 Kotlin 专题讨论
中有一个热烈的讨论。
热点新闻:最近发布的 Kotlin 1.1 beta 版本提供了编译器插件使 open 成为默认关键字。
7. 不检查异常
Java 有一个被称为异常检查的功能,编译器强迫函数的调用者捕获(或者重新抛出)一个异常。
《Effective Java》有一整个章节讲解如何恰当地使用和处理异常。Kotlin 的文档中解释了一个检查异常的问题在于:有时候你必须捕获一些永远不会出现的异常。这导致了空的 catch 块和冗余的代码。更糟的是当开发者被迫对可能的异常作出反应时会觉得很恼火,导致他们很可能忽略异常,从而出现空的 catch 块。
《Effective Java》第 65 条建议“不要忽略异常”。
而根据《Effective Java》第 59 条的建议,检查异常通常是没必要的,可以通过在调用方法前检查对象的状态或者判断返回值来替代。
在我的研究期间,我发现了很多其他关于异常检查的问题:
- throws 会将实现细节添加到接口,这是不好的;
- 版本化可能有问题,如果你更改了你的实现,并且你的方法中添加了 throws,API 都会发生变化(因为异常也是 API 的一部分);
- 调用函数不应该规定调用者如何执行其异常处理。
因为有很多潜在的问题,Kotlin 和其他的一些编程语言,就没有异常检查。为了让调用者知道可能出现的异常,你应该在 kdoc 文档中使用 throws 标签来说明。
8. 强制空检查
在 Java 中,一个公共方法的方法签名不会告诉你返回值是否为空,例如:
public List<Item> getSelectedItems()
当没有项目被选中会发生什么?这个方法会返回 null 还是空列表?如果我们没有看到具体的实现就没办法确定(除非很幸运的,这个方法有充足的 javadoc 描述)。
有两个开发者可能会犯的错误:
- 当返回值可能为 null 时,忘记检查它是否为 null,导致著名的 NullPointerException 错误;
- 检查它是否为 null,尽管它永远不会为 null,这就导致了冗余的代码。
Joshua Bloch 在第 43 条中建议方法应该总是返回空的集合而不是 null。而在 Kotlin 中因为空值安全,你就知道什么时候返回值可能为 null 或者不可能为 null。
像上面的例子,在 Kotlin 中如果一个返回类型 List<Item>? 就意味着可能为 null,而 List<Item> 就意味着不会为 null。如果它可以为 null,编译器就会在我们访问它的属性或者调用它的函数时强迫我们检查它是否为 null,也预防了开发者可能出现的失误。
9. 没有原始类型
泛型在 Java 1.5 中被添加,它们是一个很好的保证类型安全的方式。Joshua Bloch 在第 23 项中建议使用通用类型(List <Integer> 而不是 List),这样可以避免 ClassCastExceptions。
在 Kotlin 中甚至干脆不允许使用原始类型了,所以你总是要声明泛型的类型参数,这会让你的代码更安全。
10. 更容易定义差异
《Effective Java》第 28 项中讨论了使用有界通配符,在 Java 中这非常的晦涩难懂。 Joshua Bloch 定义了 PECS 记忆单词(Producer extends, Consumer super) 帮助你决定什么时候使用 <? extends E>(协变)或者 <? super E>(逆变)。
而 Kotlin 团队努力使差异处理变得更容易,所以他们放弃了通配符,并且像 C#,为协变提供了关键字 in,为逆变提供了关键字 out。
这篇博文太短,无法详细解释这些关键字如何工作,但是如果你有兴趣的话可以看一下声明侧变量,类型投影或者泛型。
英文原文:How “Effective Java” may have influenced the design of Kotlin — Part 2