From J to K

smallufo
14 min readDec 31, 2017

這一個多月來(since 2017-Dec),我大量把我之前用 Java 所寫的核心library轉換成Kotlin,一方面是學習 Kotlin,另一方面是看到 Kotlin 的未來,覺得應該是可以大膽投資的語言。尤其自Google 宣布Kotlin 為 1st class supported language 之後,更讓其聲勢大漲,在觀察幾個月之後,便決定踏入此坑,大膽把之前所撰寫的business objects、libraries、services 逐漸轉換成Kotlin。包含 Spring、DAO、rxJava、stream…等,均逐漸改以 Kotlin-style。以下就是這一個多月來的一點心得筆記。

Type Declaration

首先提及的是變數的宣告,從 Java 轉換的歷程中,第一個遇到不習慣的,就是變數的 Type 宣告於名稱後方。從以往的

int a = 10;

變成

var a:Int = 10
甚至是
val a = 10

這種寫法雖然不是首創,但對於習慣 Java 語法的我,一開始仍不太習慣。雖然之前 Scala 也是這樣寫,但畢竟我算是 Scala 的逃兵,所以一直沒養成 Type 在後方的習慣。

至於這種寫法有什麼好處?一開始的確不習慣。記得N年前 Gavin King (Ceylon、Hibernate 發明者)也曾批判如此寫法,意思是無法一目了然該 Type,會對後續 maintainer 造成心理負擔等等。我當時的確很贊同該論點。

但其實對於真正在處理資料流的程式型態而言,大部分都是把資料結構與底層library或網路API不斷地 mapflatMap 去,有時候對我而言,剛開始要轉換資料時,還不是很確定自己到底需要的是什麼 type,而把 Type 寫在後面甚至省略其定義,由 compiler 自動 infer 出來,的確省了不少心理負擔。若真正需要確認最終 type,可由 IntelliJ 的 Specify Type Explicitly 的幫助而寫於 code 中。

這邊所提的「心理負擔」就是類似在 Java 中,已經把最終需要的 Type 「烙」在最前面,我們在資料流的轉換過程中,內心就會不斷地朝該type前進,否則compile 也過不了是其次,惱人的是編寫過程中IDE會不斷出現警告,很干擾思索。但其實大多數時刻,最終的 type (不論是 local type 或是 function return type)是寫到後面自己才會明瞭的,若不需要把 type 預先「烙印」下來,就可以編寫邊測,內心負擔會省去許多。

而其實 Type 寫於前面其實並不是沒有優點,在 GUI 的環境中,Type寫於前方其實是遠比寫在後方容易理解。

Coroutine

雖然 coroutine 還是 experimental 階段,但我已經大量將其用於我的 service 當中。我並未用到 continuation 的功能,頂多就是將它用於大量併發的工作。

用VisualVM 觀察 Coroutine 到底在做什麼是很有趣的一件事。此圖我開兩組 ExecutorService 分別做不同的事,交給coroutine 作為 CoroutineDispatcher,然後觀察他們的行為,看著資料流這樣填滿性能也是挺滿足的。

以前我是用 RxJava(1 to 2)來做這些事,其實本質上沒什麼不同,畢竟底層都是委以 ExecoturService 來處理。所以若是您已經熟悉了 RxJava 或是 CompletableFuture,我相信跑起來餵以相同的 ExecutorService 效能不會有太大的差異,頂多就是 coding style (push vs pull) 的不同。只是對於我而言,整個 service 不用 depend on 另一套 framework 就是減輕一種負擔。

Coroutine 真正的強項在於 continuation,能將一段流程 suspend 晾到一旁,必要時再把它喚醒。這 suspend 的 function 會被 compile 成類似 state machine 的 code。蠻厲害的,但我沒碰這塊,以後有機會再玩。

Stream

以前從 Java7 轉換為 Java8 時,對於 Stream 簡直愛不釋手,可以把 list / set / map 轉成 stream 然後透過各種 map/flatMap 轉換,最後再由 terminal operator 轉回 collection,寫起來真的挺過癮的。

但是到了 Kotlin,它的 list/set 直接支援 map/flatMap,也不用 terminal operator 把資料流「搜集」回 collection,又帶給我們更大的自由!此時就會感嘆,當時 Java8 的 collection 為什麼不直接這樣設計呢?少了一層「心理負擔」對於寫程式而言是很大的釋放啊!

這又是怎樣的一個心理負擔?就是在撰寫 stream 的轉換時,心中都要注意到「目前是 stream,待會兒記得要用 terminator operator結尾」,以及「目前轉換成什麼 type 了?等會兒要 collect 成什麼?尤其對於 Collectors.toMap 這個operator:

public static <T,K,U,M extends Map<K,U>> Collector<T,?,M> toMap(
Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier
)

每次要 collect Map 總是膽戰心驚,讓人瞻前顧後,目前到底是什麼 type、我這樣寫的 supplier 到底對不對…,或是去翻之前的 code,才能寫出正確的 toMap。這對於程式設計師的心理負擔是很大的。

而相對於 kotlin,可以直接在 collection 裡面toMaptoSetsort,其 type 也因為不用事先定義,可以寫到哪裡測到哪裡,或是由 IntelliJ 幫忙確認,其輕鬆程度不可同日而語!

以下舉個例子:假設有一個「圖書館Service」,裡面有一個 method findBooks,負責找出「在某範圍日期內,單一使用者的借書紀錄」,可能會設計如下:

而如今若要加入一個功能:列出一群使用者,在某範圍日期內的所有借書紀錄,按照日期排序,在不更動原有 findBooks 的前提下,大概可以寫出如下的這個 default method:

Write Once , Read Never

老實講,這種 code 寫過一次,大概就不會想再review。這裡先不討論在此使用 Tuple2 / Tuple3 的優缺,但在撰寫這類 code 時,的確心理壓力是不小,而且因為 map 的轉換太過複雜,我還定義了一個中間的 variable : map (line 6),其實是可以省略,但是再 review 起來就會更複雜。

如果把以上的 code 直接用 IntelliJ 轉換成 Kotlin,會得到如下的結果:

WTF

這是一段非常 WTF 的 code,而且還不能 run,還得仔細調整半天才能 run,還會遇到 Kotlin 的 Function 與 java.util.function.Function 不相容的麻煩問題。

而我們真正需要的是什麼?就是這樣而已:

簡潔、明瞭,省去太多心理負擔,寫起來輕鬆多了!

由此可看出一個重點:如果您的code包含了 stream 的操作,不要期待 IntelliJ 可以成功轉換,大部分都會失敗的。如果真的轉換成功,也建議您仔細 review code,建議把 stream 的 dependency 全部移除,或許會讓您的程式碼乾淨又清爽!

Tuple (Pair/Triple)

我在 coding java 時,非常倚賴 tuple 這種資料結構,對於懶得定義零碎的 Business Object,只是在 library 中傳來傳去的 objects,我常用 tuple 來取代。當時 survey 過,設計得最精巧的 tuple library 來自 jOOλ 的 Tuple ,其功能比 Apache 的 Pair/Triple 強大得多,包含許多方便的運算子。不過到了 Kotlin 卻只有提供 Pair/Triple 這兩組 Tuple,這是我個人認為非常不足之處。您 Function 都提供到 Function22 了,怎麼 Tuple 就這麼小氣!

當然,JetBrains 有他們的考量,例如未來 Project Valhalla 的 Value Type 推出後,類似這種「小物件」就沒有存在的必要。但取名 Pair/Triple 我個人認為不及 Tuple2/Tuple3 直覺,對於 code formatting 也沒有比較漂亮。此為一個小遺憾。

let

let 是一個非常方便的 extension function,它有一個很重要的功能:避免coding的思緒被打亂!

舉例而言,假設有一個 function :

fun adder(a:Int , b:Int): Int

今天我們要測試這個 function

adder(1,2)

此時寫到尾端了,我們才會想到:「啊!要在前面加上 assertEquals 」包裝起來,變成 assertEquals(3,adder(1,2)) , 為此,必須要將游標移至前方,把整段 code 包覆起來。要不然就是要另外定義一個變數:

int result = adder(1,2);assertEquals(3,result);

這些移動游標、或是建立一個中介變數的行為,對於寫程式而言,都是一種思緒的中斷。而 let 剛好可以解救我們的頭腦,讓我們可以直接coding 下去:

adder(1,2).let {  assertEquals(3,it)}getUserFromDb(1).let {   assertNotNull(it)  assertEquals("admin" , it!!.name)
}

不過上文的 null safety smart cast 有點破功,即使我使用的是 kotlin.test的套件,Kotlin 仍無法判別此時 it 一定不為 null,必須要強加 !! 才可以過關。這問題有人提出來了(KT-7566),會在 1.3 解決,就拭目以待吧。

let 可以直接轉換物件,有點類似 map 的效果,但畢竟不像 Java8 的 Optional 是個 monad還可以 flatMap

Optional

既然談到了 Optional,就來聊聊 Kotlin 的 null safety。

在當年把 code 轉換成 Java8 時,覺得 Optional 真是好物啊。可以直接轉換 nullable 的物件,省略 null check,真的是很方便。於是便把所有標示為 @Nullable 的 method 轉成 Optional<T>。但畢竟 Optional 並沒有 enforce null return,也就是說,即使宣告回傳 Optional<T> 而硬要 return null 其實是合法的。

而 Kotlin 的 nullable type 直接在 compiler 端強制做 null 檢查,釜底抽薪,徹底解決這個 Billion Dollar Mistake …(呃,幾乎啦,只要不 call Java 的話),對於程式的 integrity、robustness 有非常大的幫助。(於是我又把所有 Optional 改為 nullable 了)。

但我認為 Kotlin 缺少類似 swift 的 guard 機制,如果對於多個 nullable 物件,要確認他們均不為 null 再做某件事,程式難免會寫出許多 if null check,會讓 code 繁瑣,詳情可以參考此討論 Kotlin null check for multiple nullable vars

val , immutable

寫 Kotlin 寫到後來會有一種想法,只要是code 間看到 var 或是 mutableMapOfmutableSetOfmutableListOf ,都會要仔細想想真的是否有此必要?於是就會不斷思索有哪些方法可以把這些 mutable 資料結構取代掉,例如 while 去對某 mutable 資料結構去做更新,就可以用 generateSequence 去取代掉。

舉例而言

上例中,有一個「取得下一個滿月」的 getNextFullMoon 方法,以及「取得全年滿月時刻」 getYearlyFullMoons 的方法,傳統用 Java 寫大概會寫成以上的 code。

而該段 code 轉成 Kotlin style 便會呈現如下

其實這也沒什麼錯,但是看到 code 中出現了 mutableListOf 以及 var,我就覺得礙眼、不太對勁。該段 code 可以改寫如下:

以上只是其中一例,其實幾乎所有的 mutable 資料結構如果仔細思考,都可以找到functional、immutable 的 solution。

當看到從 Java 被轉換過來的 Kotlin code 再想辦法去除 var 或是 mutable 資料結構而程式碼少掉一大半時,內心真的是有一種滿足感!或許這就是另一種偏執狂(functional immutable paranoid)吧。注意:改用這種寫法效能不一定比較高!若是真正效能導向的code,建議還是兩者都測測看再決定吧!(真正寫 FP的高手一定笑說:這樣就自稱 paranoid?還沒碰 Higher Kind、Category Theory 咧)

Multiplatform , Kotlin is Ambitious

一個小結,我這半年觀察到的 JetBrains 對於 Kotlin 非常有野心,也非常謹慎。Kotlin 非常謹慎地權衡各種語言功能,並且適當地加入其中。其語法並非只是 syntax sugar而已,若是觀察其 bytecode,會發現其中多了許多 annotation、meta data,好像是在Java語言中打上各種補丁。

跨平台開發將是 Kotlin 下一個非常大的戰場,從 server到 client(Android or iOS)都可以 share 同樣的 codebase!這對於我是一個極大的誘因。我也是看到 1.2 支援 multiplatform 才毅然決定 migrate 到 Kotlin。

給後續開發者的一點建議:

如果您認為您所開發的 business objects、logics 很有可能跨平台,那麼,您的 code 儘量使用 Kotlin Standard Library (kotlin-stdlib),「不要」出現任何 java.util.* 或是其他第三方「非純Kotlin」的 library。以往您愛用的Optionalslf4jlog4japache.commons.[io、lang、text、httpclient、net…] ,以及 jOOλ 的 Tuple (若您跟我一樣是其愛好者)等,通通不要用!因為他們並非純 Kotlin。只有純 Kotlin 才有跨平台的機會!

最可惜的莫過於 java.time整套 package,這是一套設計相當精良的時間處理 library,其 LocalDateLocalDateTimeInstantZoneIdDuration …等,若你想要跨平台,這些都不能用!

我強調的是「跨平台」,若是您認為您的這段 code只會在 server 上跑,那當然隨您 import。這牽扯整個系統架構,通常是 DTO 才有必要做此考量。

其實我認為 Kotlin 正在嘗試遮蔽(eclipse) Java,它提供了相當方便的語法以及資料結構,他希望大家直接使用其 stdlib ,由他們去串接、翻譯成各平台的 bytecode 或 native code,當然這對他們有極大的利益(賣 IDE)。但許多細節就看他們做得夠不夠好,例如,未來或許會推出對應到 java.time 的 stdlib,若設計得不夠好,難免會被人詬病。而到目前為止,他們做得相當不錯!

讓我們拭目以待吧!

Kotlin Happy New Year 2018

--

--