最近 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. 如果您有多个存储库, 则在自动完成列表中找不到正确的选项. 这意味着必须使用手工输入完整的变量名称.
一位 Java 程序员来到 Kotlin.
"嗨, Kotlin. 我是新来的, 我可以使用静态成员吗?" 他问.
"不. 我是面向对象的, 静态成员不是面向对象的,"Kotlin 回答.
- "好吧, 但我需要 logger 的 MyClass, 我该怎么办?"
- "没问题, 用 Companion object 呢."
- "哪个是 Companion object 吗?"
"这是单独的对象, 绑定到你的类. 把你的 logger 放到 Companion object 中就可以了,"Kotlin 解释说.
- "我懂了. 这样对吗?"
- class MyClass {
- companion object {
- val logger = LoggerFactory.getLogger(MyClass::class.java)
- }
- }
- "是的!"
"很详细的语法," 程序员看起来很疑惑,"但是没关系, 现在我可以像这样调用我的 logger - MyClass.logger 就像 Java 中的一个静态成员?"
"嗯...... 是的, 但它不是静态成员! 这里只有对象. 把它看作是已经实例化为单例的匿名内部类. 事实上, 这个类并不是匿名的, 它的名字是 Companion, 但你可以省略这个名字. 看到吗? 这很简单."
我很欣赏对象声明的概念 - 单例很有用. 但是从语言中删除静态成员是不切实际的. 在 Java 中, 我们使用静态 logger 多年. 这很经典. 它只是一个记录器, 所以我们不关心面向对象的纯度. 它的工作原理并不会造成没有任何问题.
有时候, 你必须使用静态. 旧版本 public static void main()仍然是启动 Java 应用程序的唯一方式. 试着写这 companion object 的咒语, 不要使用谷歌搜索.
- class AppRunner {
- companion object {
- @JvmStatic fun main(args: Array) {
- SpringApplication.run(AppRunner::class.java, *args)
- }
- }
- }
集合字面量 Collection literals
在 Java 中, 初始化列表需要很多仪式:
- import java.util.Arrays;
- ...
- List strings = Arrays.asList("Saab", "Volvo");
初始化过程非常冗长, 很多人使用 Guava:
- import com.google.common.collect.ImmutableMap;
- ...
- Map 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 = ['firstName': 'John', 'lastName': 'Doe']
简单地说, 集合字面量的整齐语法就是您对现代编程语言的期望, 特别是如果它是从头开始创建的. Kotlin 提供了一组内置函数实现集合字面量: listOf(),mutableListOf(),mapOf(),hashMapOf(), 等.
- Kotlin:
- val list = listOf("Saab", "Volvo")
- val map = mapOf("firstName" to "John", "lastName" to "Doe")
在 mao 中, 键和值与 to 运算符配对, 这很好, 但为什么不使用众所周知的符号 ":" 呢, 令人失望.
Maybe? 不
函数式语言 (如 Haskell) 没有空值. 相反, 他们提供 Maybe monad(如果你不熟悉单子, 请阅读 Tomasz Nurkiewicz 的文章).
Maybe 很久以前就被 Scala 引入 JVM 世界作为 Option, 然后在 Java 8 中被采用为 Optional. 现在, Maybe 是在 API 边界处理返回类型中的空值的非常流行的方式.
Kotlin 中没有可选的等价物. 看来你应该使用裸 Kotlin 的可空类型. 让我们来调查这个问题.
通常情况下, 当你有一个 Optional 的时候, 你想要应用一系列无效的转换, 并在和处理 null.
例如, 在 Java 中:
- public int parseAndInc(String number) {
- return Optional.ofNullable(number)
- .map(Integer::parseInt)
- .map(it -> it + 1)
- .orElse(0);
- }
人们可能会说在 Kotlin 中也没问题, 为了映射你可以使用这个 let 函数:
- fun parseAndInc(number: String?): Int {
- return number.let { Integer.parseInt(it) }
- .let { it -> it + 1 } ?: 0
- }
你可以这样做吗? 是的, 但并不那么简单. 上面的代码是错误的, 并从 parseInt()抛出 NPE.monadic 风格 map()仅在值存在时执行. 否则, null 就会通过. 这就是为什么 map()这么方便. 不幸的是, Kotlin's let 不会那样工作. 它只是从左侧的所有内容中调用, 包括空值.
所以为了使这个代码无效, 你必须 let 在每个代码之前添加问号 "?":
- fun parseAndInc(number: String?): Int {
- return number?.let { Integer.parseInt(it) }
- ?.let { it -> it + 1 } ?: 0
- }
现在, 比较 Java 和 Kotlin 版本的可读性. 你更倾向哪个?
数据类
数据类 是 Kotlin 在实现值对象 Value Objects(又名 DTO)时减少 Java 中不可避免的僵化样板仪式的有效方法.
例如, 在 Kotlin 中, 你只写了一个 Value Object 很简单:
data class User(val name: String, val age: Int)
而 Kotlin 会产生良好的实施方法: equals(),hashCode(),toString(), 和 copy().
在实现简单的 DTO 时它非常有用. 但请记住, 数据类带有严重的局限性 - 它们是 final 的. 您无法扩展 Data 类或将其抽象化. 所以, 你可能不会在核心领域模型中使用它们.(banq 注: DDD 领域驱动设计中核心领域模型有值对象类型)
这个限制不是 Kotlin 的错. 在 equals()没有违反 Liskov 原则的情况下, 没有办法产生正确的基于价值的数据. 这就是为什么 Kotlin 不允许 Data 类继承的原因.
开放类
在 Kotlin 中, 类默认是 final 的. 如果你想扩展一个类, 你必须添加 open 修饰符.
继承语法如下所示:
- open class Base
- class Derived : Base()
Kotlin 将 extends 关键字更改为: 运算符, 该运算符已用于将变量名称与其类型分开. 回到 C ++ 语法? 对我来说这很混乱.
这里有争议的是, 默认情况下, class 是 final 的. 也许 Java 程序员会过度使用继承. 也许你应该在考虑继承类之前考虑三次. 但我们生活在框架世界, 框架爱 AOP.Spring 使用库 (cglib,jassist) 为您的 bean 生成动态代理. Hibernate 扩展你的实体以启用延迟加载.
如果你使用 Spring, 你有两种选择. 你可以把 open 放在所有 bean 类的前面(这很枯燥), 或者使用这个棘手的编译器插件:
- buildscript {
- dependencies {
- classpath group: 'org.jetbrains.kotlin', name: 'kotlin-allopen', version: "$versions.kotlin"
- }
- }
陡峭的学习曲线
如果你认为你可以快速学习 Kotlin, 因为你已经掌握了 Java - 那么你错了. kotlin 会让你陷入深渊. 事实上, Kotlin 的语法更接近 Scala. 这是全押下注. 你将不得不忘记 Java 并切换到完全不同的语言.
相反, 学习 Groovy 是一个愉快的旅程. Groovy 会牵着你的手. Java 代码是正确的 Groovy 代码, 因此您可以通过将文件扩展名从更改. java 为. groovy. 每次您学习新的 Groovy 功能时, 您都可以决定. 你喜欢它还是喜欢用 Java 方式? 棒极了.
最后的想法
学习新技术就像投资. 我们投入时间, 然后从技术上得到应该得到回报. 我不是说 Kotlin 是一种糟糕的语言. 我只是说在我们的案例中, 成本超过了收益.
请不要太认真, 但我希望你会喜欢它.
来源: http://www.jianshu.com/p/68b3e91fea63