Java到Kotlin,從入門到放棄

2020-12-13 蟲蟲搜奇

Kotlin是目前非常熱門的用於取代Java的流行語言。 Kotlin的編譯時安全性,簡潔明快,去掉Java繁瑣的臭裹腳布。是該切換到Kotlin呢?還是堅守Java繼續擼呢,這是每一個Java碼農需要思考的問題。那麼本文蟲蟲就給大家白一白。

我最喜歡的一組JVM語言棧:主推Java,Groovy 做測試。去年夏天,我們團隊開始了一個新的微服務項目,和往常一樣,除了擼碼,我們也爭論語言和技術。有很多人倡導Kotlin的團隊,所以我們想嘗嘗鮮,所以就決定試試Kotlin。由於沒有Spock對應Kotlin,我們還用Groovy做測試。去年冬天,在經過幾個月的日息相處之後,我們做了技術總結,最後的結論是Kotlin並沒有給我們代來什麼改進,反而拖慢了我們的進度。所以我們從入門到放棄,又回歸了Java 10。

命名隱藏(Name shadowing)

隱藏(shadowing)是Kotlin帶給我們的最大驚喜。考慮以下函數:

fun inc(num : Int) {

val num = 2

if (num > 0) {

val num = 3

}

println ("num: " + num)

}

當你調用inc(1)時輸出是啥? 在Kotlin中,方法參數都是值,所以你不能改變num參數。這是非常棒的的特性,因為你不應該在方法中試圖改變參數。但是你可以用相同的名稱(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做法可能是對的。我還無法想出一個用於映射方法參數的有效用例。

類型推斷(Type inference)

在Kotlin中,當你聲明一個var或val時,編譯器會通過右邊的表達式的類型來自動探測到變量的類型。我們稱之為局部變量類型推斷,這對程式設計師來說是一個很大的改進。它允許我們在不影響靜態類型檢查的情況下簡化代碼。

舉例如下:

var a ="10"

Kotlin編譯器會自動轉化成:

var a:String ="10"

這是也是Java的優勢。我故意說是因為,有了好消息,Java 10已經支持這個特性了。

Java 10中對應的語法為:

var a = "10";

公平來說,我們要說的是,儘管如此,Kotlin在這方面還是更勝一籌。你還可以在其他上下文中使用類型推斷,比如單行方法

關於Java10的局部類型推斷可以參考Java 10相關的文檔。

編譯時NULL安全(Compile time null-safety)

Null安全類型是Kotlin的殺手鐧,非常棒的功能。在Kotlin中,類型默認是不可空的。如果你需要一個可為空的類型,你需要在它的後面添加"?"來修飾它,例如:

val a: String? = null // 可以編譯

val b: String = null // 編譯錯誤

如果使用不帶空值檢查的可空變量,Kotlin編譯器就會報錯,例如:

println (a.length) // error

println (a?.length) //可編譯,列印為空

println (a?.length ?: 0) //可編譯,列印為 0

一旦你有這兩種類型,不可為空的T和可空的T?,你就可以不用在考慮Java中最常見的異常:"NullPointerException(NPE)"。真有這麼神奇?然而,事情沒有這麼簡單。

當你的Kotlin代碼必須與Java代碼混合使用時,情況會變得很糟糕(很多庫都是用Java編寫的,這種情況常見)。然後,第三種類型就來了,T!。它被稱為平臺類型,不知為什麼他可以是T或T?。或者更確切的說,T!意味著T具有未定義的可空性。這種奇怪的類型無法在Kotlin中表示,它只能從Java的類型推斷出來。它可能會誤導你,因為它對空值放鬆檢查,並且Kotlin的的NULL安全機制也對它失效了。

考慮下面的Java方法:

public class Utils {

static String format(String text) {

return text.isEmpty() ? null : text;

}

}

現在,你想調用Kotlin的格式(String)。你應該使用哪種類型來表示這個Java方法的返回結果呢? 你有三個選擇:

方法一。你可以使用String,代碼看起來很安全,但會拋出NPE異常。

fun chongchong(text: String) {

val f: String = Utils.format(text)

// 可編譯,但是運行時會拋出NPE異常

println ("f.len : " + f.length)

}

你需要用Elvis來修正它:

fun chongchong1(text: String) {

val f: String = Utils.format(text) ?: ""

// 用了Elvis符(?:)所以ok了

println ("f.len : " + f.length)

}

方法二 你可以使用String?,然後NULL安全:

fun chongchong2(text: String) {

val f: String? = Utils.format(text) // 安全

println ("f.len : " + f.length) // 運行時錯誤

println ("f.len : " + f?.length) // 通過添加?符號,null安全

}

方法三 你可以利用Kotlin的局部變量類型推斷:

fun chongchong3(text: String) {

val f = Utils.format(text) // f 推斷為String

println ("f.len : " + f.length)

// 可以編譯,但是可能運行時拋出NPE異常

}

這些代碼看起來是編譯時安全的,但是這會讓你代碼完全不會做NULL安全檢查,就像Java中的那樣。

還有一種更巧妙的辦法:!!操作符,用它來強制f的類型為String:

fun chongchong4(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?)。

類字面量(literal)

使用類似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()

反式類型聲明(Reversed type declaration)

在C語言系列程式語言中,我們有標準聲明類型的方法。簡而言之,首先進入一個類型,然後輸入一個類型的東西(變量,欄位,方法等)。

Java中的標準表示法:

int inc(int i) {

return i + 1;

}

Kotlin中的反向類型聲明:

fun inc(i: Int): Int {

return i + 1

}

這種反序的表示令人很討厭:

首先,你需要在名稱和類型之間鍵入並閱讀這個多餘的冒號。增加他的目的是什麼?為什麼名稱要與類型分開?最主要的是,這會增加很多工作量。

其次,當你讀取一個方法聲明時,你第一對名字和返回類型感興趣,然後才是參數。

在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

而Kotlin的語法,使得在IntelliJ中輸入這個變量非常困難。如果你有多個存儲庫,則在自動補全列表中找不到正確的項目。這意味著需要用手輸入完整的變量名稱。

repository : MongoExperimentsRepository

伴生對象(Companion object)

某Kotlin QQ群對話:

"嗨,我是新來的,我可以在Kotlin中使用靜態成員嗎?"

"沒有。Kotlin中一切皆對象的,靜態成員不是面向對象的。"

"哦,那麼我想對我的類搞個記錄器,怎麼做?"

"沒問題,你可以使用伴生對象。"

"什麼是伴生對象?"

"這是與你的類相關的單例對象。你可以在你的伴生對象中放記錄器"

"哦,知道了,謝謝"

伴生對象寫法例子:

class Chongchong {

companion object {

val logger = LoggerFactory.getLogger(Chongchong::class.java)

}

}

看起來很繁瑣的,但是現在可以很方便的用Chongchong.logger來調用這個記錄器,和 Java中的靜態成員類似。但它不是一個靜態成員。因為kotlin中只有對象。我們可以把它看作是已經實例化為單例的匿名內部類。實際上這個類又不是匿名的,它被命名為Companion,但是你可以省略這個名字。

單例很有用,但是完全拋棄Java的靜態成員有點不切實際的。在Java中,我們使用靜態記錄器多年。它只是一個記錄器,所以我們不關心他是否面向對象。它運行的很好,也沒有帶來什麼危害。

有的時候,你必須使用靜態。比如老的公共靜態方法void main()仍然是啟動Java應用程式的唯一方式。為他寫伴生對象沒有太大必要:

class AppRunner {

companion object {

@JvmStatic fun main(args: Array<String>) {

SpringApplication.run(AppRunner::class.java, *args)

}

}

}

集合字面量(Collection literals)

在Java中,初始化一個列表很繁瑣:

import java.util.Arrays;

...

List<String> strings = Arrays.asList("Chong", "CC");

初始化一個Map也是如此,所以很多人使用Guava:

import com.google.common.collect.ImmutableMap;

...

Map<String, String> string = ImmutableMap.of("firstName", " Chong", "lastName", "CC");

在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("Chong", "CC")

val map = mapOf("firstName" to " Chong ", "lastName" to "CC")

在map中,鍵和值通過to運算符做配對,這很好,但是為啥不用大家都熟悉的:呢?有點令人失望。

Maybe? 語法

函數式語言(如Haskell)沒有NULL值。而是提供Maybe語法。很久以前Scala就把這種語法引進到了JVM中,這就是Optional。然後在Java 8中被採用為。現在,Optional是在API邊界返回類型中的空值處理的非常流行的方式。

Kotlin中沒有相應的語法。一般來說應該用Kotlin的可空類型。

通常情況下,當你有一個可選的時候,你想要應用一系列NULL安全的轉換,並在末端處理NULL值。

例如,在Java中:

public int parseAndInc(String number) {

return Optional.ofNullable(number)

.map(Integer::parseInt)

.map(it -> it + 1)

.orElse(0);

}

你可以嗎?是的,但並不那麼簡單。上面的代碼會報錯,並且parseInt()也會引發NPE。

monadic風格的map()僅在存在值時執行。否則,就會傳遞null值。這就是為什麼map()非常方便。不幸的是,Kotlin中不是這樣的。它只是獲取左側的內容,包括空值。

所以為了使這個代碼null安全,你必須在每一個let之前添加?:

fun parseAndInc(number: String?): Int {

return number?.let { Integer.parseInt(it) }

?.let { it -> it + 1 } ?: 0

}

現在,比較Java和Kotlin版本的可讀性。你會更喜歡哪一個呢?

數據類(Data classes)

數據類是Kotlin在實現Java中值對象(又名DTO)的必須方法。

例如,在Kotlini中一個基本的數據類:

data class User(val name: String, val age: Int)

Kotlin就會自動生成equals(), hashCode(), toString(),和copy()的,這是非常很好的實現。

在實現簡單的DTO時數據類非常有用。但是需要注意,數據類有很大的局限性,它們是終態的(final)。我們無法擴展數據類或對其抽象。所以可能你無法在核心域模型中使用它們。

這個限制不是Kotlin的錯。因為沒有辦法在不違反Liskov原則的情況下生成正確的基於值的equals()。這就是為什麼Kotlin不允許數據類繼承的原因。

公開類(Open classes)

在Kotlin中,類默認是final的。如果你想擴展一個類,你必須給它添加open修飾符。

Kotlin的類繼承語法如下所示:

open class Base

class Derived : Base()

Kotlin將extends關鍵字更改為:運算符,該運算符已用於變量名稱與類型的分隔符。這是致敬C++語法?對我來說這有點混亂。

而且最具爭議的是,默認情況下,類是final的。這也許是由於Java程式設計師過度使用繼承。也許想讓你你應該在考慮擴展類之前再三考慮。但我們生活在富框架的世界,框架喜歡AOP。Spring使用庫(cglib,jassist)給你的bean生成動態代理。Hibernate擴展你的實體以啟用惰加載(lazy loading)。

如果你使用Spring,你有兩種選擇:把所有的bean類放在前面(這很枯燥),或者使用這個繁瑣的編譯器插件:

buildscript {

dependencies {

classpath group: 'org.jetbrains.kotlin', name: 'kotlin-allopen', version: "$versions.kotlin"

}

}

曲折的學習路線

如果你認為你可以快速學習Kotlin,因為你已經熟悉Java,那你錯了。Kotlin會讓你陷入深淵。事實上,Kotlin的語法更接近Scala。你將不得不忘記Java並切換到完全不同的語言。

做為對比,學習Groovy是一個愉快的旅程。 Groovy會牽著你的手。 Java代碼是正確的Groovy代碼,因此你可以從將.java文件擴展名更改為.groovy開始。每次您學習新的Groovy功能時,你都可以決定。你喜歡它還是喜歡用Java方式?

相關焦點

  • Kotlin入門教程,快使用Kotlin吧
    學習網站 Kotlin 從入門到放棄:https://www.jianshu.com/c/d3eac4c37b5fKotlin 菜鳥教程:http://www.runoob.com/kotlin/kotlin-tutorial.html基本語法 1、數據類型1.1、基本數據類型類型位寬度Double64Float32Long64Int32Short16Byte8每一個類型都有一個toXXX
  • Kotlin學習筆記——基礎篇
    level文件最外層、object單例類或companion object伴生對象中聲明(3)構造函數,分為主構造函數和次構造函數,主構造函數可以直接寫在類後面,也可以使用constructor聲明主次構造函數,init自動在主構造函數中執行(4)字符串拼接,單變量字符$a,複雜表達式${a.b},特殊字符需要轉義,轉義可以使用\***或${'***'}參考:Kotlin入門
  • Kotlin 怎麼學 ?遇到過哪些坑?
    by-lazy 上面說到了,lateinit 簡單用法如下:lateinit var mStirng: StringmString = "some"if(TestActivity::mStirng.isLateinit){  }4.比 java 更強大的類型判斷
  • java編程從入門到放棄?關於Java自學,我的3點思考
    首先,Java作為一門適用性很強的語言,入門是不難的。但要系統全面地自學Java,那可就很難了!如果每個人都能通過自學,系統全面的掌握Java。2、如何自學java?有了堅定的學習信念後,咱們再來說怎麼學。網上Java學習的資料多如牛毛,但我們首先要有個清晰的學習思路。
  • 使用 Kotlin 進行 Android 測試
    所以先邁出第一步,準備好我們的環境配置,把Kotlin 的依賴加入到我們的 build.gradle 文件中去:buildscript {  repositories {    mavenCentral()    jcenter()  }  dependencies {    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:
  • Kotlin 測試利器—MockK
    關鍵字  在 Kotlin 裡面 when是關鍵字,Mockito 的when ,必須加上反引號才能使用:`when`(xxxx).thenReturn(xxx)  如果看起來不舒服,也可以捨棄 Mockito 改用 mockito-kotlin
  • 再見,Kotlin !你好, Java !
    ::classval javaClass : Class<LocalDate> = LocalDate::class.javaval gson = GsonBuilder().registerTypeAdapter(LocalDate::class.java, LocalDateAdapter()).create()C 系列的程式語言有標準的聲明類型的方法
  • 問:談談 Kotlin 範型與逆變協變?
    kotlin 系列文章均以 Java 差異為核心進行提煉,與 Java 相同部分不再列出。隨著 kotlin 官方版本的迭代,文中有些語法可能會發生變化,請務必留意。kotlin 泛型(generics)與 java 一樣,kotlin 的泛型用來表示變量類型的參數化。
  • 「從入門到放棄-Java」並發編程-鎖-synchronized
    簡介上篇【從入門到放棄-Java】並發編程-線程安全中,我們了解到,可以通過加鎖機制來保護共享對象,來實現線程安全。synchronized是java提供的一種內置的鎖機制。通過synchronized關鍵字同步代碼塊。
  • 最強總結 | 帶你快速搞定kotlin開發(上篇)
    的在強轉失敗時並不會拋異常,而是返回一個null值// Person.ktPerson::class// Man.javaMan::class.java區別於Java,kotlin要兼容Java,所以獲取Java的class和kotlin的class略有不同,kotlin的class使用::class
  • SpringBatch從入門到放棄002- 核心概念1
    Spring -batchSpringBatch從入門到放棄002- 核心概念1上一篇我們通過一個例子簡單介紹了一下 Spring Batch,讓大家有個簡答的概念,今天我們來看 Spring batch 裡面具體的概念。
  • 如何入門中學生編程
    相信很多同學對編程很感興趣,但是卻不知道如何入門以及入門難度如何,廢話少說,直接進入主題。首先,學習編程不難,難的是自己的想法(如果有同學學習過就知道了)。其次,如何入門。對安卓開發有興趣的同學,入門學習 java ,這是因為安卓開發用到的程式語言就是 java。(聽說谷歌出了 kotlin...)
  • Android:Kotlin詳細入門學習指南-基本類型-基礎語法(二)
    =1 // 一個裝箱過的 Int (java.lang.Integer)  val b: Long?不一樣,arrays 在 kotlin 中是不可變的。這意味 kotlin 不允許我們 把 Array.String轉為 Array.Any,這樣就阻止了可能的運行時錯誤(但你可 以使用 Array.outAny.Kotlin 有專門的類來表示原始類型從而避免過度裝箱:ByteArray, ShortArray, IntArray 等等。這些類與 Array 沒有繼承關係,但它們有一樣的方法與屬性。
  • Java轉電氣:從入門到放棄?
    然後下午我也不知道幾點下班,他們到5點半開始開會,到6點鐘還沒開完,走之前告訴我,早上7點50要開晨會。我自己先走了。晚上回來之後,硬著頭皮在網上開始學習PLC相關的知識,之前完全沒有接觸過PLC的概念,腦子也是懵的,幸好基本都能聽懂。當時聽的都睡著了,然後關掉,開始了labview的學習,看到晚上10點多鐘。
  • Kotlin項目實戰之手機影音---悅單條目實現及BaseListFragment抽取
    基本也把它快要遺忘在腦海裡了,好在之前還有備忘可以複習一下,準備繼續前行,不能放棄。上一次已經實現了首頁這個TAB頁面了,這次照理按順序應該來實現MV這個TAB了:但是這裡從最後一個悅單Tab開始實現:
  • 到底現在還值不值得去學習java?還是選擇Python?
    眾所周知,之前就聽說kotlin將代替java,如果java將來真的被kotlin代替,那Java現在還值得去學習嗎?其實不是說kotlin將代替java就不值得去學習java開發,從去年到現在IA人工智慧熱度一直都是上升,而且很多有經驗的程式設計師也打出「2018年不學習Python還能學習哪種程式語言「的口號,可見人工智慧才是未來發展比較的領域。
  • Java 24 歲!Google 加持的 Kotlin 真能取代它?
    接下來,我們將從近日 Kotlin 的開發商 JetBrains 最新發布的一份《Kotlin Census 2018》的報告中尋找到答案。註:該報告基於全球 4300 名開發者(不局限於 Kotlin 用戶)。
  • 一些 Kotlin 小技巧及解析
    [譯][2.4K Start] 放棄 Dagger 擁抱 Koinhttps://juejin.im/post/5ebc1eb8e51d454dcf45744e[譯][5k+] Kotlin 的性能優化那些事https://juejin.im/post/5ec0f3afe51d454db11f8a94這兩篇文章都分析了 Kotlin
  • Kotlin VS Java 編譯速度大比拼,到底誰更快?
    第一部分討論了從Java轉換到Kotlin。第二部分是對Kotlin的看法。把一個Java應用程式轉換為Kotlin,編譯時間要多久?這是關於Kotlin的一系列文章。分為三個部分。 第一部分討論了從Java轉換到Kotlin。第二部分是我對Kotlin的看法。
  • 麵包機--從入門到放棄(上篇)
    作為我大計院(此處你讀出聲的話,請不要笑)的計算機科學與技術專業畢業的科班生,我最終卻從事了市場營銷行業,大家也就知道我大學成績不怎麼樣,至於《XX語言從入門到精通》,我也就只能聊個大概(也就是目錄罷了),犟叔這次使用了同樣的結構,換了個主題,我也表示沒有看懂。因此我表示,我真的不是當極客的料。讓我們將這篇文章,獻給學弟學妹,學長學姐,讀不懂就表示你和我一樣,大學白讀。