【本人秃顶程序员】从Java到Kotlin,然后又回到Java!

←←←←←←←←←←←← 快,点关注!

最近Java与kotlin语言之争又有点小热,大概是因为某位当初吹捧Java的大神来华兜售其kotlin新书有关,但是与此同时相反观点也是不断涌现,Allegro团队就在他们的博客发表这篇文章,从Java到Kotlin,然后又回到Java的"折腾"过程。

我们过去尝试Kotlin,但现在我们正在用Java 10重写。

我有我最喜欢的一组JVM语言:Java是主打,Groovy用于测试,这是表现最好的二人组合。2017年夏季,我的团队开始了一个新的微服务项目,和往常一样,我们谈论了语言和技术。在Allegro有几个已经采用了Kotlin的团队,我们想尝试新兴事物,所以我们决定这次试一下Kotlin。由于Kotlin没有对应的Spock框架,我们决定还是使用Groovy /test (Spek不如Spock)。在2018年采取Kotlin几个月后,我们总结了正反优缺点,并得出Kotlin反而使我们的生产力下降的结论。我们开始用Java将这个微服务重写。

这是为什么呢?

从以下几个方面谈原因:

Name shadowing名称遮蔽

这是Kotlin让产生最大惊喜的地方。看看这个函数:

fun inc(num : Int) {
    val num = 2
    if (num > 0) {
        val num = 3
    }
    println ("num: " + num)
}

当你调用inc(1)会输出什么呢?在Kotlin中,方法参数是不变的值,所以你不能改变num这个方法参数。这是很好的语言设计,因为你不应该改变方法输入参数。但是你可以用相同的名称定义另一个变量名并将其初始化为任何你想要的。现在在这个方法作用域中您有两个num命名的变量。当然,你一次只能访问其中一个num,但是num值是会被改变的。怎么办?

在if正文中,又再添加另一个num,这不太令人震惊(作用域是在块内)。

好的,在Kotlin中,inc(1)输出打印数字 “2”。

Java中的等效代码如下,但是无法通过编译:

void inc(int num) {
    int num = 2; //error: variable 'num' is already defined in the scope
    if (num > 0) {
        int num = 3; //error: variable 'num' is already defined in the scope
    }
    System.out.println ("num: " + num);
}

名字遮蔽不是Kotlin发明的。这在编程语言中很常见。在Java中,我们习惯用方法参数来映射类字段:

public class Shadow {
    int val;

    public Shadow(int val) {
        this.val = val;
    }
}

在kotlin这种遮蔽却太过分了。当然,这是Kotlin团队的一个设计缺陷。IDEA团队试图通过对每个遮蔽变量显示简洁警告来解决此问题: 此名称被遮蔽Name shadowed。如果两个团队都在同一家公司工作,所以也许他们可以互相交流并就遮蔽问题达成共识。我的疑问是 - 如果IDEA这样做可行吗?我无法想象这种映射方法参数的方式会真的有效。

类型推断

在Kotlin中,当你声明一个var或val,你通常让编译器会从表达式右边开始猜测变量类型。我们称之为局部变量类型推断,这对程序员来说是一个很大的改进。它允许我们在不影响静态类型检查的情况下简化代码。

例如,下面这行Kotlin代码:

var a = "10"

将由Kotlin编译器翻译成:

var a : String = "10"

这也是Java的真正优势。我故意说是因为 - Java 10也已经有了这个功能,而且现在Java 10已经可以使用了。

Java 10中的类型推断:

var a = "10";

公平地说,我需要补充一点,Kotlin在这个领域仍然略胜一筹。您也可以在其他上下文中使用类型推断,例如,单行方法。

编译时空指针安全Null-safe

Null-safe类型是Kotlin的杀手级功能。这个想法很好。在Kotlin中,类型默认是不可为空的。如果您需要添加一个可为空的类型,可如下:

val a: String? = null // ok

val b: String = null // compilation error

如果您使用不带空值检查的可能为空的变量,Kotlin将不会编译,例如:

println (a.length) // compilation error

println (a?.length) // fine, prints null

println (a?.length ?: 0) // fine, prints 0

一旦你有这两种类型,不可为空T和可为空T?,你可以远离甚至忘记Java中最常见的空指针异常–NullPointerException。真的吗?不幸的是,事情并不这么简单。

当你的Kotlin代码必须与Java代码相处时,事情会变得很糟糕(库是用Java编写的,所以我猜这种情况经常会发生)。然后,第三种类型跳入 - T!。它被称为平台类型,那么它到底意味着T或T?呢。或者,如果我们想要精确,T!意味着T具有未定义的可空性。这种奇怪的类型不能在Kotlin中表示,它只能从Java类型推断出来。 T!却可能会误导你,因为它对空值放松并失效Kotlin的Null-safe。

考虑下面的Java方法:

public class Utils {
    static String format(String text) {
        return text.isEmpty() ? null : text;
    }
}

现在,你想要从Kotlin调用format(String) 。您应该使用哪种类型来使用此Java方法的结果呢?你有三个选择。

第一种方法。你可以使用String,代码看起来很安全,但会抛出NPE。

fun doSth(text: String) {
    val f: String = Utils.format(text)       // 会通过编译但是运行时会抛NPE
    println ("f.len : " + f.length)
}

你需要用Elvis来解决它:

fun doSth(text: String) {
    val f: String = Utils.format(text) ?: ""  // safe with Elvis
    println ("f.len : " + f.length)
}

第二种方法。你可以使用String?,然后你会null-safe:

fun doSth(text: String) {
    val f: String? = Utils.format(text)   // safe
    println ("f.len : " + f.length)       // compilation error, fine
    println ("f.len : " + f?.length)      // null-safe with ? operator
}

第三种方法,如果你只是让Kotlin做出神话般的局部变量类型推断呢?

fun doSth(text: String) {
    val f = Utils.format(text)            // f type inferred as String!
    println ("f.len : " + f.length)       // compiles but can throw NPE at runtime
}

这是馊主意。这个Kotlin代码看起来很安全,可以编译,但是允许空值,会抛空指针错误,就像在Java中一样。

!!还有一招。用它来强制推断f类型为String:

fun doSth(text: String) {

val f = Utils.format(text)!! // throws NPE when format() returns null

println ("f.len : " + f.length)

}

在我看来,Kotlin的所有这些Scala样型系统!,?以及!!过于复杂。为什么Kotlin从Java T推断到T!,而不是T?呢?与Java互操作性似乎反而损害了Kotlin的杀手功能 - 类型推断。看起来你应该为所有Kotlin会导入的Java变量显式声明类型为T?。

类字面量Class literals

使用类似Log4j或Gson的Java库时,类字面量很常见。

在Java中,我们使用.class后缀编写类名:

Gson gson = new GsonBuilder().registerTypeAdapter(LocalDate.class, new LocalDateAdapter()).create();

在Groovy中,类字面量被简化到极点。你可以忽略.class ,它是Groovy或Java类并不重要。

def gson = new GsonBuilder().registerTypeAdapter(LocalDate, new LocalDateAdapter()).create()

Kotlin区分Kotlin和Java类,并为其提供语法规范:

val kotlinClass : KClass = LocalDate::class
val javaClass : Class = LocalDate::class.java

所以在Kotlin,你不得不写下:

val gson = GsonBuilder().registerTypeAdapter(LocalDate::class.java, LocalDateAdapter()).create()

这是丑陋的。

反向类型声明

在C语言系列编程语言中,我们有标准的声明类型的方法。简而言之,首先进引入一个类型,然后输出一个类型的东西(变量,字段,方法等)。

Java中的标准表示法:

int inc(int i) {
    return i + 1;
}

Kotlin中的反转表达:

fun inc(i: Int): Int {
    return i + 1
}

这种方式有几个原因令人讨厌。

首先,您需要在名称和类型之间键入并阅读这个嘈杂的冒号。这个额外角色的目的是什么?为什么名称与其类型分离?我不知道。可悲的是,这让你在Kotlin中工作变得更加困难。

第二个问题。当你读取一个方法声明时,首先,你对名字和返回类型感兴趣,然后你扫描参数。

在Kotlin中,方法的返回类型可能远在行尾,所以需要滚动:

private fun getMetricValue(kafkaTemplate : KafkaTemplate, metricName : String) : Double {
    ...
}

或者,如果参数是逐行格式的,则需要搜索。您需要多少时间才能找到此方法的返回类型?

@Bean
fun kafkaTemplate(
        @Value("\${interactions.kafka.bootstrap-servers-dc1}") bootstrapServersDc1: String,
        @Value("\${interactions.kafka.bootstrap-servers-dc2}") bootstrapServersDc2: String,
        cloudMetadata: CloudMetadata,
        @Value("\${interactions.kafka.batch-size}") batchSize: Int,
        @Value("\${interactions.kafka.linger-ms}") lingerMs: Int,
        metricRegistry : MetricRegistry
): KafkaTemplate {

    val bootstrapServer = if (cloudMetadata.datacenter == "dc1") {
        bootstrapServersDc1
    }
    ...
}

反转符号的第三个问题是IDE的自动完成得不好。在标准符号中,您从类型名称开始,并且很容易找到类型。一旦你选择了一个类型,一个IDE会给你提供一些关于变量名的建议,这些变量名是从选定的类型派生的 所以你可以快速输入这样的变量:

MongoExperimentsRepository repository

在Kotlin中输入这个变量在IntelliJ中是很难的,而这是有史以来最伟大的IDE。如果您有多个存储库,则在自动完成列表中找不到正确的选项。这意味着必须使用手工输入完整的变量名称。

repository : MongoExperimentsRepository

待续…

欢迎大家加入粉丝群:963944895,群内免费分享Spring框架、Mybatis框架SpringBoot框架、SpringMVC框架、SpringCloud微服务、Dubbo框架、Redis缓存、RabbitMq消息、JVM调优、Tomcat容器、MySQL数据库教学视频及架构学习思维导图

你可能感兴趣的:(Java)