抛弃 Java 改用 Kotlin 的六个月后,我后悔了 - Go语言中文社区

抛弃 Java 改用 Kotlin 的六个月后,我后悔了


点击上方“CSDN”,选择“置顶公众号”

关键时刻,第一时间送达!

毫无疑问,Kotlin 目前很受欢迎,业界甚至有人认为其将取代 Java 的霸主地位。它提供了 Null 安全性,从这一点来说它确实比 Java 更好。那么是不是这就意味着开发者应该毫不犹豫地拥抱 Kotlin,否则就落伍了?

640?wx_fmt=jpeg

等等,或许事情并非如此。

在开始使用 Kotlin 编程之前,本文想要分享个故事给你。在这个故事中,作者最早使用 Kotlin 来编写一个项目,后来 Kotlin 的各种怪异模式以及一些其他障碍越来越让人厌烦,最终,他们决定重写这个项目。

以下为译文:

一直以来,我对基于 JVM 的语言都非常情有独钟。我通常会用 Java 来编写主程序,再用 Groovy 编写测试代码,两者配合使用得心应手。

2017年夏天,团队发起了一个新的微服务项目,和往常一样,我们需要对编程语言和技术进行选型。部分团队成员是 Kotlin 的拥护者,再加上我们都想尝试一下新的东西,于是我们决定用 Kotlin 来开发这个项目。由于 Spock 测试框架不支持 Kotlin,因此我们决定坚持使用 Groovy 来测试。

2018年春天,使用 Kotlin 开发几个月之后,我们总结了 Kotlin 的优缺点,最终结论表明 Kotlin 降低了我们的生产力。

于是我们使用 Java 来重写这个微服务项目。

那么 Kotlin 主要存在哪些弊端?下面来一一解释。

名称遮蔽

这是 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,因为作用域的原因 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 团队试图通过向每个遮蔽变量显示警告信息来解决这个问题。两个团队在同一家公司工作,或许他们可以互相交流并就遮蔽问题达成共识。我从个人角度赞成 IDEA 的做法因为我想不到有哪些应用场景需要遮蔽方法参数。

类型推断

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

例如,这个Kotlin代码:

var a = "10"

Kotlin 编译器会将其翻译成: 

var a : String = "10"

Java 同样具备这个特性,Java 10中的类型推断示例如下:  

var a = "10";

实话实说,Kotlin 在这一点上确实更胜一筹。当然,类型推断还可应用在多个场景。关于 Java 10中的局部变量类型推断,点击以下链接了解更多:

  • https://medium.com/@afinlay/java-10-sneak-peek-local-variable-type-inference-var-3022016e1a2b

Null 安全类型

Null 安全类型是 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

那么是不是如果你同时拥有不可为空和可为空的变量,就可以避免 Java 中最常见的 NullPointerException 异常吗?事实并没有想象的简单。

当 Kotlin 代码必须调用 Java 代码时,事情会变得很糟糕,比如库是用 Java 编写的,我相信这种情况很常见。于是第三种类型产生了,它被称为平台类型。Kotlin 无法表示这种奇怪的类型,它只能从 Java 类型推断出来。 它可能会误导你,因为它对空值很宽松,并且会禁用 Kotlin 的 NULL 安全机制。

看看下面这个 Java 方法:

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

假如你想调用 format(String)。应该使用哪种类型来获得这个 Java 方法的结果呢?你有三个选择。

第一种方法:你可以使用 String,代码看起来很安全,但是会抛出 NullPointerException 异常

fun doSth(text: String) {
    val f: String = Utils.format(text)       // compiles but assignment can throw NPE at runtime
    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 安全性。

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 的类型系统过于复杂。Java 互操作性似乎损害了 Kotlin 类型推断这个重量级功能。

类名称字面常量

使用类似 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> = LocalDate::class
val javaClass : Class<LocalDate> = LocalDate::class.java

因此在 Kotlin 中,你必须写成如下形式:

val gson = GsonBuilder().registerTypeAdapter(LocalDate::class.javaLocalDateAdapter()).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<String, ByteArray>, 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<String, ByteArray> {
    val bootstrapServer = if (cloudMetadata.datacenter == "dc1") {
        bootstrapServersDc1
    }
    ...
}

第三个问题是 IDE 中的自动化支持不够好。标准做法从类型名称开始,并且很容易找到类型。一旦选择一个类型,IDE 会提供一些关于变量名的建议,这些变量名是从选定的类型派生的,因此你可以快速输入这样的变量: 

MongoExperimentsRepository repository

Kotlin 尽管有 IntelliJ 这样强大的 IDE,输入变量仍然是很难的。如果你有多个存储库,在列表中很难实现正确的自动补全,这意味着你不得不手动输入完整的变量名称。

repository : MongoExperimentsRepository

伴生对象

一位 Java 程序员来到 Kotlin 面前。

“嗨,Kotlin。我是新来的,我可以使用静态成员吗?"他问。

 “不行。我是面向对象的,静态成员不是面向对象的。” Kotlin 回答。

 “好吧,但我需要 MyClass 的 logger,我该怎么办?” 

“这个没问题,使用伴生对象即可。”

 “那是什么东西?” “这是局限到你的类的单独对象。把你的 logger 放在伴生对象中。”Kotlin解释说。

 “我懂了。这样对吗?”

class MyClass {
    companion object {
        val logger = LoggerFactory.getLogger(MyClass::class.java)
    }
}

“正确!”

 “很详细的语法,”程序员看起来很疑惑,“但是没关系,现在我可以像 MyClass.logger 这样调用我的 logger,就像 Java 中的一个静态成员?” 

“嗯......是的,但它不是静态成员!这里只有对象。把它看作是已经实例化为单例的匿名内部类。事实上,这个类并不是匿名的,它的名字是 Companion,但你可以省略这个名字。看到了吗?这很简单。"

我很欣赏对象声明的概念——单例很有用。但从语言中删除静态成员是不切实际的。在 Java 中我们使用静态 Logger 很经典,它只是一个 Logger,所以我们不关心面向对象的纯度。它能够工作,从来没有任何坏处。

因为有时候你必须使用静态。旧版本 public static void main() 仍然是启动 Java 应用程序的唯一方式。

class AppRunner {
    companion object {
        @JvmStatic fun main(args: Array<String>) {
            SpringApplication.run(AppRunner::class.java*args)
        }
    }
}

集合字面量

在Java中,初始化列表非常繁琐:

import java.util.Arrays;
...
List<String> strings = Arrays.asList("Saab""Volvo");

初始化地图非常冗长,很多人使用 Guava:

import com.google.common.collect.ImmutableMap;
...
Map<StringString> string = ImmutableMap.of("firstName""John""lastName""Doe");

在 Java 中,我们仍然在等待新的语法来表达集合和映射。语法在许多语言中非常自然和方便。

JavaScript:

const list = ['Saab''Volvo']
const map = {'firstName''John''lastName' : 'Doe'}

Python:

list = ['Saab''Volvo']
map = {'firstName''John''lastName''Doe'}

Groovy:

def list = ['Saab''Volvo']
def map = [ 版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/csdnnews/article/details/80746096
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2019-08-27 15:54:58
  • 阅读 ( 1535 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢