[Kotlin号外]《Effective Java》在哪些方面影响了Kotlin的设计

Kotlin设计过程中大量参考了《Effective Java》这本书中的意见,作者从《Effective Java》中选出了几条项目,举例分析 Java 和 Kotlin 写法不同之处,Kotlin 从中得到启发,设计的更好。

本文译自:How “Effective Java” may have influenced the design of 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对象,也就是只能实例化一个实例的对象。下面的代码片段展示了一个独占的领域,其中只能存在一个Elvis:

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中,这种情况下你可以直接使用data类,编译器会自动导出equals(),hashCode()等方法。这是因为标准方法可以从对象的属性中直接派生出来,只需在类前面输入关键字data即可。完全不需要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就不会再有了,编译器会提前警告你。Kotlin把这些事清楚的展现出来。

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 getSelectedItems()

如果一条都没选会怎么样?这个方法是否返回null?还是返回空列表?如果不看方法的实现细节,我们就无法知道(除非这个方法有很好的javadoc描述了返回类型)。

这种情况下,开发人员可能会犯的两个错误是:

  1. 忘记检查返回值是否为空,导致著名的NullPointerException;

  2. 在返回值永远不可能为空的情况下检查了其是否为空,造成代码冗余。

Joshua Bloch 在第43条建议,用返回一个空的集合数组来取代返回null。这一条让我想到了可空和不可空类型。有了Kotlin的空安全性(null safety),你将知道返回值是否为空。

举个例子:一个返回类型List ? 意味着它可以为null,而List < Item >则表示不能为null。如果它可以为空,编译器就会强制我们在访问其属性或调用其函数之前检查它是否为null。所以,更为强大的编译器将阻止开发者犯错误,生活突然变得容易了。

9. 没有原生类型(Raw types)

Java 1.5中引入了泛型,它们是实现类型安全的好方法。而为了向后兼容性,仍然可以使用原始类型,但Joshua Bloch在第23条中建议使用泛型(List < Integer >而不是List)以避免ClassCastExceptions。Kotlin不允许使用原生类型,因此必须为泛型指定类型参数,从而实现代码的类型安全。

10 定义型变的简单方式

第28条认为Java中的界限通配符是晦涩难懂的,Joshua Bloch提出了PECS,来帮助你决定是使用< ? extends E > (协变) 还是< ? super E > (逆变),PECS代表:生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。Kotlin团队努力使型变处理更容易,他们去除了通配符,并提供关键字in用于协变,out用于逆变,这点就像C#一样。

abstract class Source {
    abstract fun nextT(): T
}

fun demo(strs: Source) {
    val objects: Source = strs // 这个没问题,因为 T 是一个 out-参数 !!!在 Java 中不允许
    // ……
}

简而言之,他们说类C是在参数T上是协变的,或者说 T 是一个协变的类型参数。你可以认为 C 是T的生产者,而不是T的消费者。

总结

以上就是我认为《Effective Java》这本书影响了 Kotlin 设计的几处地方,肯定有遗漏,如果你有其它意见和建议,欢迎讨论。

你可能感兴趣的:(kotlin)