從Java到Kotlin,然後又回到Java!

banq發表於2018-05-26
最近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)
}
<p class="indent">


當你呼叫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);
}
<p class="indent">

名字遮蔽不是Kotlin發明的。這在程式語言中很常見。在Java中,我們習慣用方法引數來對映類欄位:

public class Shadow {
    int val;

    public Shadow(int val) {
        this.val = val;
    }
}
<p class="indent">

在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;
    }
}
<p class="indent">


現在,你想要從Kotlin呼叫format(String) 。您應該使用哪種型別來使用此Java方法的結果呢?你有三個選擇。

第一種方法。你可以使用String,程式碼看起來很安全,但會丟擲NPE。

fun doSth(text: String) {
    val f: String = Utils.format(text)       // 會透過編譯但是執行時會拋NPE
    println ("f.len : " + f.length)
}
<p class="indent">


你需要用Elvis來解決它:

fun doSth(text: String) {
    val f: String = Utils.format(text) ?: ""  // safe with Elvis
    println ("f.len : " + f.length)
}
<p class="indent">


第二種方法。你可以使用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
}
<p class="indent">


第三種方法,如果你只是讓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
}
<p class="indent">

這是餿主意。這個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();
<p class="indent">


在Groovy中,類字面量被簡化到極點。你可以忽略.class ,它是Groovy或Java類並不重要。

def gson = new GsonBuilder().registerTypeAdapter(LocalDate, new LocalDateAdapter()).create()
<p class="indent">


Kotlin區分Kotlin和Java類,併為其提供語法規範:

val kotlinClass : KClass<LocalDate> = LocalDate::class
val javaClass : Class<LocalDate> = LocalDate::class.java
<p class="indent">


所以在Kotlin,你不得不寫下:

val gson = GsonBuilder().registerTypeAdapter(LocalDate::class.java, LocalDateAdapter()).create()
<p class="indent">


這是醜陋的。

反向型別宣告
在C語言系列程式語言中,我們有標準的宣告型別的方法。簡而言之,首先進引入一個型別,然後輸出一個型別的東西(變數,欄位,方法等)。

Java中的標準表示法:

int inc(int i) {
    return i + 1;
}
<p class="indent">


Kotlin中的反轉表達:

fun inc(i: Int): Int {
    return i + 1
}
<p class="indent">


這種方式有幾個原因令人討厭。

首先,您需要在名稱和型別之間鍵入並閱讀這個嘈雜的冒號。這個額外角色的目的是什麼?為什麼名稱與其型別分離?我不知道。可悲的是,這讓你在Kotlin中工作變得更加困難。

第二個問題。當你讀取一個方法宣告時,首先,你對名字和返回型別感興趣,然後你掃描引數。

在Kotlin中,方法的返回型別可能遠在行尾,所以需要滾動:

private fun getMetricValue(kafkaTemplate : KafkaTemplate<String, ByteArray>, metricName : String) : Double {
    ...
}
<p class="indent">


或者,如果引數是逐行格式的,則需要搜尋。您需要多少時間才能找到此方法的返回型別?

@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
    }
    ...
}
<p class="indent">


反轉符號的第三個問題是IDE的自動完成得不好。在標準符號中,您從型別名稱開始,並且很容易找到型別。一旦你選擇了一個型別,一個IDE會給你提供一些關於變數名的建議,這些變數名是從選定的型別派生的 所以你可以快速輸入這樣的變數:

MongoExperimentsRepository repository
<p class="indent">


在Kotlin中輸入這個變數在IntelliJ中是很難的,而這是有史以來最偉大的IDE。如果您有多個儲存庫,則在自動完成列表中找不到正確的選項。這意味著必須使用手工輸入完整的變數名稱。

repository : MongoExperimentsRepository
<p class="indent">


待續...

[該貼被admin於2018-05-26 21:20修改過]

[該貼被admin於2018-05-28 16:43修改過]

相關文章