7b82330b5514b2edab51b6a38053eed4.gif

作者 | Nicolas Fränkel

译者 | 弯月

出品 | CSDN(ID:CSDNnews)

我使用Java已近二十年了。几年前,我开始学习Kotlin。

虽然Kotlin也会编译成JVM字节码,但有时候我还是要写Java。每当这时,我就会想,为什么Java代码不能像Kotlin那样漂亮。Java缺少一些关键特性,因此代码的可读性、表达性和可维护性都差强人意。

这篇文章并不是要攻击Java,只是列出了一些我希望Java拥有的功能。

a84642f6c3444b0c914974218be05dd7.png

a5d796866bd58f6f80929ef5b82cac0b.png

不可变引用

Java有不可变引用:

  • 类的属性

  • 方法的参数

  • 局部变量

class Foo {
    final Object bar = new Object();       ①
    void baz(final Object qux) {          ②
final var corge = new Object();   ③
}
}

① 不能给bar重新赋值

② 不能给qux重新赋值

③ 不能给corge重新赋值

不可变引用非常有利于避免尴尬的bug。有意思的是,final关键字并没有被广泛使用,即使是广为人知的项目也并没有使用太多final。例如,Spring的GenericBean使用了不可变属性,但没有使用不可变方法参数,也没有使用不可变局部变量;slf4j的DefaultLoggingEventBuilder没有使用上述任何一种。

尽管Java允许定义不可变引用,但并没有强制要求。默认情况下,引用是可变的。大部分Java代码都没有采用不可变引用。

而Kotlin并没有给你选择:每个属性和局部变量都要定义为val或var。而且,方法参数不能重新赋值。

Java的var关键字则有很大的不同。首先,它只能用于局部变量。更重要的是,Java并没有相应的不可变关键字val。你依然需要使用final关键字,而很少有人这么做。

635b42216ba6b60804b4eb9e46c286a4.png

Null安全性

在Java中,没有办法知道某个变量是否为null。为了明确这一点,Java 8引入了Optional类型。从Java 8以后,返回一个Optional意味着底层的值可能为null,而返回其他类型意味着不可能为null。

但是,Optional的开发者只用null作为返回值。而方法参数和返回值在Null安全性方面并没有得到语法层面上的支持。为了解决这个问题,许多库提供了编译时注释:

ef65e0ca96e438e921b79cc7ece6f438.png

显然,一些库只能用于特定的IDE。更糟糕的是,这些库之间很难相互兼容。所以很多人都在Stack Overflow上问,这么多的库应该使用哪个。

最后,开发者必须主动使用支持Null安全性的库。相反,Kotlin要求每个类型都必须是允许null或不允许null。

val nonNullable: String = computeNonNullableString()
val nullable: String? = computeNullableString()

52bf8ae937a39ce9c91c09707014b2ae.png

扩展函数

在Java中,扩展类的方法是编写子类:

class Foo {}
class Bar extends Bar {}

子类有两个主要问题。首先,标记了final的类不允许继承。许多广泛应用的JDK类都是final的,比如String。其次,如果一个不属于方法返回了某个类型,那么就只能返回那个类型,不论其行为是否符合你的要求。

为了解决这个问题,Java开发者发明了工具类的概念,例如类型XYZ的工具类通常写作XYZUtils。工具类就是一堆static方法的集合,并且构造函数是private的,因此无法创建示例。这就相当于一个命名空间,因为Java不允许在类外创建方法。

这样,如果一个类型不包含某个方法,那么工具类可以提供该方法,接受类型作为一个参数,并执行指定的行为。

class StringUtils {                                          ①
    private StringUtils() {}                                 ②
    static String capitalize(String string) {                ③
return string.substring(0, 1).toUpperCase()
+ string.substring(1);                           ④
}
}
String string = randomString();                              ⑤
String capitalizedString = StringUtils.capitalize(string); ⑥

① 工具类
② 防止工具类实例化
③ static方法
④ 一个简单的大写函数,没有考虑边界情况
⑤ String类型没有提供大写功能
⑥ 使用工具类来实现该功能

而Kotlin提供了扩展函数功能来解决这个问题。

Kotlin提供了一种方法,可以扩展类或接口,而无需从类进行集成,也无需使用诸如修饰器等设计模式。只需通过一种叫做“扩展”的特殊定义来实现。

例如,你可以给一个来自第三方库的类或接口编写新的函数,即使你无法修改该库。这种函数可以正常调用,就像它本来就属于该类一样。这种机制叫做“函数扩展”。

要定义函数扩展,只需在其名称前加上一个接收者类型,指示被扩展的类。

有了函数扩展,上述代码就可以写成:

fun String.capitalize2(): String {                            ①②
return substring(0, 1).uppercase() + substring(1);
}
val string = randomString()
val capitalizedString = string.capitalize2() ③

① 孤立的函数,不需要类封装。

② Kotlin的stdlib已经有了capitalize()函数。

③ 就像调用String自带的函数一样调用扩展函数。

注意扩展函数会被“静态地”解析。扩展函数并不会给已有类型添加新的行为,只是假装而已。它们生成的字节码非常类似于Java的静态方法。但是其语法要简洁得多,而且支持函数链式调用,这在Java中时无法做到的。

27bc6b75f0766fc131718d49030349dc.png

真实泛型

Java版本5加入了泛型支持。但是,语言设计师太执着于向下兼容性,Java 5的字节码必须能与Java 5之前的字节码完全兼容。这就是为什么生成的字节码中不包含泛型的原因。这种方式称为“泛型擦除”。与之相对的叫做“真实泛型”(reified generics),即泛型会出现在字节码中。

仅在编译期间采用泛型,会导致一系列问题。例如,下面的方法签名会生成完全相同的字节码,因此这段代码是不正确的:

class Bag {
int compute(List<Foo> persons) {}
int compute(List<Bar> persons) {}
}

另一个问题是如何从值的容器中获取有类型的值。下面是Spring中的一个例子:

org/springframework/beans/factory/BeanFactory.java
public interface BeanFactory {
<T> T getBean(Class<T> requiredType);
}

开发者添加了一个 Class<T> ,以便在方法体中获知类型。如果Java有真实泛型,只需像下面这样处理即可:

public interface BeanFactory {
<T> T getBean();
}

想象一下,如果Kotlin有真实泛型,我们可以改变上述设计:

interface BeanFactory {
fun <T> getBean(): T
}

函数调用可以改成:

val factory = getBeanFactory()
val anyBean = factory.getBean<Any>() ①

① 真实泛型!

Kotlin依然需要遵守JVM规范,生成与Java编译器的字节码兼容的字节码。但它可以通过“内联”的方式实现,即编译器用函数体替换内联函数调用。

下面是Kotlin代码:

org/springframework/beans/factory/BeanFactoryExtensions.kt
inline fun <reified T : Any> BeanFactory.getBean(): T = getBean(T::class.java)

f7dc804af7cbdf8effe50cec843a631b.png

总结

本文介绍了四个我希望Java也有的Kotlin功能:不可变引用、null安全性、扩展函数,以及真实泛型。Kotlin还有许多其他很好的功能,但这四个功能就足以提升Java。

例如,有了扩展函数和真是繁星,再加上一些语法糖,就可以很轻松地编写DSL,就像Kotlin Routes和Beans DSL一样:

beans {
bean {
router {
GET("/hello") { ServerResponse.ok().body("Hello world!") }
}
}
}

别误会:我知道Java作为一种语言,发展时需要考虑很多因素,而Kotlin的包袱更轻。但是,有竞争是好事,两者可以互相学习。

同时,我只在必要时才会编写Java,因为Kotlin已成为了我的JVM首选。

原文地址:https://blog.frankel.ch/miss-in-java-kotlin-developer/

 
 

Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐