1. 程式人生 > >寫給伺服器端Java開發人員的Kotlin簡介

寫給伺服器端Java開發人員的Kotlin簡介

Kotlin簡介

JetBrains有一個明確的目標:讓Kotlin成為一種多平臺語言,並提供100%的Java互操作性。Kotlin最近的成功和成熟水平為它進入伺服器端提供了一個很好的機會。

選擇Kotlin的理由

許多語言都試圖成為更好的Java。Kotlin在語言和生態系統方面做得都很好。成為更好的Java,同時又要保護JVM和巨大的庫空間,這是一場姍姍來遲的進化。這種方法與來自JetBrains和谷歌的支援相結合,使它成為一個真正的競爭者。讓我們來看看Kotlin帶來的一些特性。

型別推斷 —— 型別推斷是一等特性。Kotlin推斷變數的型別,而不需要顯式指定。在需要明確型別的情況下,也可以指定型別。

通過引入var關鍵字,Java 10也在朝著類似的方向發展。雖然表面看起來類似,但它的範圍僅限於區域性變數,不能用於欄位和方法簽名。

嚴格空檢查 —— Kotlin將可空程式碼流視為編譯時錯誤。它提供了額外的語法來處理空檢查。值得注意的是,它提供了鏈式呼叫中的NPE保護。

與Java互操作 —— Kotlin在這方面明顯優於其他JVM語言。它可以與Java無縫地互動。可以在Kotlin中匯入框架中的Java類並使用,反之亦然。值得注意的是,Kotlin集合可以與Java集合互操作。

不變性 —— Kotlin鼓勵使用不可變的資料結構。常用的資料結構(Set/ List/ Map

)是不可變的,除非顯式地宣告為可變的。變數也被指定為不可變(val)和可變(var)。所有這些變化對狀態可管理性的影響是顯而易見的。

簡潔而富有表達力的語法 —— Kotlin引入了許多改進,這些改進對程式碼的可讀性產生了重大影響。舉幾個例子:

  • 分號是可選的
  • 大括號在沒有用處的情況下是可選的
  • Getter/Setter是可選的
  • 一切都是物件——如果需要,在後臺自動使用原語
  • 表示式: 表示式求值時返回結果

在Kotlin中,所有的函式都是表示式,因為它們至少返回Unit 。控制流語句如if

trywhen(類似於switch)也是表示式。例如:

String result = null;

try {
    result = callFn();
} catch (Exception ex) {
    result = “”;
}

becomes:

val result = try {
    callFn()
} catch (ex: Exception) {
    “”
}

迴圈支援範圍,例如:

for (i in 1..100) { println(i) }

還有一些其他的改進,我們將繼續討論。 

把Kotlin引入Java專案

循序漸進

考慮到Java的互操作性,建議循序漸進地將Kotlin新增到現有的Java專案中。主流產品的支援專案通常是不錯的選擇。一旦團隊感到舒適了,他們就可以評估自己是否更喜歡完全切換。

選擇哪類專案好?

所有的Java專案都可以從Kotlin中獲益。但是,具有以下特徵的專案可以使決策更簡單。

包含大量DTO或模型/實體物件的專案 —— 這對於處理CRUD或資料轉換的專案非常典型。此類專案往往充斥著getter/setter。這裡可以利用Kotlin的屬性大幅簡化類。

大量依賴實用工具類的專案 —— Java中的實用工具類通常是為了彌補Java中頂級函式的缺乏。在許多情況下,這包括含全域性無狀態public static函式。這些可以分解成純函式。更進一步,Kotlin支援類似Function型別這樣的FP結構和高階函式,這可以用來使程式碼更易於維護和測試。

類中邏輯複雜的專案 —— 這些專案容易受到空指標異常(NPE)的影響,而這是Kotlin很好地解決了的其中一個問題。通過讓語言分析可能導致NPE的程式碼路徑為開發人員提供支援。Kotlin的when結構(一個更好的switch)在這裡非常有用,可以將巢狀的邏輯樹分解為可管理的函式。對變數和集合的不變性支援有助於簡化邏輯,避免由於引用洩漏而導致難以查詢的錯誤。雖然上面的一些功能可以通過Java實現,但Kotlin的優勢在於升級了這些範例,並使它們保持簡潔一致。

讓我們在這裡暫停一下,看一個典型的Java邏輯片段以及對應的Kotlin實現:

public class Sample {
  
   public String logic(String paramA, String paramB) {
       String result = null;
       try {
           if (paramA.length() > 10) {
               throw new InvalidArgumentException(new String[]{"Unknown"});
           } else if ("AB".equals(paramA) && paramB == null) {
               result = subLogicA(paramA + "A", "DEFAULT");
           } else if ("XX".equals(paramA) && "YY".equals(paramB)) {
               result = subLogicA(paramA + "X", paramB + "Y");
           } else if (paramB != null) {
               result = subLogicA(paramA, paramB);
           } else {
               result = subLogicA(paramA, "DEFAULT");
           }
       } catch (Exception ex) {
           result = ex.getMessage();
       }
       return result;
   }
   private String subLogicA(String paramA, String paramB) {
       return paramA + "|" + paramB;
   }
}

對應的Kotlin實現:

fun logic(paramA: String, paramB: String?): String {
   return try {
       when {
           (paramA.length > 10) -> throw InvalidArgumentException(arrayOf("Unknown"))
           (paramA == "AB" && paramB == null) -> subLogicA(paramA + "A")
           (paramA == "XX" && paramB == "YY") -> subLogicA(paramA + "X", paramB + "X")
           else -> if (paramB != null) subLogicA(paramA, paramB) else subLogicA(paramA)
       }
   } catch (ex: Exception) {
       ex.message ?: "UNKNOWN"
   }
}
private fun subLogicA(paramA: String, paramB: String = "DEFAULT"): String {
   return "$paramA|$paramB"
}

雖然這些程式碼片段在功能上是等效的,但是它們有一些明顯的區別。

logic()函式不需要包含在類中。Kotlin提供了頂級函式。這開闢了一個廣闊的空間,鼓勵我們去思考是否真的需要一個物件。單獨的純函式更容易測試。這為團隊提供了採用更簡潔的函式方法的選項。

Kotlin引入了when,這是一個處理條件流的強大結構。它比ifswitch語句的功能要強大得多。任意邏輯都可以使用when進行條理的組織。

注意,在Kotlin版本中,我們從未宣告返回變數。這是可能的,因為Kotlin允許我們使用whentry作為表示式。

subLogicA函式中,我們可以在函式宣告中為paramB指定一個預設值。

private fun subLogicA(paramA: String, paramB: String = "DEFAULT"): String {

現在,我們可呼叫任何一個函式簽名了:

subLogicA(paramA, paramB)

或者

subLogicA(paramA) # In this case the paramB used the default value in the function declaration

現在,邏輯更容易理解了,程式碼行數減少了約35%。

把Kotlin加入Java構建

MavenGradle通過外掛支援Kotlin。Kotlin程式碼被編譯成Java類幷包含在構建過程中。Kobalt等比較新的構建工具看起來也很有前景。Kobalt受Maven/Gradle啟發,但完全是用Kotlin編寫的。

首先,將Kotlin外掛依賴項新增到MavenGradle構建檔案中。

如果你使用的是Spring和JPA,你還應該新增kotlin-springkotlin-jpa編譯器外掛。專案的編譯和構建沒有任何明顯的差異。

如果要為Kotlin程式碼庫生成JavaDoc則需要這個外掛

有針對IntelliJ和Eclipse Studio的IDE外掛,但正如我們所預料的那樣,Kotlin的開發和構建工具從IntelliJ關聯中獲益良多。從社群版開始,該IDE對Kotlin提供了一等支援。其中一個值得注意的特性是,它支援將現有的Java程式碼自動轉換為Kotlin。這種轉換很準確,而且是一種很好的學習Kotlin慣用法的工具。

與流行框架整合

因為我們將Kotlin引入了現有的專案中,所以框架相容性是一個問題。Kotlin完美融入了Java生態系統,因為它可以編譯成Java位元組碼。一些流行的框架已經宣佈支援Kotlin,包括Spring、Vert.x、Spark等。讓我們看下Kotlin和Spring及Hibernate一起使用是什麼樣子。

Spring

Spring是Kotlin的早期支持者之一,在2016年首次增加支援。Spring 5利用Kotlin提供更簡潔的DSL。你可以認為,現有的Java Spring程式碼無需任何更改就可繼續執行。

Kotlin中的Spring註解

Spring註釋和AOP都是開箱即用的。你可以像註解Java一樣註解Kotlin類。考慮下面的服務宣告片段。

@Service
@CacheConfig(cacheNames = [TOKEN_CACHE_NAME], cacheResolver = "envCacheResolver")
open class TokenCache @Autowired constructor(private val repo: TokenRepository) {

這些是標準的Spring註解:

@Service: org.springframework.stereotype.Service

@CacheConfig: org.springframework.cache

注意,constructor是類宣告的一部分。

@Autowired constructor(private val tokenRepo: TokenRepository)

Kotlin將其作為主建構函式,它可以是類宣告的一部分。在這個例項中,tokenRepo是一個內聯宣告的屬性。

編譯時常量可以在註解中使用,通常,這有助於避免拼寫錯誤。

final類處理

Kotlin類預設為final的。它提倡將繼承作為一種有意識的設計選擇。這在Spring AOP中是行不通的,但也不難彌補。我們需要將相關類標記為open —— Kotlin的非final關鍵字。

IntelliJ會給你一個友好的警告。

寫給伺服器端Java開發人員的Kotlin簡介

你可以通過使用maven外掛all open來解決這個問題。這個外掛可以open帶有特定註解的類。更簡單的方法是將類標記為open

自動裝配和空值檢查

Kotlin嚴格執行null檢查。它要求初始化所有標記為不可空的屬性。它們可以在宣告時或建構函式中初始化。這與依賴注入相反——依賴注入在執行時填充屬性。

lateinit修飾符允許你指定屬性將在使用之前被初始化。在下面的程式碼片段中,Kotlin相信config物件將在首次使用之前被初始化。

@Component
class MyService {

   @Autowired
   lateinit var config: SessionConfig
}

雖然lateinit對於自動裝配很有用,但我建議謹慎地使用它。另一方面,它會關閉屬性上的編譯時空檢查。如果在第一次使用時是null仍然會出現執行時錯誤,但是會丟失很多編譯時空檢查。

建構函式注入可以作為一種替代方法。這與Spring DI可以很好地配合,並消除了許多混亂。例如:

@Component
class MyService constructor(val config: SessionConfig)

這是Kotlin引導你遵循最佳實踐的一個很好的例子。

Hibernate

Hibernate和Kotlin可以很好地搭配使用,不需要做大的修改。一個典型的實體類如下所示:

@Entity
@Table(name = "device_model")
class Device {

   @Id
   @Column(name = "deviceId")
   var deviceId: String? = null

   @Column(unique = true)
   @Type(type = "encryptedString")
   var modelNumber = "AC-100"

   override fun toString(): String = "Device(id=$id, channelId=$modelNumber)"

   override fun equals(other: Any?) =
       other is Device
           && other.deviceId?.length == this.deviceId?.length
           && other.modelNumber == this.modelNumber

   override fun hashCode(): Int {
       var result = deviceId?.hashCode() ?: 0
       result = 31 * result + modelNumber.hashCode()
       return result
   }
}

在上面的程式碼片段中,我們利用了幾個Kotlin特性:

屬性

通過使用屬性語法,我們就不必顯式地定義gettersetter了。這減少了混亂,使我們能夠專注於資料模型。

型別推斷

在我們可以提供初始值的情況下,我們可以跳過型別規範,因為它可以被推斷出來。例如:

var modelNumber = "AC-100"

modelNumber屬性會被推斷為String型別。

表示式

如果我們稍微仔細地看下toString()方法,就會發現它有與Java有一些不同:

override fun toString(): String = "Device(id=$id, channelId=$modelNumber)"

它沒有返回語句。這裡,我們使用了Kotlin表示式。對於返回單個表示式的函式,我們可以省略花括號,通過等號賦值。

字串模板

"Device(id=$id, channelId=$modelNumber)"

在這裡,我們可以更自然地使用模板。Kotlin允許在任何字串中嵌入${表示式}。這消除了笨拙的連線或對String.format等外部輔助程式的依賴。

相等測試

equals方法中,你可能已經注意到了這個表示式:

other.deviceId?.length == this.deviceId?.length

它用==符號比較兩個字串。在Java中,這是一個長期存在的問題,它將字串視為相等測試的特殊情況。Kotlin最終修復了這個問題,始終把==用於結構相等測試(Java中的equals())。把===用於引用相等檢查。

資料類

Kotlin還提供一種特殊型別的類,稱為資料類。當類的主要目的是儲存資料時,這些類就特別適合。資料類會自動生成equals()hashCode()toString()方法,進一步減少了樣板檔案。

有了資料類,我們的最後一個示例就可以改成:

@Entity
@Table(name = "device_model")
data class Device2(
   @Id
   @Column(name = "deviceId")
   var deviceId: String? = null,

   @Column(unique = true)
   @Type(type = "encryptedString")
   var modelNumber: String = "AC-100"
)

這兩個屬性都作為建構函式的引數傳入。equalshashCodetoString是由資料類提供的。

但是,資料類不提供預設建構函式。這是對於Hibernate而言是個問題,它使用預設建構函式來建立實體物件。這裡,我們可以利用kotlin-jpa外掛,它為JPA實體類生成額外的零引數建構函式。

在JVM語言領域,Kotlin的與眾不同之處在於,它不僅關注工程的優雅性,而且解決了現實世界中的問題。

採用Kotlin的實際好處

減少空指標異常

解決Java中的NPE是Kotlin的主要目標之一。將Kotlin引入專案時,顯式空檢查是最明顯的變化。

Kotlin通過引入一些新的操作符解決了空值安全問題。Kotlin的?操作符就提供了空安全呼叫,例如:

val model: Model? = car?.model

只有當car物件不為空時,才會讀取model屬性。如果car為空,model計算為空。注意model的型別是Model?——表示結果可以為空。此時,流分析就開始起作用了,我們可以在任何使用model 變數的程式碼中進行NPE編譯時檢查。

這也可以用於鏈式呼叫:

val year = car?.model?.year

下面是等價的Java程式碼:

Integer year = null;
if (car != null && car.model != null) {
   year = car.model.year;
}

一個大型的程式碼庫會省掉許多這樣的null檢查。編譯時安全自動地完成這些檢查可以節省大量的開發時間。

在表示式求值為空的情況下,可以使用Elvis操作符( ?: )提供預設值:

val year = car?.model?.year ?: 1990

在上面的程式碼片段中,如果year最終為null,則使用值1990。如果左邊的表示式為空,則?: 操作符取右邊的值。

函數語言程式設計選項

Kotlin以Java 8的功能為基礎構建,並提供了一等函式。一等函式可以儲存在變數/資料結構中並傳遞出去。例如,在Java中,我們可以返回函式:

@FunctionalInterface
interface CalcStrategy {
   Double calc(Double principal);
}

class StrategyFactory {
   public static CalcStrategy getStrategy(Double taxRate) {
       return (principal) -> (taxRate / 100) * principal;
   }
}

Kotlin讓這個過程變得更加自然,讓我們可以清晰地表達意圖:

// Function as a type
typealias CalcStrategy = (principal: Double) -> Double
fun getStrategy(taxRate: Double): CalcStrategy = { principal -> (taxRate / 100) * principal }

當我們深入使用函式時,事情就會發生變化。下面的Kotlin程式碼片段定義了一個生成另一個函式的函式:

val fn1 = { principal: Double ->
   { taxRate: Double -> (taxRate / 100) * principal }
} 

我們很容易呼叫fn1及結果函式:

fn1(1000.0) (2.5)

輸出
25.0

雖然以上功能在Java中也可以實現,但並不直接,並且包含樣板程式碼。

提供這些功能是為了鼓勵團隊嘗試FP概念,開發出更符合要求的程式碼,從而得到更穩定的產品。

注意,Kotlin和Java的lambda語法略有不同。這在早期可能會給開發人員帶來煩惱。

Java程式碼:

( Integer first, Integer second ) -> first * second

等價的Kotlin程式碼:

{ first: Int, second: Int -> first * second }

隨著時間的推移,情況就變得明顯了,Kotlin支援的應用場景需要修改後的語法。

減少專案佔用空間大小

Kotlin最被低估的優點之一是它可以減少專案中的檔案數量。Kotlin檔案可以包含多個/混合類宣告、函式和列舉類等其他結構。這提供了許多Java沒有提供的可能性。另一方面,它提供了一種新的選擇——組織類和函式的正確方法是什麼?

在《程式碼整潔之道》一書中,Robert C Martin打了報紙的比方。好程式碼應該讀起來和報紙一樣——高階結構在檔案上部,越往下面越詳細。這個檔案應該講述一個緊湊的故事。Kotlin的程式碼佈局從這個比喻中可見一斑。

建議是——把相似的東西放在一起——放在更大的上下文裡。

雖然Kotlin不會阻止你放棄“結構(structure)”,但這樣做會使後續的程式碼導航變得困難。組織東西要以它們之間的關係和使用順序為依據,例如:

enum class Topic {
    AUTHORIZE_REQUEST,
    CANCEL_REQUEST,
    DEREG_REQUEST,
    CACHE_ENTRY_EXPIRED
}

enum class AuthTopicAttribute {APP_ID, DEVICE_ID}
enum class ExpiryTopicAttribute {APP_ID, REQ_ID}

typealias onPublish = (data: Map<String, String?>) -> Unit

interface IPubSub {
    fun publish(topic: Topic, data: Map<String, String?>)
    fun addSubscriber(topic: Topic, onPublish: onPublish): Long
    fun unSubscribe(topic: Topic, subscriberId: Long)
}
class RedisPubSub constructor(internal val redis: RedissonClient): IPubSub {
...}

在實踐中,通過減少為獲得全貌而需要跳轉的檔案數量,可以顯著減少腦力開銷。

一個常見的例子是Spring JPA庫,它使包變得混亂。可以把它們重新組織到同一個檔案中:

@Repository
@Transactional
interface DeviceRepository : CrudRepository<DeviceModel, String> {
    fun findFirstByDeviceId(deviceId: String): DeviceModel?
}

@Repository
@Transactional
interface MachineRepository : CrudRepository<MachineModel, String> {
    fun findFirstByMachinePK(pk: MachinePKModel): MachineModel?
}
@Repository
@Transactional
interface UserRepository : CrudRepository<UserModel, String> {
    fun findFirstByUserPK(pk: UserPKModel): UserModel?
}

上述內容的最終結果是程式碼行數(LOC)顯著減少。這直接影響了交付速度和可維護性。

我們統計了Java專案中移植到Kotlin的檔案數量和程式碼行數。這是一個典型的REST服務,包含資料模型、一些邏輯和快取。在Kotlin版本中,LOC減少了大約50%。開發人員在跨檔案瀏覽和編寫樣板程式碼上消耗的時間明顯減少。

簡潔而富有表達力的程式碼

編寫簡潔的程式碼是一個寬泛的話題,這取決於語言、設計和技術的結合。然而,Kotlin提供了一個良好的工具集,為團隊的成功奠定了基礎。下面是一些例子。

型別推斷

型別推斷最終會減少程式碼中的噪音。這有助於開發人員關注程式碼的目標。

型別推斷可能會增加我們跟蹤正在處理的物件的難度,這是一種常見的擔憂。從實際經驗來看,這種擔憂只在少數情況下有必要,通常少於5%。在大多數情況下,型別是顯而易見的。

下面的例子:

LocalDate date = LocalDate.now();
String text = "Banner";

變成了:

val date = LocalDate.now()
val text = "Banner"

在Kotlin中,也可以指定型別:

val date: LocalDate = LocalDate.now()
val text: String = "Banner"

值得注意的是,Kotlin提供了一個全面的解決方案。例如,在Kotlin中,我們可以將函式型別定義為:

val sq = { num: Int -> num * num }

另一方面,Java 10通過檢查右邊表示式的型別推斷型別。這引入了一些限制。如果我們嘗試在Java中執行上述操作,我們會得到一個錯誤:

寫給伺服器端Java開發人員的Kotlin簡介

類型別名

這是Kotlin中一個方便的特性,它允許我們為現有型別分配別名。它不引入新型別,但允許我們使用替代名稱引用現有型別,例如:

typealias SerialNumber = String

SerialNumber現在是String型別的別名,可以與String型別互換使用,例如:

val serial: SerialNumber = "FC-100-AC"

和下面的程式碼等價:

val serial: String = "FC-100-AC"

很多時候,typealias可以作為一個“解釋變數”,提高清晰度。考慮以下宣告:

val myMap: Map<String, String> = HashMap()

我們知道myMap包含字串,但我們不知道這些字串表示什麼。我們可以通過引入String型別的別名來澄清這段程式碼:

typealias ProductId = String
typealias SerialNumber = String

現在,上述myMap的宣告可以改成:

val myMap: Map<ProductId, SerialNumber> = HashMap()

上面兩個myMap的定義是等價的,但是對於後者,我們可以很容易地判斷Map的內容。

Kotlin編譯器用底層型別替換了類型別名。因此,myMap的執行時行為不受影響,例如:

myMap.put(“MyKey”, “MyValue”)

這種鈣化的累積效應是減少了難以捉摸的Bug。在大型分散式團隊中,錯誤通常是由於未能溝通意圖造成的。

早期應用

早期獲得吸引力通常是引入變革的最困難的部分。從確定合適的實驗專案開始。通常,有一些早期的採用者願意嘗試並編寫最初的Kotlin程式碼。在接下來的幾周裡,更大的團隊將有機會檢視這些程式碼。人們早期的反應是避免新的和不熟悉的東西。變革需要一些時間來審視。通過提供閱讀資源和技術講座來幫助評估。在最初的幾周結束時,更多的人可以決定在多大程度上採用。

對於熟悉Java的開發人員來說,學習曲線很短。以我的經驗來看,大多數Java開發人員在一週內都能高效地使用Kotlin。初級開發人員可以在沒有經過特殊培訓的情況下使用它。以前接觸過不同語言或熟悉FP概念會進一步減少採用時間。

未來趨勢

從1.1版本開始,“協同例程(Co-routine)”就可以用在Kotlin中了。在概念上,它們類似於JavaScript中的async/await。它們允許我們在不阻塞執行緒的情況下掛起流,從而降低非同步程式設計中的複雜性。

到目前為止,它們還被標記為實驗性的。協同例程將在1.3版本中從實驗狀態畢業。這帶來了更多令人興奮的機會。

Kotlin的路線圖在Kotlin Evolution and Enhancement Process(KEEP)的指導下制定。請密切關注這方面的討論和即將釋出的特性。

作者簡介

Baljeet Sandhu是一名技術負責人,擁有豐富的經驗,能夠為從製造到金融的各個領域提供軟體。他對程式碼整潔、安全和可擴充套件的分散式系統感興趣。Baljeet目前為HYPR工作,致力於構建非集中式的認證解決方案,以消除欺詐,提高使用者體驗,實現真正的無密碼安全。

檢視英文原文:An Introduction to Kotlin for Serverside Java Developers