社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
点击上方“CSDN”,选择“置顶公众号”
关键时刻,第一时间送达!
毫无疑问,Kotlin 目前很受欢迎,业界甚至有人认为其将取代 Java 的霸主地位。它提供了 Null 安全性,从这一点来说它确实比 Java 更好。那么是不是这就意味着开发者应该毫不犹豫地拥抱 Kotlin,否则就落伍了?
等等,或许事情并非如此。
在开始使用 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.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
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<String, String> 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
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!