點(diǎn)擊上方“Java技術(shù)江湖”,選擇“設(shè)為星標(biāo)”
回復(fù)”666“獲取全網(wǎng)最熱的Java核心知識點(diǎn)整理

作者:Bartosz Walacik | 來自:CSDN | 編輯 :可可原文:https://allegro.tech/2018/05/From-Java-to-Kotlin-and-Back-Again.html
毫無疑問,Kotlin 在去年很受歡迎,業(yè)界甚至有人認(rèn)為其將取代 Java 的霸主地位。它提供了 Null 安全性,從這一點(diǎn)來說它確實(shí)比 Java 更好。那么是不是這就意味著開發(fā)者應(yīng)該毫不猶豫地?fù)肀?Kotlin,否則就落伍了?在開始使用 Kotlin 編程之前,本文想要分享個故事給你。在這個故事中,作者最早使用 Kotlin 來編寫一個項(xiàng)目,后來 Kotlin 的各種怪異模式以及一些其他障礙越來越讓人厭煩,最終,他們決定重寫這個項(xiàng)目。
一直以來,我對基于 JVM 的語言都非常情有獨(dú)鐘。我通常會用 Java 來編寫主程序,再用 Groovy 編寫測試代碼,兩者配合使用得心應(yīng)手。
2017年夏天,團(tuán)隊發(fā)起了一個新的微服務(wù)項(xiàng)目,和往常一樣,我們需要對編程語言和技術(shù)進(jìn)行選型。部分團(tuán)隊成員是 Kotlin 的擁護(hù)者,再加上我們都想嘗試一下新的東西,于是我們決定用 Kotlin 來開發(fā)這個項(xiàng)目。由于 Spock 測試框架不支持 Kotlin,因此我們決定堅持使用 Groovy 來測試。2018年春天,使用 Kotlin 開發(fā)幾個月之后,我們總結(jié)了 Kotlin 的優(yōu)缺點(diǎn),最終結(jié)論表明 Kotlin 降低了我們的生產(chǎn)力。于是我們使用 Java 來重寫這個微服務(wù)項(xiàng)目。那么 Kotlin 主要存在哪些弊端?下面來一一解釋。這是 Kotlin 最讓我震驚的地方??纯聪旅孢@個方法:
fun inc(num : Int) {
val num = 2
if (num > 0) {
val num = 3
}
println ("num: " + num)
}
當(dāng)你調(diào)用 inc(1) 會輸出什么呢?在 Kotlin 中, 方法的參數(shù)無法修改,因此在本例中你不能改變 num。這個設(shè)計很好,因?yàn)槟悴粦?yīng)該改變方法的輸入?yún)?shù)。但是你可以用相同的名稱定義另一個變量并對其進(jìn)行初始化。這樣一來,這個方法作用域中就有兩個名為 num 的變量。當(dāng)然,你一次只能訪問其中一個 num,但是 num 值會被改變。在 if 語句中再添加另一個 num,因?yàn)樽饔糜虻脑?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 發(fā)明的,這在編程語言中很常見。在 Java 中我們習(xí)慣用方法參數(shù)來映射類字段:
public class Shadow {
int val;
public Shadow(int val) {
this.val = val;
}
}
在 Kotlin 中名稱遮蔽有些嚴(yán)重,這是 Kotlin 團(tuán)隊的一個設(shè)計缺陷。IDEA 團(tuán)隊試圖通過向每個遮蔽變量顯示警告信息來解決這個問題。兩個團(tuán)隊在同一家公司工作,或許他們可以互相交流并就遮蔽問題達(dá)成共識。我從個人角度贊成 IDEA 的做法因?yàn)槲蚁氩坏接心男?yīng)用場景需要遮蔽方法參數(shù)。在Kotlin中,當(dāng)你聲明一個var或是val,你通常會讓編譯器從右邊的表達(dá)式類型中猜測變量類型。我們稱之為局部變量類型推斷,這對程序員來說是一個很大的改進(jìn)。它允許我們在不影響靜態(tài)類型檢查的情況下簡化代碼。
Java 同樣具備這個特性,Java 10中的類型推斷示例如下:
實(shí)話實(shí)說,Kotlin 在這一點(diǎn)上確實(shí)更勝一籌。當(dāng)然,類型推斷還可應(yīng)用在多個場景。關(guān)于 Java 10中的局部變量類型推斷,點(diǎn)擊以下鏈接了解更多:Null 安全類型是 Kotlin 的殺手級功能。這個想法很好,在 Kotlin 中,類型默認(rèn)不可為空。如果你需要添加一個可為空的類型,可以像下列代碼這樣:
val a: String? = null // ok
val b: String = null // compilation error
假設(shè)你使用了可為空的變量但是并未進(jìn)行空值檢查,這在 Kotlin 將無法通過編譯,比如:
println (a.length) // compilation error
println (a?.length) // fine, prints null
println (a?.length ?: 0) // fine, prints 0
那么是不是如果你同時擁有不可為空和可為空的變量,就可以避免 Java 中最常見的 NullPointerException 異常嗎?事實(shí)并沒有想象的簡單。當(dāng) Kotlin 代碼必須調(diào)用 Java 代碼時,事情會變得很糟糕,比如庫是用 Java 編寫的,我相信這種情況很常見。于是第三種類型產(chǎn)生了,它被稱為平臺類型。Kotlin 無法表示這種奇怪的類型,它只能從 Java 類型推斷出來。它可能會誤導(dǎo)你,因?yàn)樗鼘罩岛軐捤?,并且會禁?Kotlin 的 NULL 安全機(jī)制。
public class Utils {
static String format(String text) {
return text.isEmpty() ? null : text;
}
}
假如你想調(diào)用 format(String)。應(yīng)該使用哪種類型來獲得這個 Java 方法的結(jié)果呢?你有三個選擇。第一種方法:你可以使用 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)
}
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 中一樣。除此之外,還有另外一個方法,就是強(qiáng)制將 f 類型推斷為 String:
fun doSth(text: String) {
val f = Utils.format(text)!! // throws NPE when format() returns null
println ("f.len : " + f.length)
}
在我看來,Kotlin 的所有這些類似 scala 的類型系統(tǒng)過于復(fù)雜。Java 互操作性似乎損害了 Kotlin 類型推斷這個重量級功能。使用類似 Log4j 或者 Gson 的 Java 庫時,類文字很常見。
Gson gson = new GsonBuilder().registerTypeAdapter(LocalDate.class, new LocalDateAdapter()).create();
Groovy 把類進(jìn)行了進(jìn)一步的簡化。你可以忽略 .class,它是 Groovy 或者 Java 類并不重要。
def gson = new GsonBuilder().registerTypeAdapter(LocalDate, new LocalDateAdapter()).create()
Kotlin 把 Kotlin 類和 Java 類進(jìn)行了區(qū)分,并為其提供了語法規(guī)范:
val kotlinClass : KClass<LocalDate> = LocalDate::class
val javaClass : Class<LocalDate> = LocalDate::class.java
val gson = GsonBuilder().registerTypeAdapter(LocalDate::class.java, LocalDateAdapter()).create()
C 系列的編程語言有標(biāo)準(zhǔn)的聲明類型的方法。簡而言之,首先指定一個類型,然后是該符合類型的東西,比如變量、字段、方法等等。
int inc(int i) {
return i + 1;
}
fun inc(i: Int): Int {
return i + 1
}
首先,你需要在名稱和類型之間加入這個多余的冒號。這個額外角色的目的是什么?為什么名稱與其類型要分離?我不知道。可悲的是,這讓你在 Kotlin 的工作變得更加困難。第二個問題,當(dāng)你讀取一個方法聲明時,你首先看到的是名字和返回類型,然后才是參數(shù)。在 Kotlin 中,方法的返回類型可能遠(yuǎn)在行尾,所以需要瀏覽很多代碼才能看到:
private fun getMetricValue(kafkaTemplate : KafkaTemplate<String, ByteArray>, metricName : String) : Double {
...
}
或者,如果參數(shù)是逐行格式的,則需要搜索。那么我們需要多少時間才能找到此方法的返回類型呢?
@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 中的自動化支持不夠好。標(biāo)準(zhǔn)做法從類型名稱開始,并且很容易找到類型。一旦選擇一個類型,IDE 會提供一些關(guān)于變量名的建議,這些變量名是從選定的類型派生的,因此你可以快速輸入這樣的變量:
MongoExperimentsRepository repository
Kotlin 盡管有 IntelliJ 這樣強(qiáng)大的 IDE,輸入變量仍然是很難的。如果你有多個存儲庫,在列表中很難實(shí)現(xiàn)正確的自動補(bǔ)全,這意味著你不得不手動輸入完整的變量名稱。
repository : MongoExperimentsRepository
“嗨,Kotlin。我是新來的,我可以使用靜態(tài)成員嗎?"他問。 “不行。我是面向?qū)ο蟮模o態(tài)成員不是面向?qū)ο蟮?。?Kotlin 回答。 “好吧,但我需要 MyClass 的 logger,我該怎么辦?” “那是什么東西?” “這是局限到你的類的單獨(dú)對象。把你的 logger 放在伴生對象中?!盞otlin解釋說。
class MyClass {
companion object {
val logger = LoggerFactory.getLogger(MyClass::class.java)
}
}
“很詳細(xì)的語法,”程序員看起來很疑惑,“但是沒關(guān)系,現(xiàn)在我可以像 MyClass.logger 這樣調(diào)用我的 logger,就像 Java 中的一個靜態(tài)成員?” “嗯......是的,但它不是靜態(tài)成員!這里只有對象。把它看作是已經(jīng)實(shí)例化為單例的匿名內(nèi)部類。事實(shí)上,這個類并不是匿名的,它的名字是 Companion,但你可以省略這個名字??吹搅藛??這很簡單。"
我很欣賞對象聲明的概念——單例很有用。但從語言中刪除靜態(tài)成員是不切實(shí)際的。在 Java 中我們使用靜態(tài) Logger 很經(jīng)典,它只是一個 Logger,所以我們不關(guān)心面向?qū)ο蟮募兌?。它能夠工作,從來沒有任何壞處。因?yàn)橛袝r候你必須使用靜態(tài)。舊版本 public static void main() 仍然是啟動 Java 應(yīng)用程序的唯一方式。
class AppRunner {
companion object {
@JvmStatic fun main(args: Array<String>) {
SpringApplication.run(AppRunner::class.java, *args)
}
}
}
import java.util.Arrays;
...
List<String> strings = Arrays.asList("Saab", "Volvo");
import com.google.common.collect.ImmutableMap;
...
Map<String, String> string = ImmutableMap.of("firstName", "John", "lastName", "Doe");
在 Java 中,我們?nèi)匀辉诘却碌恼Z法來表達(dá)集合和映射。語法在許多語言中非常自然和方便。
const list = ['Saab', 'Volvo']
const map = {'firstName': 'John', 'lastName' : 'Doe'}
list = ['Saab', 'Volvo']
map = {'firstName': 'John', 'lastName': 'Doe'}
def list = ['Saab', 'Volvo']
def map = ['firstName': 'John', 'lastName': 'Doe']
簡單來說,集合字面量的整齊語法就是你對現(xiàn)代編程語言的期望,特別是如果它是從頭開始創(chuàng)建的。Kotlin 提供了一系列內(nèi)置函數(shù),比如 listOf()、mutableListOf()、mapOf()、hashMapOf() 等等。
val list = listOf("Saab", "Volvo")
val map = mapOf("firstName" to "John", "lastName" to "Doe")
在地圖中,鍵和值與 to 運(yùn)算符配對,這很好。但為什么一直沒有得到廣泛使用呢?令人失望。函數(shù)式語言(比如 Haskell)沒有空值。相反,他們提供 Maybe monad(如果你不熟悉monad,請閱讀 Tomasz Nurkiewicz 的這篇文章:http://www.nurkiewicz.com/2016/06/functor-and-monad-examples-in-plain-java.html)。Maybe 很久以前就被 Scala 以 Option 引入到 JVM 世界,然后在 Java 8 中被采用為 Optional。如今,Optional 是在 API 邊界處理返回類型中的空值的非常流行的方式。Kotlin 中沒有 Optional 的等價物,所以你大概應(yīng)該使用 Kotlin 的可空類型。讓我們來調(diào)查一下這個問題。通常情況下,當(dāng)你有一個 Optional 的時候,你想要應(yīng)用一系列無效的轉(zhuǎn)換。
public int parseAndInc(String number) {
return Optional.ofNullable(number)
.map(Integer::parseInt)
.map(it -> it + 1)
.orElse(0);
}
在 Kotlin 中,為了映射你可以使用 let 函數(shù):
fun parseAndInc(number: String?): Int {
return number.let { Integer.parseInt(it) }
.let { it -> it + 1 } ?: 0
}
上面的代碼是錯誤的,parseInt() 會拋出 NPE 。map() 僅在有值時執(zhí)行。否則,Null 就會跳過,這就是為什么 map() 如此方便。不幸的是,Kotlin 的 let 不會那樣工作。它從左側(cè)的所有內(nèi)容中調(diào)用,包括空值。為了保證這個代碼 Null 安全,你必須在每個代碼之前添加 let:
fun parseAndInc(number: String?): Int {
return number?.let { Integer.parseInt(it) }
?.let { it -> it + 1 } ?: 0
}
現(xiàn)在,比較 Java 和 Kotlin 版本的可讀性。你更傾向哪個?數(shù)據(jù)類是 Kotlin 在實(shí)現(xiàn) Value Objects 時使用的方法,以減少 Java 中不可避免的樣板問題。例如,在 Kotlin 中,你只寫一個 Value Object :
data class User(val name: String, val age: Int)
Kotlin 對 equals()、hashCode()、toString() 以及 copy() 有很好的實(shí)現(xiàn)。在實(shí)現(xiàn)簡單的DTO 時它非常有用。但請記住,數(shù)據(jù)類帶有嚴(yán)重的局限性。你無法擴(kuò)展數(shù)據(jù)類或者將其抽象化,所以你可能不會在核心模型中使用它們。這個限制不是 Kotlin 的錯。在 equals() 沒有違反 Liskov 原則的情況下,沒有辦法產(chǎn)生正確的基于價值的數(shù)據(jù)。這也是為什么 Kotlin 不允許數(shù)據(jù)類繼承的原因。Kotlin 類默認(rèn)為 final。如果你想擴(kuò)展一個類,必須添加 open 修飾符。
open class Base
class Derived : Base()
Kotlin 將 extends 關(guān)鍵字更改為: 運(yùn)算符,該運(yùn)算符用于將變量名稱與其類型分開。那么再回到 C ++語法?對我來說這很混亂。這里有爭議的是,默認(rèn)情況下類是 final。也許 Java 程序員過度使用繼承,也許應(yīng)該在考慮擴(kuò)展類之前考慮三次。但我們生活在框架世界,Spring 使用 cglib、jassist 庫為你的 bean 生成動態(tài)代理。Hibernate 擴(kuò)展你的實(shí)體以啟用延遲加載。如果你使用 Spring,你有兩種選擇。你可以在所有 bean 類的前面添加 open,或者使用這個編譯器插件:
buildscript {
dependencies {
classpath group: 'org.jetbrains.kotlin', name: 'kotlin-allopen', version: "$versions.kotlin"
}
}
如果你認(rèn)為自己有 Java 基礎(chǔ)就可以快速學(xué)習(xí) Kotlin,那你就錯了。Kotlin 會讓你陷入深淵,事實(shí)上,Kotlin 的語法更接近 Scala。這是一項(xiàng)賭注,你將不得不忘記 Java 并切換到完全不同的語言。相反,學(xué)習(xí) Groovy 是一個愉快的過程。Java 代碼是正確的 Groovy 代碼,因此你可以通過將文件擴(kuò)展名從 .java 更改為 .groovy。學(xué)習(xí)新技術(shù)就像一項(xiàng)投資。我們投入時間,新技術(shù)讓我們得到回報。但我并不是說 Kotlin 是一種糟糕的語言,只是在我們的案例中,成本遠(yuǎn)超收益。以上內(nèi)容編譯自 From Java to Kotlin and Back Again,作者 Kotlin ketckup。他是一名具有15年以上專業(yè)經(jīng)驗(yàn)的軟件工程師,專注于JVM 。在 Allegro,他是一名開發(fā)團(tuán)隊負(fù)責(zé)人,JaVers 項(xiàng)目負(fù)責(zé)人,Spock 倡導(dǎo)者。此外,他還是 allegro.tech/blog 的主編。
本文一出就引發(fā)了業(yè)內(nèi)的廣泛爭議,Kotlin 語言擁護(hù)者 Márton Braun 就表示了強(qiáng)烈的反對。Márton Braun 十分喜歡 Kotlin 編程,目前他在 StackOverflow 上 Kotlin 標(biāo)簽的最高用戶列表中排名第三,并且是兩個開源 Kotlin 庫的創(chuàng)建者,最著名的是 MaterialDrawerKt。此外他還是 Autosoft 的 Android 開發(fā)人員,目前正在布達(dá)佩斯技術(shù)經(jīng)濟(jì)大學(xué)攻讀計算機(jī)工程碩士學(xué)位。當(dāng)我第一次看到這篇文章時,我就想把它轉(zhuǎn)發(fā)出來看看大家會怎么想,我肯定它會是一個有爭議的話題。后來我讀了這篇文章,果然證明了它是一種主觀的、不真實(shí)的、甚至有些居高臨下的偏見。有些人已經(jīng)在原貼下進(jìn)行了合理的批評,對此我也想表達(dá)一下自己的看法。“IDEA 團(tuán)隊”(或者 Kotlin 插件團(tuán)隊)和“Kotlin 團(tuán)隊”肯定是同樣的人,我從不認(rèn)為內(nèi)部沖突會是個好事。語言提供這個功能給你,你需要的話就使用,如果討厭,調(diào)整檢查設(shè)置就是了。Kotlin 的類型推斷無處不在,作者說的 Java 10 同樣可以簡直是在開玩笑。Kotlin 的方式超越了推斷局部變量類型或返回表達(dá)式體的函數(shù)類型。這里介紹的這兩個例子是那些剛剛看過關(guān)于 Kotlin 的第一次介紹性講話的人會提到的,而不是那些花了半年學(xué)習(xí)該語言的人。例如,你怎么能不提 Kotlin 推斷泛型類型參數(shù)的方式?這不是 Kotlin 的一次性功能,它深深融入了整個語言。這個批評是對的,當(dāng)你與 Java 代碼進(jìn)行互操作時,Null 安全性確實(shí)被破壞了。該語言背后的團(tuán)隊曾多次聲明,他們最初試圖使 Java 可為空的每種類型,但他們發(fā)現(xiàn)它實(shí)際上讓代碼變得更糟糕。Kotlin 不比 Java 更差,你只需要注意使用給定庫的方式,就像在 Java 中使用它一樣,因?yàn)樗]有不去考慮 Null 安全。如果 Java 庫關(guān)心 Null 安全性,則它們會有許多支持注釋可供添加。也許可以添加一個編譯器標(biāo)志,使每種 Java 類型都可以為空,但這對 Kotlin 團(tuán)隊來說不得不花費(fèi)大量額外資源。:: class 為你提供了一個 KClass 實(shí)例,以便與 Kotlin 自己的反射 API 一起使用,而:: class.java為你提供了用于 Java 反射的常規(guī) Java 類實(shí)例。為了清楚起見,顛倒的順序是存在的,這樣你就可以以合理的方式省略顯式類型。冒號只是語法,這在現(xiàn)代語言中是相當(dāng)普遍的一種,比如 Scala、Swift 等。我不知道作者在使用什么 IntelliJ,但我使用的變量名稱和類型都能夠自動補(bǔ)全。對于參數(shù),IntelliJ 甚至?xí)o你提供相同類型的名稱和類型的建議,這實(shí)際上比 Java 更好。有時候你必須使用靜態(tài)。舊版本 public static void main() 仍然是啟動 Java 應(yīng)用程序的唯一方式。
class AppRunner {
companion object {
@JvmStatic fun main(args: Array<String>) {
SpringApplication.run(AppRunner::class.java, *args)
}
}
}
實(shí)際上,這不是啟動 Java 應(yīng)用程序的唯一方式。你可以這樣做:
fun main(args:Array <String>){ SpringApplication.run(AppRunner :: class.java,* args)}
fun main(args:Array <String>){ runApplication <AppRunner>(* args)}
你可以在注釋中使用數(shù)組文字。但是,除此之外,這些集合工廠的功能非常簡潔,而且它們是另一種“內(nèi)置”到該語言的東西,而它們實(shí)際上只是庫函數(shù)。你只是抱怨使用:進(jìn)行類型聲明。而且,為了獲得它不必是單獨(dú)的語言結(jié)構(gòu)的好處,它只是一個任何人都可以實(shí)現(xiàn)的功能。如果你喜歡 Optional ,你可以使用它。Kotlin 在 JVM 上運(yùn)行。
對于代碼確實(shí)這有些難看。但是你不應(yīng)該在 Kotlin 代碼中使用 parseInt,而應(yīng)該這樣做(我不知道你使用該語言的 6 個月中為何錯過這個)。你為什么要明確地命名一個 Lambda 參數(shù)呢?這個限制不是 Kotlin 的錯。在 equals() 沒有違反 Liskov 原則的情況下,沒有辦法產(chǎn)生正確的基于價值的數(shù)據(jù)。這就是為什么 Kotlin 不允許數(shù)據(jù)類繼承的原因。
我不知道你為什么提出這個問題。如果你需要更復(fù)雜的類,你仍然可以創(chuàng)建它們并手動維護(hù)它們的 equals、hashCode 等方法。數(shù)據(jù)類僅僅是一個簡單用例的便捷方式,對于很多人來說這很常見。作者認(rèn)為學(xué)習(xí) Kotlin 很難, 但是我個人并不這么認(rèn)為。最近的語言排行,java依然排在第一,在語言排行榜上,Koltin已被甩到10名之外!關(guān)注公眾號【Java技術(shù)江湖】后回復(fù)“PDF”即可領(lǐng)取200+頁的《Java工程師面試指南》
強(qiáng)烈推薦,幾乎涵蓋所有Java工程師必知必會的知識點(diǎn),不管是復(fù)習(xí)還是面試,都很實(shí)用。
