天天看點

java中通路不可視字段_Kotlin與Java的不同之處

code小生,一個專注 Android 領域的技術平台

公衆号回複 Android 加入我的安卓技術群

作者:小村醫

連結:https://www.jianshu.com/p/f7deb4fe6427

聲明:本文已獲

小村醫

授權發表,轉發等請聯系原作者授權

伴生對象

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

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

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

val instance = MyClass.create()
           

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

伴生對象的作用

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

在 Java 代碼中調用伴生對象

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

  • 如果聲明伴生對象有名稱,則使用:
  • 如果聲明伴生對象無名稱,則采用 Companion 關鍵字調用:
.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 所修飾的變量。如下代碼:

companion 
           

擴充屬性和擴充方法

擴充函數

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

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

package com.binzi.kotlin
           

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

擴充函數的導入

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

import 
           

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

import com.binzi.kotlin.toInt as toInteger
           

擴充函數不可覆寫

擴充方法的原理

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

如:

fun String?.toInt(): 
           

反編譯為對應的Java代碼:

public 
           

擴充屬性

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

為MutableList擴充一個firstElement屬性:

var 
           

反編譯後的java代碼如下:

内部類

kotlin的内部類與java的内部類有點不同java的内部類可以直接通路外部類的成員,kotlin的内部類不能直接通路外部類的成員,必須用inner标記之後才能通路外部類的成員

  • 沒有使用inner标記的内部類

反編譯後的java代碼

public 
           
  • 用inner标記的内部類

反編譯後的java代碼

public 
           

從上面可以看出,沒有使用inner标記的内部類最後生成的是靜态内部類,而使用inner标記的生成的是非靜态内部類

匿名内部類

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

//java,匿名内部類的寫法
           

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

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

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

print(
           

資料類

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

  • Java
public 
           

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

  • Kotlin

    在Kotlin中,可以通過關鍵字data來生成資料類:

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. 枚舉值分别實作接口的抽象成員
enum 
           
  1. 枚舉類統一實作接口的抽象成員
enum 
           
  1. 分别實作抽象枚舉類抽象成員
enum 
           

委托

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

Java中委托:

Kotlin:

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

委托屬性

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

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

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

委托屬性的文法是:

var : 
           

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

标準委托:

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

  1. 延遲屬性 Lazy

    lazy() 接受一個 lambda 并傳回一個 Lazy

    執行個體的函數,傳回的執行個體可以作為實作延遲屬性的委托:第一次調用 get() 會執行已傳遞給 lazy() 的 lambda 表達式并記錄結果, 後續調用 get() 隻是傳回記錄的結果。例如:

val lazyValue: String 
           
  1. 可觀察屬性 Observable

    Delegates.observable() 接受兩個參數:初始值和修改時處理程式(handler)。每當我們給屬性指派時會調用該處理程式(在指派後執行)。它有三個參數:被指派的屬性、舊值和新值:

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

  1. 把屬性存在map 中

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

例如:

class User(map: Map
           

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

内聯函數

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

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

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

假如我們這樣調用doSomething:

上面的調用會被編譯成:

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

上面的調用會被編譯成:

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

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

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

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

inline 
           

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

inline 
           

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

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

Kotlin下單例模式

餓漢式實作

//Java實作
           

懶漢式

//Java實作
           

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

  • 如果有小夥伴不清楚Kotlin構造函數的使用方式。請點選 - - - 構造函數
  • 不清楚Kotlin的屬性與通路器,請點選 - - -屬性和字段

線程安全的懶漢式

//Java實作
           

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

雙重校驗鎖式

//Java實作
           

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

Lazy内部實作

public 
           

觀察上述代碼,因為我們傳入的mode = LazyThreadSafetyMode.SYNCHRONIZED,

那麼會直接走 SynchronizedLazyImpl,我們繼續觀察SynchronizedLazyImpl。

Lazy接口

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

public 
           

繼續檢視SynchronizedLazyImpl,具體實作如下:

SynchronizedLazyImpl内部實作

private 
           

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

到裡這裡其實大家還是肯定有疑問,我這裡隻是執行個體化了SynchronizedLazyImpl對象,并沒有進行值的擷取,它是怎麼拿到高階函數的傳回值呢?。這裡又涉及到了委托屬性。

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

而Lazy.kt檔案中,聲明了Lazy接口的getValue擴充函數。故在最終指派的時候會調用該方法。

internal.InlineOnly
           

靜态内部類式

//Java實作
           

靜态内部類的實作方式,也沒有什麼好說的。Kotlin與Java實作基本雷同。

補充

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

class SingletonDemo private constructor(
           

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

Kotlin 智能類型轉換

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

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

安全的類型轉換

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

在空類型中的智能轉換

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

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

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

public 
           

.()-Unit與()->Unit的差別是我們調用時,在代碼塊裡面寫this,的時候,兩個this代表的含義不一樣,T.()->Unit裡的this代表的是自身執行個體,而()->Unit裡,this代表的是外部類的執行個體。

推薦閱讀

對 Kotlin 與 Java 程式設計語言的思考

使用 Kotlin 做開發一個月後的感想

java中通路不可視字段_Kotlin與Java的不同之處

掃一掃 關注我的公衆号 如果你想要跟大家分享你的文章,歡迎投稿~