當前位置:主頁 > SEO教程培訓 > /正文

我們與Kotlin的故事:從嘗試到放棄

作者:風力刷百度指數 ???時間:2018-08-24 15:32


Kotlin 現在很流行,它提供了編譯時 null 安全,代碼更加簡潔。它比 Java 更好,你應該切換到 Kotlin,否則就只能坐以待斃。不過,在轉向 Kotlin 之前,請先聽聽這個故事在這個故事里,那些稀奇古怪的東西讓我們忍無可忍,最后不得不使用 Java 重寫整個項目

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,不過你可以忽略這個名字。是不是很簡單?“


我們與Kotlin的故事:從嘗試到放棄


 

通過單例來聲明對象的做法很管用,但是從語言中移除靜態成員是不切實際的。在 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 是一種糟糕的語言,但在我們的案例中,成本超過了收益。

本文地址:http://www.pwkzxw.tw/peixun/1283.html

上一篇:行業門戶網站SEO操作規范大綱
下一篇:【搜索引擎優化】解答一個老網站為什么一直沒有排名

相關推薦
Tags:
刷指數

最新文章



刷百度指數 聯系我們
  • 咨詢電話:18927460947
  • 客服QQ:208777028

  • 掃一掃關注我們的微信號

    刷百度指數二維碼
    ? 杀平特一肖公式规律