从Java到 Kotlin,再从Kotlin回归Java

开发 后端
由于此博客文章引起高度关注和争议,我们认为值得在Allegro上增加一些关于我们如何工作和做出决策的背景。Allegro拥有超过50个开发团队可以自由选择被我们PaaS所支持的技术。我们主要使用Java、Kotlin、Python和Golang进行编码。本文中提出的观点来自作者的经验。

由于此博客文章引起高度关注和争议,我们认为值得在Allegro上增加一些关于我们如何工作和做出决策的背景。Allegro拥有超过50个开发团队可以自由选择被我们PaaS所支持的技术。我们主要使用Java、Kotlin、Python和Golang进行编码。本文中提出的观点来自作者的经验。

[[231717]]

Kotlin很流行,Kotlin很时髦。Kotlin为你提供了编译时null-safety和更少的boilerplate。当然,它比Java更好。你应该切换到Kotlin或作为码农遗老直到死亡。等等,或者你不应该如此?在开始使用Kotlin编写之前,请阅读一个项目的故事。关于奇技和障碍的故事变得如此令人讨厌,因此我们决定重写之。

我们尝试过Kotlin,但现在我们正在用Java10重写

我有我最喜欢的JVM语言集。Java的/main和Groovy的/test对我来说是组好的组合。2017年夏季,我的团队开始了一个新的微服务项目,我们就像往常一样谈论了语言和技术。在Allegro有几个支持Kotlin的团队,而且我们也想尝试新的东西,所以我们决定试试Kotlin。由于Kotlin中没有 Spock 的替代品,我们决定继续在/test中使用Groovy( Spek 没有Spock好用)。在2018年的冬天,每天与Kotlin相伴的几个月后,我们总结出了正反两面,并且得出Kotlin使我们的生产力下降的结论。我们开始用Java重写这个微服务。

这有几个原因:

  • 名称遮蔽
  • 类型推断
  • 编译时空指针安全
  • 类文字
  • 反向类型声明
  • 伴侣对象
  • 集合文字
  • 也许? 不
  • 数据类
  • 公开课
  • 陡峭的学习曲线

名称遮掩

这是 Kotlin 让我感到***惊喜的地方。看看这个函数:

 

  1. fun inc(num : Int) { 
  2.     val num = 2 
  3.     if (num > 0) { 
  4.         val num = 3 
  5.     } 
  6.     println ("num: " + num) 

当你调用inc(1)的时候会输出什么呢?在Kotlin中方法参数是一个值,所以你不能改变num参数。这是好的语言设计,因为你不应该改变方法的参数。但是你可以用相同的名称定义另一个变量,并按照你想要的方式初始化。现在,在这个方法级别的范围中你拥有两个叫做num的变量。当然,同一时间你只能访问其中一个num,所以num的值会改变。将军,无解了。

在if主体中,你可以添加另一个num,这并不令人震惊(新的块级别作用域)。

好的,在Kotlin中,inc(1)输出2。但是在Java中,等效代码将无法通过编译。

 

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

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

 

  1. public class Shadow { 
  2.     int val; 
  3.  
  4.     public Shadow(int val) { 
  5.         this.val = val; 
  6.     } 

在Kotlin中,遮蔽有点过分了。当然,这是Kotlin团队的一个设计缺陷。IDEA团队试图把每一个遮蔽变量都通过简洁的警告来向你展示,以此修复这个问题:Name shadowed。两个团队都在同一家公司工作,所以或许他们可以相互交流并在遮蔽问题上达成一致共识?我感觉——IDEA是对的。我无法想象存在这种遮蔽了方法参数的有效用例。

类型推断

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

例如,这段Kotlin代码:

  1. var a = "10" 

将由Kotlin编译器翻译成:

  1. var a : String = "10" 

它曾经是胜过Java的真正优点。我故意说曾经是,因为——有个好消息——Java10 已经有这个功能了,并且Java10现在已经可以使用了。

Java10 中的类型涂端:

  1. var a = "10"

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

更多关于Java10 中的 局部变量类型推断 。

编译时空值安全

Null-safe 类型是Kotlin的杀手级特征。 这个想法很好。 在Kotlin,类型是默认的非空值。 如果您需要一个可空类型,您需要添加?符号, 例如:

 

  1. val a: String? = null      // ok  
  2. val b: String = null       // 编译错误 

如果您在没有空检查的情况下使用可空变量,那么Kotlin将无法编译,例如:

 

  1. println (a.length)          // compilation error 
  2. println (a?.length)         // fine, prints null 
  3. println (a?.length ?: 0)    // fine, prints 0 

一旦你有了这两种类型, non-nullable T 和nullable T?, 您可以忘记Java中最常见的异常——NullPointerException。 真的吗? 不幸的是,事情并不是那么简单。

当您的Kotlin代码必须与Java代码一起使用时,事情就变得很糟糕了(库是用Java编写的,所以我猜它经常发生)。 然后,第三种类型就跳出来了——T! 它被称为平台类型,它的意思是T或T?, 或者如果我们想要精确,T! 意味着具有未定义空值的 T类型 。 这种奇怪的类型不能用Kotlin来表示,它只能从Java类型推断出来。 T! 会误导你,因为它放松了对空的限制,并禁用了Kotlin的空值安全限制。

看看下面的Java方法:

 

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

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

***种方法。 你可以使用字符串,代码看起来很安全,但是会抛出空指针异常。

 

  1. fun doSth(text: String) { 
  2.     val f: String = Utils.format(text)       // compiles but assignment can throw NPE at runtime 
  3.     println ("f.len : " + f.length) 

你需要用增加判断来解决这个问题:

 

  1. fun doSth(text: String) { 
  2.     val f: String = Utils.format(text) ?: ""  //  
  3.     println ("f.len : " + f.length) 

第二种方法。 您可以使用String?, 然后你的程序就是空值安全的了。

 

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

第三种方法。 如果你让Kotlin做了令人难以置信的局部变量类型推断呢?

 

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

坏主意。 这个Kotlin的代码看起来很安全,也可以编译通过,但是允许空值在你的代码中不受约束的游走,就像在Java中一样。

还有一个窍门,!! 操作符。 使用它来强制推断f类型为String类型:

 

  1. fun doSth(text: String) { 
  2.     val f = Utils.format(text)!!          // throws NPE when format() returns null 
  3.     println ("f.len : " + f.length) 

在我看来, Kotlin 的类型系统中所有这些类似scala的东西!,?和!!,实在是 太复杂了。 为什么Kotlin从Java的T类型推断到T! 而不是T?呢? 似乎Java互操作性破坏了Kotlin的杀手特性——类型推断。 看起来您应该显式地声明类型(如T?),以满足由Java方法填充的所有Kotlin变量。

类 字面量

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

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

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

在 Groovy 中,类字面量被简化为本质。 你可以省略.class,不管它是Groovy还是Java类都没关系。

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

Kotlin区分了Kotlin和Java类,并为其准备了不同的语法形式:

 

  1. val kotlinClass : KClass<LocalDate> = LocalDate::class 
  2. val javaClass : Class<LocalDate> = LocalDate::class.java 

所以在 Kotlin ,你不得不写:

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

这真是丑爆了。

相反顺序的类型声明

在C系列编程语言中,有一个标准的声明类型的方式。即先写出类型,再写出声明为该类型的东西(变量、字段、方法等)。

在 Java 中如下表示:

 

  1. int inc(int i) { 
  2.     return i + 1; 

在 Kotlin 中则是相反顺序的表示:

 

  1. fun inc(i: Int): Int { 
  2.     return i + 1 

这让人觉得恼火,因为:

首先,你得书写或者阅读介于名称和类型之间那个讨厌的冒号。这个多余的字母到底起什么作用?为什么要把名称和类型 分隔开 ?我不知道。不过我知道这会加大使用Kotlin的难度。

第二个问题。在阅读一个方法声明的时候,你***想知道的应该是方法的名称和返回类型,然后才会去了解参数。

在 Kotlin 中,方法的返回类型远在行末,所以可能需要滚动屏幕来阅读:

 

  1. private fun getMetricValue(kafkaTemplate : KafkaTemplate<String, ByteArray>, metricName : String) : Double { 
  2.     ... 

另一种情况,如果参数是按分行的格式写出来的,你还得去寻找返回类型。要在下面这个方法定义中找到返回类型,你需要花多少时间?

 

  1. @Bean 
  2. fun kafkaTemplate( 
  3.         @Value("\${interactions.kafka.bootstrap-servers-dc1}") bootstrapServersDc1: String, 
  4.         @Value("\${interactions.kafka.bootstrap-servers-dc2}") bootstrapServersDc2: String, 
  5.         cloudMetadata: CloudMetadata, 
  6.         @Value("\${interactions.kafka.batch-size}") batchSize: Int
  7.         @Value("\${interactions.kafka.linger-ms}") lingerMs: Int
  8.         metricRegistry : MetricRegistry 
  9. ): KafkaTemplate<String, ByteArray> { 
  10.  
  11.     val bootstrapServer = if (cloudMetadata.datacenter == "dc1") { 
  12.         bootstrapServersDc1 
  13.     } 
  14.     ... 

关于相反顺序的 第三个问题 是限制了IDE的自动完成功能。在标准顺序中,因为是从类型开始,所以很容易找到类型。一旦确定了类型,IDE 就可以根据类型给出一些与之相关的变量名称作为建议。这样就可以快速输入变量名,不像这样:

  1. MongoExperimentsRepository repository 

即时在 Intellij 这么优秀的 IDE 中为 Kotlin 输入这样的变量名也十分不易。如果代码中存在很多 Repository,就很难在自动完成列表中找到匹配的那一个。换句话说,你得手工输入完整的变量名。

  1. repository : MongoExperimentsRepository 

伴生对象

一个 Java 程序员来到 Kotlin 阵营。

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

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

“好吧,但我需要用于 MyClass 日志记录器,该怎么办?”

“没问题,可以使用伴生对象。”

“伴生对象是什么鬼?”

“它是与类绑定的一个单例对象。你可以把日志记录器放在伴生对象中,” Kotlin 如此解释。

“明白了。是这样吗?”

 

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

“对!“

“好麻烦的语法,”这个程序看起来有些疑惑,“不过还好,现在我可以像这样——MyClass.logger——调用日志记录了吗?就像在 Java 中使用静态成员那样?”

“嗯……是的,但是它不是静态成员!它只是一个对象。可以想像那是一个匿名内部类的单例实现。而实际上,这个类并不是匿名的,它的名字是 Companion,你可以省略这个名称。明白吗?这很简单。”

我很喜欢 对象声明 的概念——单例是种很有用的模式。从从语言中去掉静态成员就不太现实了。我们在Java中已经使用了若干年的静态日志记录器,这是非常经典的模式。因为它只是一个日志记录器,所以我们并不关心它是否是纯粹的面向对象。只要它起作用,而且不会造成损害就好。

有时候,我们 必须 使用静态成员。古老而友好的 public static void main() 仍然是启动 Java 应用的唯一方式。在没有Google的帮助下尝试着写出这个伴生对象。

 

  1. class AppRunner { 
  2.     companion object { 
  3.         @JvmStatic fun main(args: Array<String>) { 
  4.             SpringApplication.run(AppRunner::class.java, *args) 
  5.         } 
  6.     } 

集合字面量

在 Java 中初始化列表需要大量的模板代码:

 

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

初始化 Map 更加繁琐,所以不少人使用 Guava :

 

  1. import com.google.common.collect.ImmutableMap; 
  2. ... 
  3.  
  4. Map<String, String> string = ImmutableMap.of("firstName""John""lastName""Doe"); 

我们仍然在等待 Java 产生新语法来简化集合和映射表的字面表达。这样的语法在很多语言中都自然而便捷。

JavaScript:

 

  1. const list = ['Saab''Volvo'
  2. const map = {'firstName''John''lastName' : 'Doe'

Python:

 

  1. list = ['Saab''Volvo'
  2. map = {'firstName''John''lastName''Doe'

Groovy:

 

  1. def list = ['Saab''Volvo'
  2. def map = ['firstName''John''lastName''Doe'

简单来说,简洁的集合字面量语法在现代编程语言中倍受期待,尤其是初始化集合的时候。Kotlin 提供了一系列的内建函数来代替集合字面量: listOf()、mutableListOf()、mapOf()、hashMapOf(),等等。

Kotlin:

 

  1. val list = listOf("Saab""Volvo"
  2. val map = mapOf("firstName" to "John""lastName" to "Doe"

映射表中的键和值通过 to 运算符关联在一起,这很好,但是为什么不使用大家都熟悉的冒号(:)?真是令人失望!

Maybe?不

函数式编程语言(比如 Haskell)没有空(null)。它们提供 Maybe Monad(如果你不清楚 Monad,请阅读这篇由 Tomasz Nurkiewicz 撰写 文章 )。

在很久以前,Scala 就将 Maybe 作为 Option 引入 JVM 世界,然后在 Java 8 中被采用,成为 Optional。现在 Optional 广泛应用于 API 边界,用于处理可能含空值的返回类型。

Kotlin 中并没有与 Optional 等价的东西。看起来你应该使用 Kotlin 的可空类型封装。我们来研究一下这个问题。

通常,在使用 Optional 时,你会先进行一系列空安全的转换,***来处理空值。

比如在 Java 中:

 

  1. public int parseAndInc(String number) { 
  2.     return Optional.ofNullable(number) 
  3.                    .map(Integer::parseInt) 
  4.                    .map(it -> it + 1) 
  5.                    .orElse(0); 

在 Kotlin 中也没问题,使用 let 功能:

 

  1. fun parseAndInc(number: String?): Int { 
  2.     return number.let { Integer.parseInt(it) } 
  3.                  .let { it -> it + 1 } ?: 0 

可以吗?是的,但并不是这么简单。上面的代码可能会出错,从 parseInt() 中抛出 NPE。只有值存在的时候才能执行 Monad 风格的 map(),否则,null 只会简单的传递下去。这就是 map() 方便的原因。然后不幸的是,Kotlin 的 let 并不是这样工作的。它只是从左往右简单地执行调用,不在乎是否是空。

因此,要让这段代码对空安全,你必须在 let 前添加 ?:

 

  1. fun parseAndInc(number: String?): Int { 
  2.     return number?.let { Integer.parseInt(it) } 
  3.                  ?.let { it -> it + 1 } ?: 0 

现在,比如 Java 和 Kotlin 两个版本的可读性,你更喜欢哪一个?

责任编辑:未丽燕 来源: 开源中国翻译文章
相关推荐

2017-09-22 11:31:28

KotliJava编程语言

2020-08-26 14:44:55

Java开发代码

2022-07-15 13:01:13

Kotlin编程语言Java

2017-05-19 18:01:04

GoogleKotlin数据

2017-08-03 15:54:50

Kotlin继承

2022-02-28 10:38:13

Kotlin插件Android

2018-03-12 10:57:14

JavaKotlin语法

2018-01-03 11:51:06

KotlinTipsJava

2024-01-19 09:21:35

携程开源

2021-03-15 09:00:00

开发JavaKotlin

2020-08-20 20:45:17

KotlinJava优势

2022-06-15 09:01:57

开发Java

2023-11-15 16:37:46

2020-06-01 09:30:25

代码开发Kotlin

2012-12-10 13:24:15

回归分析数据挖掘

2019-01-02 11:30:36

JavaKotlin语言

2017-12-19 16:24:20

2019-08-28 18:46:33

KotlinAndroid语言

2012-03-31 10:49:18

ibmdw

2023-10-24 19:37:34

协程Java
点赞
收藏

51CTO技术栈公众号