我们与Kotlin的故事:从尝试到放弃-前端之巅 作者|Bartosz Walacik译者|无明编辑|覃云Kotlin 现在很流行,它提供了编译时 null 安全,代码更加简洁。它??
我们与Kotlin的故事:从尝试到放弃-前端之巅
作者|Bartosz Walacik译者|无明编辑|覃云
Kotlin 现在很流行,它提供了编译时 null 安全,代码更加简洁。它比 Java 更好,你应该切换到 Kotlin,否则就只能坐以待毙。不过,在转向 Kotlin 之前,请先听听这个故事——在这个故事里,那些稀奇古怪的东西让我们忍无可忍,最后不得不使用 Java 重写整个项目。
我们尝试过 Kotlin,但现在开始使用 Java 10 重写代码。
我有一组自己最喜欢的 JVM 语言,/main 目录下的 Java 代码和 /test 目录下的 Groovy 代码是我最爱的组合。2017 年夏天,我的团队开始了一个新的微服务项目,和往常一样,我们讨论了要使用什么编程语言和技术。我们想尝试新的东西,所以决定试试 Kotlin。由于在 Kotlin 中找不到可替代 Spock 的测试框架,所以我们决定继续在 /test 目录中使用 Groovy(Spek 不如 Spock 好)。2018 年冬天,在使用 Kotlin 数月之后,我们总结了它的优势和劣势,并得出结论:Kotlin 导致我们生产力下降。于是,我们开始使用 Java 重写这个微服务。
原因如下:
命名遮蔽(name shadowing)
类型推断
编译时 null 安全
类字面量
反向类型声明
Companion 对象
集合字面量
Maybe 语法
数据类
公开类
陡峭的学习曲线命名遮蔽
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"
这是 Kotlin 曾经比 Java 真正好的地方。我故意说“曾经”,那是因为 Java 10 现在也有了局部变量类型推断。
Java 10 中的类型推断:
var a = "10";
为了公平起见,我需要补充一点,Kotlin 在这方面仍然略胜一筹,因为在 Kotlin 中,可以在其他上下文中使用类型推断,例如,单行代码方法。编译时 null 安全
null 安全类型是 Kotlin 的杀手级特性。在 Kotlin 中,
谢宗芬类型默认是不可空的。如果你需要一个可空类型,需要添加?,例如:
val a: String? = null// okval b: String = null // compilation error
如果使用不带空值检查的可空变量,将无法通过编译,例如:
println (a.length) // compilation errorprintln (a?.length) // fine, prints nullprintln (a?.length ?: 0) // fine, prints 0
一旦使用了这两种类型,不可空的 T 和可空的 T?,那么就可以避免出现 Java 中最常见的异常——NullPointerException。真的吗?事情并没有那么简单。
当需要将 Kotlin 代码和 Java 代码(库是用 Java 编写的,所以我猜经常会发生这种情况)混在一起时,事情就会变得很糟糕。于是,出现了第三种类型 T!。它被称为平台类型,代表 T 或 T?。或者更确切地说,T! 表示未定义可空性的 T。这种奇怪的类型无法在 Kotlin 中表示,只能从 Java 类型推断出来。T! 可能会误导你,因为它对空值放松了警惕,并禁用了 Kotlin 的 null 安全。
比如下面的 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)// compiles but assignment can throw NPE at runtime println ("f.len : " + f.length)}
你需要这样来解决这个问题:
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 类型系统中的!、? 和!! 太过复杂了。为什么 Kotlin 将 Java T 推断为 T! 而不是 T? 呢?Java 互操作性似乎损害了 Kotlin 的类型推断特性。看起来,我们似乎应该为所有通过 Java 方法赋值的 Kotlin 变量显式声明类型(如 T?)。
类字面量
在使用 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::classval javaClass : Class<LocalDate> = 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<String, ByteArray>, metricName : String) : Double { ...}
或者,如果参数按照行进行了格式化,则可能需要通过搜索才能找到返回类型。你需要花多少时间才能找到此方法的返回类型?
@Beanfun 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
但即使是在 IntelliJ 中输入这个变量也是很费事的。如果你有多个 repository,则在自动完成列表中找不到正确的可选项,这意味需要手动输入完整的变量名。
repository : MongoExperimentsRepositoryCompanion 对象
一位 Java 程序员来到 Kotlin 面前。
“嗨,Kotlin。我是新来的,可以使用静态成员吗?“他问。
“不行,我是面向对象的,而静态成员不是面向对象的。“Kotlin 回答道。
“好吧,但我需要 MyClass 的 logger 对象,我该怎么办?”
“没问题,你可以使用 Companion 对象。”
“什么是 Companion 对象?”
“它是与类绑定的单例对象,可以把你的 logger 放在 Companion 对象中。“Kotlin 解释说“我懂了,是这样吗?”
class MyClass { companion object { val logger = LoggerFactory.getLogger(MyClass::class.java) }}
“是的!”
“非常繁琐的语法,”程序员似乎感到困惑,“但没关系,现在我可以这样调用 logger——MyClass.logger,就像 Java 中的静态成员一样?”
“嗯……是的,但它不是一个静态成员!这里只有对象。你可以把它看作是已经实例化为单例对象的匿名内部类,但实际上这个类不是匿名的,它叫作 Companion,不过你可以忽略这个名字。是不是很简单?“
通过单例来声明对象的做法很管用,但是从语言中移除静态成员是不切实际的。在 Java 中,我们一直使用静态的 logger 对象。它只是一个 logger 而已,这个时候我们没有必要关心它是不是面向对象的,而且它并不会带来任何坏处。
有时候,我们必须使用 static,比如 public static void main() 仍然是启动 Java 应用程序的唯一方式。试着不使用谷歌搜索写出下面的 Companion 对象吧。
class AppRunner { companion object { @JvmStatic fun main(args: Array<String>) {SpringApplication.run(AppRunner::class.java, *args) } }}集合字面量
在 Java 中,初始化一个 List 需要很多代码:
import java.util.Arrays;...List<String> strings = Arrays.asList("Saab", "Volvo");
而初始化一个 Map 更加繁琐,所以很多人使用 Guava 来代替:
import com.google.common.collect.ImmutableMap;...Map<String, String> string = ImmutableMap.of("firstName", "John", "lastName", "Doe");
我们仍然在等待新的 Java 语法,可以简化集合和 Map 字面量的声明。而在其他很多语言中老吾老以及,已经有了便利的语法。
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 = ['firstName': 'John', 'lastName': 'Doe']
简单来说,整洁的集合字面量语法是我们对现代编程语言的期待,特别是如果这门语言是从头开始创建的。Kotlin 提供了一堆内置函数:listOf()、mutableListOf()、mapOf()、hashMapOf() 等。
Kotlin:
val list = listOf("Saab", "Volvo")val map = mapOf("firstName" to "John", "lastName" to "Doe")
键和值通过 to 操作符配对释魂乐队,这样很好,但为什么不使用众所周知的冒号呢?Maybe 语法
函数式语言(如 Haskell)没有空值,相反,它们提供了 Maybe monad(如果你对 monad 不熟悉,请阅读 Tomasz Nurkiewicz 的这篇文章 http://www.nurkiewicz.com/2016/06/functor-and-monad-examples-in-plain-java.html)花瓶记。
在很早以前,Scala 就将 Maybe 语法引入到了 JVM 世界,也就是 Option,然后 Java 8 也推出了 Optional。现在,Optional 是处理 API 返回类型空值的一种非常流行的方式。
Kotlin 中没有 Optional,所以似乎应该用 Kotlin 的可空类型来代替。
通常情况下,当你有一个 Optional 时,想要进行一系列 null 安全的转换,并在最后处理 null。
例如,在 Java 中:
public int parseAndInc(String number) { return Optional.ofNullable(number) .map(Integer::parseInt) .map(it -> it + 1) .orElse(0);}
也许会有人说,在 Kotlin 可以使用 let 函数代替 map:
fun parseAndInc(number: String?): Int { return number.let { Integer.parseInt(it) }.let { it -> it + 1 } ?: 0}
这样可以吗?可以的,但并没那么简单。上面的代码是错误的,parseInt() 会抛出 NPE。
只有当值存在时,monad 风格的 map() 才会被执行,null 会被忽略。可惜的是,Kotlin 的 let 函数与 map 不一样,它会从左侧调用所有的内容,包括 null。
所以,为了让代码变得 null 安全,必须在每个 let 前面添加?:
fun parseAndInc(number: String?): Int { return number?.let { Integer.parseInt(it) }?.let { it -> it + 1 } ?: 0}
现在,比较 Java 和 Kotlin 版本的可读性,你更倾向哪个?数据类
在实现 Value Object(也叫 DTO)时,Kotlin 使用数据类来减少样板代码,而在 Java 中,样板代码是不可避免的。
例如,在 Kotlin 中,你写了一个 Value Object:
data class User(val name: String, val age: Int)
Kotlin 负责生成 equals()、hashCode()、toString() 和 copy() 方法。
在实现简单的 DTO 时它非常有用,但请记住,数据类有严重的局限性——它们是 final 的。也就是说,我们无法扩展数据类或将其抽象化,所以你可能不会在核心领域模型中使用它们。
这个局限性不是 Kotlin 的错,因为我们没有办法在不违反替换原则的情况下正确生成基于值的 equals() 方法。这就是为什么 Kotlin 不允许继承数据类。公开类
在 Kotlin 中,类默认是 final 的。如果想扩展一个类,必须添加 open 修饰符。
继承语法如下所示:
open class Baseclass Derived : Base()
Kotlin 使用: 操作符代替 extends 关键字,还记得吗,这个操作符已经用于分隔变量名与类型佛萨 。难道我们又回到了 C++ 语法?
颇具争议的是,在默认情况下,类是 final 的。但我们生活在一个满是框架的世界,而框架喜欢使用 AOP。 Spring 使用库(cglib、jassist)为 bean 生成动态代理,Hibernate 通过扩展实体类来实现延迟加载。
如果你使用 Spring,那么就有两种选择。你可以在所有的 bean 类前面加上 open(这很枯燥),或者使用这个编译器插件:
buildscript { dependencies { classpath group: 'org.jetbrains.kotlin', name: 'kotlin-allopen', version: "$versions.kotlin" }}陡峭的学习曲线
如果你认为你可以快速学习 Kotlin,因为你已经学过 Java,那么你错了。Kotlin 会让你陷入深渊。事实上,Kotlin 的语法更接近 Scala。你将不得不忘记 Java,切换到一个完全不同的语言。
相反,学习 Groovy 是一趟愉快的旅程。Java 代码与 Groovy 代码相得益彰,因此你可以从将.java 文件扩展名改为.groovy 开始。最后的想法
学习新技术就像投资,我们投入时间,然后应该得到回报。我不是说 Kotlin 是一种糟糕的语言,但在我们的案例中,成本超过了收益。原文链接
https://allegro.tech/2018/05/From-Java-to-Kotlin-and-Back-Again.html前端之巅
「前端之巅」是 InfoQ 旗下关注大前端技术的垂直社群。紧跟时代潮流,共享一线技术,欢迎关注。
活动推荐
8月18日,我们将在一场面向技术人的区块链大会,大会关注目前区块链领域前沿技术与落地应用,将邀请国内外一线技术专家交流分享,和你一起探索区块链技术的更多可能!目前大会6折最后一周,火热招募中!扫描二维码进入官网查看大会议题。
全文详见:https://p66p.cn/32716.html
TOP