Kotlin與Java的不同之處 [復制鏈接]

2019-11-6 15:39
水刀八木 閱讀:113 評論:0 贊:0
Tag:  Kotlin Java

伴生對象

在 Kotlin 中并不沒有 static 這個關鍵字,該如何處理呢?這里需要用到 Kotlin 的伴生對象來處理。

類內部的對象聲明可以用 companion 關鍵字標記:

該伴生對象的成員可通過只使用類名作為限定符來調用:

可以省略伴生對象的名稱,在這種情況下將使用名稱 Companion:

伴生對象的作用

類似于 Java 中使用類訪問靜態成員的語法。因為 Kotlin 取消了 static 關鍵字,所以 Kotlin 引入伴生對象來彌補沒有靜態成員的不足。可見,伴生對象的主要作用就是為其所在的外部類模擬靜態成員。

在 Java 代碼中調用伴生對象

如何在 Java 代碼中調用 Kotlin 的伴生對象呢?

  • 如果聲明伴生對象有名稱,則使用:
  • 如果聲明伴生對象無名稱,則采用 Companion 關鍵字調用:

@JvmField 和 @JvmStatic 的使用

在上面的例子中,我們知道了可以在 Java 代碼中調用 Kotlin 中伴生對象的成員,類似于 Java 類中的靜態成員。但是看上去和 Java 中的還是略有區別,因為類名和方法名/屬性setter,getter方法名之間多了個伴生對象的名稱或者 Companion 關鍵字。如何使其在調用的時候與 Java 中的調用看上去一樣呢?

Kotlin 為我們提供了 @JvmField 和 @JvmStatic 兩個注解。@JvmField 使用在屬性上,@JvmStatic 使用在方法上。如:

這樣我們在 Java 代碼中調用的時候就和 Java 類調用靜態成員的形式一致了,Kotlin 代碼調用方式不變:

const 關鍵字

在伴生對象中,我們可能需要聲明一個常量,目的是等同于 Java 中的靜態常量。有兩種方式,一種是上面所提到的使用 @JvmField 注解,另一種則是使用 const 關鍵字修飾。這兩種聲明方式都等同于 Java 中 static final 所修飾的變量。如下代碼:

擴展屬性和擴展方法

擴展函數

Kotlin的擴展函數可以讓你作為一個類成員進行調用的函數,但是是定義在這個類的外部。這樣可以很方便的擴展一個已經存在的類,為它添加額外的方法

下面我們為String添加一個toInt的方法

在這個擴展函數中,你可以直接訪問你擴展的類的函數和屬性,就像定義在這個類中的方法一樣,但是擴展函數并不允許你打破封裝。跟定義在類中方法不同,它不能訪問那些私有的、受保護的方法和屬性。

擴展函數的導入

我們直接在包里定義擴展函數。這樣我們就可以在整個包里面使用這些擴展,如果我們要使用其他包的擴展,我們就需要導入它。導入擴展函數跟導入類是一樣的方式。

有時候,可能你引入的第三方包都對同一個類型進行了相同函數名擴展,為了解決沖突問題,你可以使用下面的方式對擴展函數進行改名

擴展函數不可覆蓋

擴展方法的原理

Kotlin 中類的擴展方法并不是在原類的內部進行拓展,通過反編譯為Java代碼,可以發現,其原理是使用裝飾模式,對源類實例的操作和包裝,其實際相當于我們在 Java中定義的工具類方法,并且該工具類方法是使用調用者為第一個參數的,然后在工具方法中操作該調用者

如:

反編譯為對應的Java代碼:

擴展屬性

類的擴展屬性原理其實與擴展方法是一樣的,只是定義的形式不同,擴展屬性必須定義get和set方法

為MutableList擴展一個firstElement屬性:

內部類

kotlin的內部類與java的內部類有點不同java的內部類可以直接訪問外部類的成員,kotlin的內部類不能直接訪問外部類的成員,必須用inner標記之后才能訪問外部類的成員

  • 沒有使用inner標記的內部類

反編譯后的java代碼

  • 用inner標記的內部類

反編譯后的java代碼

從上面可以看出,沒有使用inner標記的內部類最后生成的是靜態內部類,而使用inner標記的生成的是非靜態內部類

匿名內部類

匿名內部類主要是針對那些獲取抽象類或者接口對象而來的。最常見的匿名內部類View點擊事件:

上面這個是java匿名內部類的寫法,kotlin沒有new關鍵字,那么kotlin的匿名內部類該怎么寫呢?

方法的參數是一個匿名內部類,先寫object:,然后寫你的參數類型View.OnClickListener{}

kotlin還有一個寫法lambda 表達式,非常之方便:

數據類

在Java中沒有專門的數據類,常常是通過JavaBean來作為數據類,但在Kotlin中提供了專門的數據類。

  • Java

從上面的例子中可以看到,如果要使用數據類,需要手動寫相應的setter/getter方法(盡管IDE也可以批量生成),但是從代碼閱讀的角度來說,在屬性較多的情況下,諸多的seeter/getter方法還是不利于代碼的閱讀和維護。

  • Kotlin
    在Kotlin中,可以通過關鍵字data來生成數據類:

即在class關鍵字之前添加data關鍵字即可。編譯器會根據主構造函數中的參數生成相應的數據類。自動生成setter/getter、toString、hashCode等方法

要聲明一個數據類,需要滿足:

  • 主構造函數中至少有一個參數
  • 主構造函數中所有參數需要標記為val或var
  • 數據類不能是抽象、開發、密封和內部的

枚舉類

枚舉類是一種特殊的類,kotlin可以通過enum class關鍵字定義枚舉類。

枚舉類可以實現0~N個接口;

  • 枚舉類默認繼承于kotlin.Enum類(其他類最終父類都是Any),因此kotlin枚舉類不能繼承類;
  • 非抽象枚舉類不能用open修飾符修飾,因此非抽象枚舉類不能派生子類;
  • 抽象枚舉類不能使用abstract關鍵字修飾enum class,抽象方法和抽象屬性需要使用;
  • 枚舉類構造器只能使用private修飾符修飾,若不指定,則默認為private;
  • 枚舉類所有實例在第一行顯式列出,每個實例之間用逗號隔開,整個聲明以分號結尾;
  • 枚舉類是特殊的類,也可以定義屬性、方法、構造器;
  • 枚舉類應該設置成不可變類,即屬性值不允許改變,這樣更安全;
  • 枚舉屬性設置成只讀屬性后,最好在構造器中為枚舉類指定初始值,如果在聲明時為枚舉指定初始值,會導致所有枚舉值(或者說枚舉對象)的該屬性都一樣。

定義枚舉類

枚舉類實現接口

  1. 枚舉值分別實現接口的抽象成員
  1. 枚舉類統一實現接口的抽象成員
  1. 分別實現抽象枚舉類抽象成員

委托

委托模式 是軟件設計模式中的一項基本技巧。在委托模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委托給另一個對象來處理。委托模式是一項基本技巧,許多其他的模式,如狀態模式、策略模式、訪問者模式本質上是在更特殊的場合采用了委托模式。委托模式使得我們可以用聚合來替代繼承。

Java中委托:

Kotlin:

by表示 p 將會在 PrintImpl 中內部存儲, 并且編譯器將自動生成轉發給 p 的所有 Printer 的方法。

委托屬性

有一些常見的屬性類型,雖然我們可以在每次需要的時候手動實現它們, 但是如果能夠為大家把他們只實現一次并放入一個庫會更好。例如包括:

  • 延遲屬性(lazy properties): 其值只在首次訪問時計算;
  • 可觀察屬性(observable properties): 監聽器會收到有關此屬性變更的通知;
  • 把多個屬性儲存在一個映射(map)中,而不是每個存在單獨的字段中。

為了涵蓋這些(以及其他)情況,Kotlin 支持 委托屬性 。

委托屬性的語法是:

在 by 后面的表達式是該 委托, 因為屬性對應的 get()(和 set())會被委托給它的 getValue() 和 setValue() 方法。

標準委托:

Kotlin 標準庫為幾種有用的委托提供了工廠方法。

  1. 延遲屬性 Lazy
    lazy() 接受一個 lambda 并返回一個 Lazy實例的函數,返回的實例可以作為實現延遲屬性的委托:第一次調用 get() 會執行已傳遞給 lazy() 的 lambda 表達式并記錄結果, 后續調用 get() 只是返回記錄的結果。例如:
  1. 可觀察屬性 Observable
    Delegates.observable() 接受兩個參數:初始值和修改時處理程序(handler)。每當我們給屬性賦值時會調用該處理程序(在賦值后執行)。它有三個參數:被賦值的屬性、舊值和新值:

如果想攔截賦的新值,并根據你是不是想要這個值來決定是否給屬性賦新值,可以使用 vetoable() 取代 observable(),接收的參數和 observable 一樣,不過處理程序 返回值是 Boolean 來決定是否采用新值,即在屬性被賦新值生效之前 會調用傳遞給 vetoable 的處理程序。例如:

  1. 把屬性存在map 中
    一個常見的用例是在一個映射(map)里存儲屬性的值。這經常出現在像解析 JSON 或者做其他“動態”事情的應用中。在這種情況下,你可以使用映射實例自身作為委托來實現委托屬性。

例如:

在上例中,委托屬性會從構造函數傳入的map中取值(通過字符串鍵——屬性的名稱),如果遇到聲明的屬性名在map 中找不到對應的key 名,或者key 對應的value 值的類型與聲明的屬性的類型不一致,會拋出異常。

內聯函數

當一個函數被聲明為inline時,它的函數體是內聯的,也就是說,函數體會被直接替換到函數被調用地方

inline函數(內聯函數)從概念上講是編譯器使用函數實現的真實代碼來替換每一次的函數調用,帶來的最直接的好處就是節省了函數調用的開銷,而缺點就是增加了所生成字節碼的尺寸。基于此,在代碼量不是很大的情況下,我們是否有必要將所有的函數定義為內聯?讓我們分兩種情況進行說明:

  1. 將普通函數定義為內聯:眾所周知,JVM內部已經實現了內聯優化,它會在任何可以通過內聯來提升性能的地方將函數調用內聯化,并且相對于手動將普通函數定義為內聯,通過JVM內聯優化所生成的字節碼,每個函數的實現只會出現一次,這樣在保證減少運行時開銷的同時,也沒有增加字節碼的尺寸;所以我們可以得出結論,對于普通函數,我們沒有必要將其聲明為內聯函數,而是交給JVM自行優化。
  2. 將帶有lambda參數的函數定義為內聯:是的,這種情況下確實可以提高性能;但在使用的過程中,我們會發現它是有諸多限制的,讓我們從下面的例子開始展開說明:

假如我們這樣調用doSomething:

上面的調用會被編譯成:

從上面編譯的結果可以看出,無論doSomething函數還是action參數都被內聯了,很棒,那讓我們換一種調用方式:

上面的調用會被編譯成:

doSomething函數被內聯,而action參數沒有被內聯,這是因為以函數型變量的形式傳遞給doSomething的lambda在函數的調用點是不可用的,只有等到doSomething被內聯后,該lambda才可以正常使用。

通過上面的例子,我們對lambda表達式何時被內聯做一下簡單的總結:

  1. 當lambda表達式以參數的形式直接傳遞給內聯函數,那么lambda表達式的代碼會被直接替換到最終生成的代碼中。
  2. 當lambda表達式在某個地方被保存起來,然后以變量形式傳遞給內聯函數,那么此時的lambda表達式的代碼將不會被內聯。

上面對lambda的內聯時機進行了討論,消化片刻后讓我們再看最后一個例子:

上面的例子是否有問題?是的,編譯器會拋出“Illegal usage of inline-parameter”的錯誤,這是因為Kotlin規定內聯函數中的lambda參數只能被直接調用或者傳遞給另外一個內聯函數,除此之外不能作為他用;那我們如果確實想要將某一個lambda傳遞給一個非內聯函數怎么辦?我們只需將上述代碼這樣改造即可:

很簡單,在不需要內聯的lambda參數前加上noinline修飾符就可以了。

以上便是我對內聯函數的全部理解,通過掌握該特性的運行機制,相信大家可以做到在正確的時機使用該特性,而非濫用或因恐懼棄而不用。

Kotlin下單例模式

餓漢式實現

懶漢式

上述代碼中,我們可以發現在Kotlin實現中,我們讓其主構造函數私有化并自定義了其屬性訪問器,其余內容大同小異。

  • 如果有小伙伴不清楚Kotlin構造函數的使用方式。請點擊 - - - 構造函數
  • 不清楚Kotlin的屬性與訪問器,請點擊 - - -屬性和字段

線程安全的懶漢式

大家都知道在使用懶漢式會出現線程安全的問題,需要使用使用同步鎖,在Kotlin中,如果你需要將方法聲明為同步,需要添加@Synchronized注解。

雙重校驗鎖式

哇!小伙伴們驚喜不,感不感動啊。我們居然幾行代碼就實現了多行的Java代碼。其中我們運用到了Kotlin的延遲屬性 Lazy。

Lazy內部實現

觀察上述代碼,因為我們傳入的mode = LazyThreadSafetyMode.SYNCHRONIZED,
那么會直接走 SynchronizedLazyImpl,我們繼續觀察SynchronizedLazyImpl。

Lazy接口

SynchronizedLazyImpl實現了Lazy接口,Lazy具體接口如下:

繼續查看SynchronizedLazyImpl,具體實現如下:

SynchronizedLazyImpl內部實現

通過上述代碼,我們發現 SynchronizedLazyImpl 覆蓋了Lazy接口的value屬性,并且重新了其屬性訪問器。其具體邏輯與Java的雙重檢驗是類似的。

到里這里其實大家還是肯定有疑問,我這里只是實例化了SynchronizedLazyImpl對象,并沒有進行值的獲取,它是怎么拿到高階函數的返回值呢?。這里又涉及到了委托屬性

委托屬性語法是:val/var <屬性名>: <類型> by <表達式>。在 by 后面的表達式是該 委托, 因為屬性對應的 get()(和 set())會被委托給它的 getValue() 和 setValue() 方法。屬性的委托不必實現任何的接口,但是需要提供一個 getValue() 函數(和 setValue()——對于 var 屬性)。

而Lazy.kt文件中,聲明了Lazy接口的getValue擴展函數。故在最終賦值的時候會調用該方法。

靜態內部類式

靜態內部類的實現方式,也沒有什么好說的。Kotlin與Java實現基本雷同。

補充

在該篇文章結束后,有很多小伙伴咨詢,如何在Kotlin版的Double Check,給單例添加一個屬性,這里我給大家提供了一個實現的方式。(不好意思,最近才抽出時間來解決這個問題)

其中關于?:操作符,如果 ?: 左側表達式非空,就返回其左側表達式,否則返回右側表達式。請注意,當且僅當左側為空時,才會對右側表達式求值。

Kotlin 智能類型轉換

對于子父類之間的類型轉換

  • 先看這樣一段 Java 代碼
  • 盡管在 main 函數中,對 person 這個對象進行了類型判斷,但是在使用的時候還是需要強制轉換成 Student 類型,這樣是不是很不智能?
  • 同樣的情況在 Kotlin 中就變得簡單多了
  • 在 Kotlin 中,只要對類型進行了判斷,就可以直接通過父類的對象去調用子類的函數了

安全的類型轉換

  • 還是上面的那個例子,如果我們沒有進行類型判斷,并且直接進行強轉,會怎么樣呢?
  • 結果就只能是 Exception in thread "main" java.lang.ClassCastException
  • 那么在 Kotlin 中是不是會有更好的解決方法呢?
  • 在轉換操作符后面添加一個 ?,就不會把程序 crash 掉了,當轉化失敗的時候,就會返回一個 null

在空類型中的智能轉換

  • 需要提前了解 Kotlin 類型安全的相關知識(Kotlin 中的類型安全(對空指針的優化處理))
  • aString 在定義的時候定義成了有可能為 null,按照之前的寫法,我們需要這樣寫
  • 但是已經進行了是否為 String 類型的判斷,所以就一定 不是 空類型了,也就可以直接輸出它的長度了

T.()->Unit 、 ()->Unit

在做kotlin開發中,經常看到一些系統函數里,用函數作為參數

.()-Unit與()->Unit的區別是我們調用時,在代碼塊里面寫this,的時候,兩個this代表的含義不一樣,T.()->Unit里的this代表的是自身實例,而()->Unit里,this代表的是外部類的實例


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

安卓版28杠游戏