您的位置: 百度SEO > 【SEO优化】-我们与Kotlin的故事:从尝试到放弃
百度SEO

【SEO优化】-我们与Kotlin的故事:从尝试到放弃

SEO实战SEO培训SEO优化

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      // ok

val b: String = null       // compilation error
 

如果使用不带空值检查的可空变量,将无法通过编译,例如:
 

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 安全。
 

比如下面的 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::class

val 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 {

    ...

}
 

或者,如果参数按照行进行了格式化,则可能需要通过搜索才能找到返回类型。你需要花多少时间才能找到此方法的返回类型?
 

@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
 

但即使是在 IntelliJ 中输入这个变量也是很费事的。如果你有多个 repository,则在自动完成列表中找不到正确的可选项,这意味需要手动输入完整的变量名。
 

repository : MongoExperimentsRepository

Companion 对象

一位 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 不熟悉。
 

在很早以前,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 Base

 

class 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 是一种糟糕的语言,但在我们的案例中,成本超过了收益。

{本文"我们与Kotlin的故事:从尝试到放弃" 责任编辑:小周seo顾问} SEO实战SEO培训SEO优化
百度SEO
上一篇:【SEO优化】-2018百度对医疗的态度,共同打击“仿
下一篇:【SEO优化】-网站权重值:靠SEO伪原创提升网站优化

您可能喜欢