[外文翻译]《Effective Java》在哪些方面影响了 Kotlin 的设计
Java是伟大的编程语言无疑,但它也有一些众所周知的缺陷,比如那些常见的坑和从早期继承下来的不太重要的东西(Java 1.0发布于1995年)。 Joshua Bloch写了一本颇受推崇的书叫《Effective Java》,内容是关于如何写出好的Java代码,同时避免常见的编码错误及如何应对Java的不足。它有78个章节,称为“条目”,从多个方面为读者提供关于Java编程的宝贵建议。
现代编程语言的创造者有很大的优势,因为他们能够分析现有语言的缺点,并在设计语言的时候尽量避免。Jetbrains是一家开发了几款非常受欢迎的IDE的公司,于2010年决定为自己的开发工作创造一种编程语言——Kotlin。它的目标是更简洁、更有表现力,同时避免Java的一些不足。这家公司之前发布的所有IDE都是用Java编写的,所以他们需要一种与Java高度互操作的语言,并能够编译成Java字节码。他们还希望Java开发人员可以轻松切换到Kotlin. 也就是说,Jetbrains希望构建一个更好的Java。
在重读《Effective Java》时,我发现其中的很多内容对Kotlin来说已经用不着了,所以产生了一个想法,想探讨一下这本书是否影响了Kotlin的设计。
1. Kotlin 的默认值不再需要builder
当Java构造函数有很多可选参数时,代码将变得冗长,可读性差且容易出错。针对这个问题,Effective Java的条目2讲述了如何有效地使用构造器模式(Builder Pattern)。构建这样的对象需要写很多代码,如下面的代码示例中的“营养学”对象。它有两个必需的参数(serveSize,servings)和四个可选参数(calories, fat, sodium, carbohydrates):
public class JavaNutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories = 0; private int fat = 0; private int carbohydrate = 0; private int sodium = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public JavaNutritionFacts build() { return new JavaNutritionFacts(this); } } private JavaNutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } }
要用Java实例化一个对象,就得这样:
final JavaNutritionFacts cocaCola = new JavaNutritionFacts.Builder(240,8) .calories(100) .sodium(35) .carbohydrate(27) .build();
而在Kotlin中,你不再需要使用构造器模式,因为它有默认参数的功能,允许你为每个可选的构造函数参数定义默认值:
class KotlinNutritionFacts( private val servingSize: Int, private val servings: Int, private val calories: Int = 0, private val fat: Int = 0, private val sodium: Int = 0, private val carbohydrates: Int = 0)
所以在Kotlin中创建对象就可以这样:
val cocaCola = KotlinNutritionFacts(240,8, calories = 100, sodium = 35, carbohydrates = 27)
如果想让可读性更强,你也可以把必需的参数命名为 servingSize 和 servings
val cocaCola = KotlinNutritionFacts( servingSize = 240, servings = 8, calories = 100, sodium = 35, carbohydrates = 27)
跟Java一样,这里创建的对象是不可变的。
我们将Java的47行代码减少到了Kotlin的7行,大大提高了生产力。
温馨提示:如果想用Java创建这样的 KotlinNutrition 对象当然也是可以做到的,但你得为每个可选参数设定一个值。还好,只要加上 JvmOverloads 注解,那么就会自动生成多个构造器,使用注解时需要 constructor关键字:
class KotlinNutritionFacts @JvmOverloads constructor( private val servingSize: Int, private val servings: Int, private val calories: Int = 0, private val fat: Int = 0, private val sodium: Int = 0, private val carbohydrates: Int = 0)
2. 创建单例(singleton)很容易
Effective Java 的条目3说了如何设计一个单例Java对象,也就是只能实例化一个实例的对象。下面的代码片段展示了一个“单向”的宇宙,其中只能存在一个猫王:
public class JavaElvis { private static JavaElvis instance; private JavaElvis() {} public static JavaElvis getInstance() { if (instance == null) { instance = new JavaElvis(); } return instance; } public void leaveTheBuilding() { } }
Kotlin 有“对象声明”的概念,可以方便的通过对象声明来获得一个单例。
object KotlinElvis { fun leaveTheBuilding() {} }
再也不用费劲构造单例了。
3. 开箱即用的 equals() 和 hashCode()
良好的编程实践起源于功能编程,简化代码主要靠使用不可变值对象。条目15建议“类应该是不可变的,除非有足够的理由将它们设为可变”。创建不可变的值对象在Java中非常繁琐,因为你必须为每个对象重写equals()和hashCode()。Joshua Bloch在条目8和9用了足足18页描述了关于这两种方法的准则。例如,如果你重写equals(),你必须保证自反性、对称性、传递性、一致性和无效性,听起来不像在编程而更像数学。
在Kotlin中,这种情况下你可以直接使用数据类,编译器会自动导出equals(),hashCode()等方法。这是因为标准方法可以从对象的属性中直接派生出来,只需在类前面输入关键字数据即可,完全不需要18页的描述。
提示:最近,Java的AutoValue很流行,该库可为Java 1.6+生成不可变值类。
4. 属性(properties)取代域(fields)
public class JavaPerson { // don't use public fields in public classes! public String name; public Integer age; }
条目14建议在公有类中使用访问器方法而不是公有字段。如果您不这么做的话可能会遇到麻烦,因为域可以直接访问,导致完全享受不到封装好处。这意味着日后你将无法在不改动其公共API的情况下更改该类的内部表达。比如,后面你就不能再去限制某个字段的值,例如人的年龄。这就是为什么我们总是在Java中创建这些冗长的默认getter和setter的原因之一。
而Kotlin直接用自动生成默认getter和setter的属性取代了字段/域。
class KotlinPerson { var name: String? = null var age: Int? = null }
从语法上来说,你可以使用person.name 或者 person.age访问Java中的公共字段等属性。之后也可以添加自定义的getter和setter而无需更改类的API:
class KotlinPerson { var name: String? = null var age: Int? = null set(value) { if (value in 0..120){ field = value } else{ throw IllegalArgumentException() } } }
长话短说:有了Kotlin的属性,我们的类将更简洁,同时还具有与生俱来的灵活性。
5. override成为强制关键字而不是可选注解
Java 1.5 中加入了注解(annotation),其中最重要的一个是重写(override),表示这个方法是对超类中该方法的重写。基于书中条目36,应该尽量使用这个可选注解以避免一些恶心的bug。比如当你以为你重写了超类的方法但其实并没有时,编译器会抛出一个错误。不过如果你记得加上了override注解的话就没事。
在Kotlin中,override不是可选的注解而是强制关键字。所以由此引发的bug就不会再有了,编译器会提前警告你。
6. 默认的 final 类
《Effective Java》在第17条说,要么为继承而设计,并提供文档说明,要么就禁止继承。在Java中,除非将类显式指定为final,否则每个类都可以被继承。如果你忘记把类指定为final,也没有好好为继承而设计,那么当客户创建子类并覆盖某些方法时,很可能功能会出问题。
在Kotlin,所有类默认都是final的。如果要允许继承,则必须明确使用关键字open,这与Java的final完全相反。这样可以避免创建并非有意设计继承的非final类。
Kotlin社区有人对这个 “默认的final” 设计很不满。Kotlin论坛对此进行了激烈的讨论。后来,在Kotlin 1.1 beta版中提供了一个编译器插件,可以让class默认是open.
7. 没有检查型异常(checked exceptions)
Java有一个广为人知的特性,即检查型异常,编译器会强制函数的调用者捕获(或重新抛出)异常,这个功能总是容易出问题。 《Effective Java》花了一整个章节来阐述如何正确的使用和处理检查型和非检查型(即运行时)异常。
如Kotlin的文档中所述,检查型异常的一个问题是你有时必须捕获永远不会发生的异常,这将导致空的catch块和冗余代码。而且开发人员经常在被迫处理异常感到麻烦而直接忽略它们,这也会导致空的catch块。第65项说“不要忽视异常”。
根据第59条,检查型异常往往是不必要的,而且这应该通过在调用对象之前检查对象的状态,或者通过判断返回值(比如null)来避免。
我还发现了检查型异常的其它问题:
- throws子句把实现细节加入接口,这种做法不好;
- 版本化可能有问题。如果你修改了类的实现并向函数中添加了一个throws子句,那么API就发生了变化;
- 调用函数不应该规定调用者如何处理异常;
由于存在大量潜在问题,Kotlin等优秀编程语言(如C#)没有检查型异常。为了让调用者知道可能发生的异常,应该用 throws 标签在函数的文档中定义它们。
8. 强制的 null 检查
在Java中,public方法的方法签名不会告诉你返回值是否为空。例如:
public List<Item> getSelectedItems()
如果一条都没选会怎么样?这个方法是否返回null?还是返回空列表?如果不看方法的实现细节,我们就无法知道(除非这个方法有很好的javadoc描述了返回类型)。
��种情况下,开发人员可能会犯的两个错误是:
1.忘记检查返回值是否为空,导致著名的NullPointerException;
2.在返回值永远不可能为空的情况下检查了其是否为空,造成代码冗余。
Joshua Bloch 在第43条建议,用返回一个空的集合数组来取代返回null。这一条让我想到了可空和不可空类型。有了Kotlin的空安全性(null safety),你将知道返回值是否为空。
举个例子:一个返回类型List <Item>? 意味着它可以为null,而List <Item>则表示不能为null。如果它可以为空,编译器就会强制我们在访问其属性或调用其函数之前检查它是否为null。所以,更为强大的编译器将阻止开发者犯错误,生活突然变得容易了。
9. 没有原始类型(Raw types)
Java 1.5中引入了泛型,它们是实现类型安全的好方法。而为了向后兼容性,仍然可以使用原始类型,但Joshua Bloch在第23条中建议使用泛型(List <Integer>而不是List)以避免ClassCastExceptions。Kotlin不允许使用原始类型,因此必须为泛型指定类型参数,从而实现代码的类型安全。
总结
以上就是我认为《Effective Java》这本书影响了 Kotlin 设计的几处地方,肯定有遗漏,如果你有其它意见和建议,欢迎讨论。
原文:How “Effective Java” may have influenced the design of Kotlin
翻译:技术风向标