從Java角度深入理解Kotlin

Chiclaim發表於2019-01-01

前言

前幾個月,在組內分享了關於Kotlin相關的內容。但由於PPT篇幅的原因,有些內容講的也不是很詳細。另外本人也參與了 碼上開學,該社群主要用於分享 KotlinJetpack 相關的技術, 如果您對Kotlin或者Jetpack使用上有想要分享的地方,也歡迎您一起來完善該社群。

為了方便大家對本文有一個大概的瞭解,先總的說下本文主要講 Kotlin 哪些方面的內容(下面的目錄和我在組內分享時PPT目錄是類似的):

  1. Kotlin資料型別、訪問修飾符
    1. Kotlin和Java資料型別對比
    2. Kotlin和Java訪問修飾符對比
  2. Kotlin中的Class和Interface
    1. Kotlin中宣告類的幾種方式
    2. Kotlin中interface原理分析
  3. lambda 表示式
    1. lambda 初體驗
    2. 定義 lambda 表示式
    3. Member Reference
    4. 常用函式 let、with、run、apply 分析
    5. lambda 原理分析
  4. 高階函式
    1. 高階函式的定義
    2. 高階函式的原理分析
    3. 高階函式的優化
  5. Kotlin泛型
    1. Java 泛型:不變、協變、逆變
    2. Kotlin 中的協變、逆變
    3. Kotlin 泛型擦除和具體化
  6. Kotlin集合
    1. kotlin 集合建立方式有哪些
    2. kotlin 集合的常用的函式
    3. Kotlin 集合 Sequence 原理
  7. Koltin 和 Java 互動的一些問題
  8. 總結

Kotlin 資料型別、訪問修飾符

為什麼要講下 Kotlin 資料型別和訪問修飾符修飾符呢?因為 Kotlin 的資料型別和訪問修飾符和 Java 的還是有些區別的,所以單獨拎出來說一下。

Kotlin 資料型別

我們知道,在 Java 中的資料型別分基本資料型別和基本資料型別對應的包裝型別。如 Java 中的整型 int 和它對應的 Integer包裝型別。

在 Kotlin 中是沒有這樣的區分的,例如對於整型來說只有 Int 這一個型別,Int 是一個類(姑且把它當裝包裝型別),我們可以說在 Kotlin 中在編譯前只有包裝型別,為什麼說是編譯前呢?因為編譯時會根據情況把這個整型( Int )是編譯成 Java 中的 int 還是 Integer。 那麼是根據哪些情況來編譯成基本型別還是包裝型別呢,後面會講到。我們先來看下 Kotlin和 Java 資料型別對比:

Java基本型別 Java包裝型別 Kotlin對應
char java.lang.Character kotlin.Char
byte java.lang.Byte kotlin.Byte
short java.lang.Short kotlin.Short
int java.lang.Integer kotlin.Int
float java.lang.Float kotlin.Float
double java.lang.Double Kotlin.Double
long java.lang.Long kotlin.Long
boolean java.lang.Boolean kotlin.Boolean

下面來分析下哪些情況編譯成Java中的基本型別還是包裝型別。下面以整型為例,其他的資料型別同理。

1. 如果變數可以為null(使用操作符?),則編譯後是包裝型別


//因為可以為 null,所以編譯後為 Integer
var width: Int? = 10
var width: Int? = null

//編譯後的程式碼

@Nullable
private static Integer width = 10;
@Nullable
private static Integer width;


再來看看方法返回值為整型:


//返回值 Int 編譯後變成基本型別 int
fun getAge(): Int {
    return 0
}

//返回值 Int 編譯後變成 Integer
fun getAge(): Int? {
    return 0
}

複製程式碼

所以宣告變數後者方法返回值的時候,如果宣告可以為 null,那麼編譯後時是包裝型別,反之就是基本型別。

2. 如果使用了泛型則編譯後是包裝型別,如集合泛型、陣列泛型等


//集合泛型
//集合裡的元素都是 Integer 型別
fun getAge3(): List<Int> {
    return listOf(22, 90, 50)
}

//陣列泛型
//會編譯成一個 Integer[]
fun getAge4(): Array<Int> {
    return arrayOf(170, 180, 190)
}

//看下編譯後的程式碼:

@NotNull
public static final List getAge3() {
  return CollectionsKt.listOf(new Integer[]{22, 90, 50});
}

@NotNull
public static final Integer[] getAge4() {
  return new Integer[]{170, 180, 190};
}

複製程式碼

3. 如果想要宣告的陣列編譯後是基本型別的陣列,需要使用 xxxArrayOf(...),如 intArrayOf

從上面的例子中,關於集合泛型編譯後是包裝型別在 Java 中也是一樣的。如果想要宣告的陣列編譯後是基本型別的陣列,需要使用 Kotlin 為我們提供的方法:

//會編譯成一個int[]
fun getAge5(): IntArray {
    return intArrayOf(170, 180, 190)
}

當然,除了intArrayOf,還有charArrayOf、floatArrayOf等等,就不一一列舉了。

複製程式碼

4. 為什麼 Kotlin 要單獨設計一套這樣的資料型別,不共用 Java 的那一套呢?

我們都知道,Kotlin 是基於 JVM 的一款語言,編譯後還是和 Java 一樣。那麼為什麼不像集合那樣直接使用 Java 那一套,要單獨設計一套這樣的資料型別呢?

Kotlin 中沒有基本資料型別,都是用它自己的包裝型別,包裝型別是一個類,那麼我們就可以使用這個類裡面很多有用的方法。下面看下 Kotlin in Action 的一段程式碼:

fun showProgress(progress: Int) {
    val percent = progress.coerceIn(0, 100)
    println("We're $percent% done!")
}

編譯後的程式碼為:

public static final void showProgress(int progress) {
  int percent = RangesKt.coerceIn(progress, 0, 100);
  String var2 = "We're " + percent + "% done!";
  System.out.println(var2);
}

複製程式碼

從中可以看出,在開發階段我們可很方便地使用 Int 類擴充套件函式。編譯後,依然編譯成基本型別 int,使用到的擴充套件函式的邏輯也會包含在內。

關於 Kotlin 中的資料型別就講到這裡,下面來看下訪問修飾符

Kotlin 訪問修飾符

我們知道訪問修飾符可以修飾類,也可以修飾類的成員。下面通過兩個表格來對比下 Kotlin 和 Java 在修飾類和修飾類成員的異同點:

表格一:類訪問修飾符:

類訪問修飾符 Java可訪問級別 Kotlin可訪問級別
public 均可訪問 均可訪問
protected 同包名 同包名也不可訪問
internal 不支援該修飾符 同模組內可見
default 同包名下可訪問 相當於public
private 當前檔案可訪問 當前檔案可訪問

表格二:類成員訪問修飾符:

成員修飾符 Java可訪問級別 Kotlin可訪問級別
public 均可訪問 均可訪問
protected 同包名或子類可訪問 只有子類可訪問
internal 不支援該修飾符 同模組內可見
default 同包名下可訪問 相當於public
private 當前檔案可訪問 當前檔案可訪問

通過以上兩個表格,有幾點需要講一下。

1. internal 修飾符是 Kotlin 獨有而 Java 中沒有的

internal 修飾符意思是隻能在當前模組訪問,出了當前模組不能被訪問。

需要注意的是,如果 A 類是 internal 修飾,B 類繼承 A 類,那麼 B 類也必須是 internal 的,因為如果 kotlin 允許 B 類宣告成public 的,那麼 A 就間接的可以被其他模組的類訪問。

也就是說在 Kotlin 中,子類不能放大父類的訪問許可權。類似的思想在 protected 修飾符中也有體現,下面會講到。

2. protected 修飾符在Kotlin和Java中的異同點

1) protected 修飾類

我們知道,如果 protected 修飾類,在 Java 中該類只能被同包名下的類訪問。

這樣也可能產生一些問題,比如某個庫中的類 A 是 protected 的,開發者想訪問它,只需要宣告一個類和類A相同包名即可。

而在 Kotlin 中就算是同包名的類也不能訪問 protected 修飾的類。

為了測試 protected 修飾符修飾類,我在寫demo的時候,發現 protected 修飾符不能修飾頂級類,只能放在內部類上。

為什麼不能修飾頂級類?

一方面,在 Java 中 protected 修飾的類,同包名可以訪問,default 修飾符已經有這個意思了,把頂級類再宣告成 protected 沒有什麼意義。

另一方面,在 Java 中 protected 如果修飾類成員,除了同包名可以訪問,不同包名的子類也可以訪問,如果把頂級類宣告成protected,也不會存在不同包名的子類了,因為不同包名無法繼承 protected 類

在 Kotlin 中也是一樣的,protected 修飾符也不能修飾頂級類,只能修飾內部類。

在 Kotlin 中,同包名不能訪問 protected 類,如果想要繼承 protected 類,需要他們在同一個內部類下,如下所示:

open class ProtectedClassTest {

    protected open class ProtectedClass {
        open fun getName(): String {
            return "chiclaim"
        }
    }

    protected class ProtectedClassExtend : ProtectedClass() {
        override fun getName(): String {
            return "yuzhiqiang"
        }
    }

}

複製程式碼

除了在同一內部類下,可以繼承 protected 類外,如果某個類的外部類和 protected 類的外部類有繼承關係,這樣也可以繼承protected 類

class ExtendKotlinProtectedClass2 : ProtectedClassTest() {
    
    private var protectedClass: ProtectedClass? = null

    //繼承protected class
    protected class A : ProtectedClass() {

    }
}

複製程式碼

需要注意的是,繼承 protected 類,那麼子類也必須是 protected,這一點和 internal 是類似的。Kotlin 中不能放大訪問許可權,能縮小訪問許可權嗎?答案是可以的。

可能有人會問,既然同包名都不能訪問 protected 類,那麼這個類跟私有的有什麼區別?確實,如果外部類沒有宣告成 open,編譯器也會提醒我們此時的 protected 就是 private

所以在 Kotlin 中,如果要使用 protected 類,需要把外部宣告成可繼承的 (open),如:

//繼承 ProtectedClassTest
class ExtendKotlinProtectedClass2 : ProtectedClassTest() {
    //可以使用 ProtectedClassTest 中的 protected 類了
    private var protectedClass: ProtectedClass? = null
}

複製程式碼
2) protected修飾類成員

如果 protected 修飾類成員,在 Java 中可以被同包名或子類可訪問;在 Kotlin 中只能被子類訪問。

這個比較簡單就不贅述了

3) 訪問修飾符小結
  1. 如果不寫訪問修飾符,在 Java 中是 default 修飾符 (package-private);在 Kotlin 中是 public 的
  2. internal 訪問修飾符是 Kotlin 獨有,只能在模組內能訪問的到
  3. protected 修飾類的時候,不管是 Java 和 Kotlin 都只能放到內部類上
  4. 在 Kotlin 中,要繼承 protected 類,要麼子類在同一內部類名下;要麼該類的的外部類和 protected 類的外部類有繼承關係
  5. 在 Kotlin 中,繼承 protected 類,子類也必須是 protected 的
  6. 在 Kotlin 中,對於 protected 修飾符,去掉了同包名能訪問的特性
  7. 如果某個 Kotlin 類能夠被繼承,需要 open 關鍵字,預設是 final 的

雖然Kotlin的資料型別和訪問修飾符比較簡單,還是希望大家能夠動手寫些demo驗證下,這樣可能會有意想不到的收穫。你也可以訪問我的 github 上面有比較詳細的測試 demo,有需要的可以看下。

Kotlin 中的 Class 和 Interface

Kotlin 中宣告類的幾種方式

在實際的開發當中,經常需要去新建類。在 Kotlin 中有如下幾種宣告類的方式:

1) class className

這種方式和 Java 類似,通過 class 關鍵字來宣告一個類。不同的是,這個類是 public final 的,不能被繼承。


class Person

編譯後:

public final class Person {

}

複製程式碼

2) class className([var/val] property: Type...)

這種方式和上面一種方式多加了一組括號,代表建構函式,我們把這樣的建構函式稱之為 primary constructor。這種方式宣告一個類的主要做了一下幾件事:

  1. 會生成一個構造方法,引數就是括號裡的那些引數
  2. 會根據括號的引數生成對應的屬性
  3. 會根據 val 和 var 關鍵字來生成 setter、getter 方法

var 和 val 關鍵字:var 表示該屬性可以被修改;val 表示該屬性不能被修改

class Person(val name: String) //name屬性不可修改

---編譯後---

public final class Person {
   //1. 生成 name 屬性
   @NotNull
   private final String name;

   //2. 生成 getter 方法
   //由於 name 屬性不可修改,所以不提供 name 的 setter 方法
   @NotNull
   public final String getName() {
      return this.name;
   }
   
   //3. 生成建構函式
   public Person(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
}

複製程式碼

如果我們把 name 修飾符改成 var,編譯後會生成 getter 和 setter 方法,同時也不會有 final 關鍵字來修飾 name 屬性

如果這個 name 不用 var 也不用 val 修飾, 那麼不會生成屬性,自然也不會生成 getter 和 setter 方法。不過可以在 init程式碼塊 裡進行初始化, 否則沒有什麼意義。

class Person(name: String) {

    //會生成 getter 和 setter 方法
    var name :String? =null

    //init 程式碼塊會在構造方法裡執行
    init {
        this.name = name
    }
}

----編譯後

public final class Person {
   @Nullable
   private String name;

   @Nullable
   public final String getName() {
      return this.name;
   }

   public final void setName(@Nullable String var1) {
      this.name = var1;
   }

   public Person(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
}

複製程式碼

從上面的程式碼可知,init 程式碼塊 的執行時機是建構函式被呼叫的時候,編譯器會把 init 程式碼塊裡的程式碼 copy 到建構函式裡。 如果有多個建構函式,那麼每個建構函式裡都會有 init 程式碼塊的程式碼,但是如果建構函式裡呼叫了另一個過載的建構函式,init 程式碼塊只會被包含在被呼叫的那個建構函式裡。 說白了,構造物件的時候,init 程式碼塊裡的邏輯只有可能被執行一次。

3) class className constructor([var/val] property: Type...)

該種方式和上面是等價的,只是多加了 constructor 關鍵字而已

4) 類似 Java 的方式宣告建構函式

不在類名後直接宣告建構函式 ,在類的裡面再宣告建構函式。我們把這樣的建構函式稱之為 secondary constructor

class Person {
    var name: String? = null
    var id: Int = 0

    constructor(name: String) {
        this.name = name
    }

    constructor(id: Int) {
        this.id = id
    }
}
複製程式碼

primary constructor 裡的引數是可以被 var/val 修飾,而 secondary constructor 裡的引數是不能被 var/val 修飾的

secondary constructor 用的比較少,用得最多的還是 primary constructor

5) data class className([var/val] property: Type)

新建 bean 類的時候,常常需要宣告 equals、hashCode、toString 等方法,我們需要寫很多程式碼。在 Kotlin 中,只需要在宣告類的時候前面加 data 關鍵字就可以完成這些功能。

節省了很多程式碼篇幅。需要注意的是,那麼哪些屬性參與 equals、hashCode、toString 方法呢? primary constructor 建構函式裡的引數,都會參與 equals、hashCode、toString 方法裡。

這個也比較簡單,大家可以利用 Kotlin 外掛,檢視下反編譯後的程式碼即可。由於篇幅原因,在這裡就不貼出來了。

6) object className

這種方法宣告的類是一個單例類,以前在Java中新建一個單例類,需要寫一些模板程式碼,在Kotlin中一行程式碼就可以了(類名前加上object關鍵字)

在 Kotlin 中 object 關鍵字有很多用法,等介紹完了 Kotlin 新建類方式後,單獨彙總下 object 關鍵字的用法。

7) Kotlin 新建內部類

在 Kotlin 中內部類預設是靜態的( Java 與此相反),不持有外部類的引用,如:

class OuterClass {

    //在 Kotlin 中內部類預設是靜態的,不持有外部類的引用
    class InnerStaticClass{
    }

    //如果要宣告非靜態的內部類,需要加上 inner 關鍵字
    inner class InnerClass{
    }
}

編譯後程式碼如下:

class OuterClass {

   public static final class InnerStaticClass {
   }

   public final class InnerClass {
   }
}
複製程式碼

8) sealed class className

當我們使用 when 語句通常需要加 else 分支,如果新增了新的型別分支,忘記了在 when 語句裡進行處理,遇到新分支,when 語句就會走 else 邏輯

sealed class 就是用來解決這個問題的。如果有新的型別分支且沒有處理編譯器就會報錯。

sealed-class.png

當 when 判斷的是 sealed class,那麼不需要加 else 預設分支,如果有新的子類,編譯器會通過編譯報錯的方式提醒開發者新增新分支,從而保證邏輯的完整性和正確性

需要注意的是,當 when 判斷的是 sealed class,千萬不要新增 else 分支,否則有新類編譯器也不會提醒

sealed class 實際上是一個抽象類且不能被繼承,構造方法是私有的。

object 關鍵字用法彙總

除了上面我們介紹的,object 關鍵字定義單例類外,object 關鍵字還有以下幾種用法:

1) companion object

我們把 companion object 稱之為伴生物件,伴生體裡面放的是一些靜態成員:如靜態常量、靜態變數、靜態方法

companion object 需要定義在一個類的內部,裡面的成員都是靜態的。如下所示:

class ObjectKeywordTest {
    //伴生物件
    companion object {
       
    }
}

複製程式碼

需要注意的是,在伴生體裡面不同定義的方式有不同的效果,雖然他們都是靜態的:

companion object {
    //公有常量
    const val FEMALE: Int = 0
    const val MALE: Int = 1

    //私有常量
    val GENDER: Int = FEMALE

    //私有靜態變數
    var username: String = "chiclaim"
    
    //靜態方法
    fun run() {
        println("run...")
    }
}

複製程式碼
  1. 如果使用 val 來定義,而沒有使用 const 那麼該屬性是一個私有常量
  2. 如果使用 const 和 val 來定義則是一個公共常量
  3. 如果使用 var 來定義,則是一個靜態變數

雖然只是一個關鍵字的差別,但是最終編譯出的結果還是有細微的差別的,在開發中注意下就可以了。

我們來看下上面程式碼編譯之後對應的 Java 程式碼:

class ObjectKeywordTest {
   //公有常量
   public static final int FEMALE = 0;
   public static final int MALE = 1;
   //私有常量
   private static final int gender = 1;
   //靜態變數
   @NotNull
   private static String username = "chiclaim";

   public static final ObjectKeywordTest.Companion Companion = new ObjectKeywordTest.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
   
      public final void run() {
         String var1 = "run...";
         System.out.println(var1);
      }
      public final int getGENDER() {
         return ObjectKeywordTest.GENDER;
      }

      @NotNull
      public final String getUsername() {
         return ObjectKeywordTest.username;
      }

      public final void setUsername(@NotNull String var1) {
         Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
         ObjectKeywordTest.username = var1;
      }

      private Companion() {
      }
   }
}

複製程式碼

我們發現會生成一個名為 Companion 的內部類,如果伴生體裡是方法,則該方法定義在該內部類中,如果是屬性則定義在外部類裡。如果是私有變數在內部類中生成 getter 方法。

同時還會在外部宣告一個名為 Companion 的內部類物件,用來訪問這些靜態成員。伴生物件的預設名字叫做 Companion,你也可以給它起一個名字,格式為:

companion object YourName{
    
}
複製程式碼

除了給這個伴生物件起一個名字,還可以讓其實現介面,如:

class ObjectKeywordTest4 {
    //實現一個介面
    companion object : IAnimal {
        override fun eat() {
            println("eating apple")
        }
    }
}

fun feed(animal: IAnimal) {
    animal.eat()
}

fun main(args: Array<String>) {
    //把類名當作引數直接傳遞
    //實際傳遞的是靜態物件 ObjectKeywordTest4.Companion
    //每個類只會有一個伴生物件
    feed(ObjectKeywordTest4)
}
複製程式碼

2) object : className 建立匿名內部類物件

如下面的例子,建立一個 MouseAdapter 內部類物件:

jLabel.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent?) {
        super.mouseClicked(e)
        println("mouseClicked")
    }

    override fun mouseMoved(e: MouseEvent?) {
        super.mouseMoved(e)
        println("mouseMoved")
    }
})

複製程式碼

至此,object 關鍵字有 3 種用法

  1. 定義單例類,格式為:object className
  2. 定義伴生物件,格式為:companion object
  3. 建立匿名內部類物件,格式為:object : className

Kotlin 中的 Interface

我們都知道,在 Java8 之前,Interface 中是不能包含有方法體的方法和屬性,只能包含抽象方法和常量。

在 Kotlin 中的介面在定義的時候可以包含有方法體的方法,也可以包含屬性。

//宣告一個介面,包含方法體的方法 plus 和一個屬性 count
interface InterfaceTest {

    var count: Int

    fun plus(num: Int) {
        count += num
    }

}

//實現該介面
class Impl : InterfaceTest {
    //必須要覆蓋 count 屬性
    override var count: Int = 0
}
複製程式碼

我們來看下底層 Kotlin 介面是如何做到在介面中包含有方法體的方法、屬性的。

public interface InterfaceTest {
   //會為我們生成三個抽象方法:屬性的 getter 和 setter 方法、plus 方法
   int getCount();

   void setCount(int var1);

   void plus(int var1);

   //定義一個內部類,用於存放有方法體的方法
   public static final class DefaultImpls {
      public static void plus(InterfaceTest $this, int num) {
         $this.setCount($this.getCount() + num);
      }
   }
}

//實現我們上面定義的介面
public final class Impl implements InterfaceTest {
   private int count;

   public int getCount() {
      return this.count;
   }

   public void setCount(int var1) {
      this.count = var1;
   }
   
   //Kotlin 會自動為我們生成 plus 方法,方法體就是上面內部類封裝好的 plus 方法
   public void plus(int num) {
      InterfaceTest.DefaultImpls.plus(this, num);
   }
}

複製程式碼

通過反編譯,Kotlin 介面裡可以定義有方法體的方法也沒有什麼好神奇的。 就是通過內部類封裝好了帶有方法體的方法,然後實現類會自動生成方法

這個特性還是挺有用的,當我們不想是使用抽象類時,具有該特性的 Interface 就派上用場了

lambda 表示式

在 Java8 之前,lambda 表示式在 Java 中都是沒有的,下面我們來簡單的體驗一下 lambda 表示式:

//在Android中為按鈕設定點選事件
button.setOnClickListener(new View.OnClickListener(){
    @override
    public void onClick(View v){
        //todo something
    }
    
});

//在Kotlin中使用lambda
button.setOnClickListener{view ->
    //todo something
}

複製程式碼

可以發現使用 lambda 表示式,程式碼變得非常簡潔。下面我們就來深入探討下 lambda 表示式。

什麼是 lambda 表示式

我們先從 lambda 最基本的語法開始,引用一段 Kotlin in Action 中對 lambda 的定義:

lambda.png

總的來說,主要有 3 點:

  1. lambda 總是放在一個花括號裡 ({})
  2. 箭頭左邊是 lambda 引數 (lambda parameter)
  3. 箭頭右邊是 lambda 體 (lambda body)

我們再來看上面簡單的 lambda 例項:

button.setOnClickListener{view -> //view是lambda引數
    //lambda體
    //todo something
}
複製程式碼

lambda 表示式與 Java 的 functional interface

上面的 OnClickListener 介面和 Button 類是定義在 Java 中的。

該介面只有一個抽象方法,在 Java 中這樣的介面被稱作 functional interfaceSAM (single abstract method)

因為我們在實際的工作中可能和 Java 定義的 API 打的交道最多了,因為 Java 這麼多年的生態,我們無處不再使用 Java 庫,

所以在 Kotlin 中,如果某個方法的引數是 Java 定義的 functional interface,Kotlin 支援把 lambda 當作引數進行傳遞的。

需要注意的是,Kotlin 這樣做是指方便的和 Java 程式碼進行互動。但是如果在 Kotlin 中定義一個方法,它的引數型別是functional interface,是不允許直接將 lambda 當作引數進行傳遞的。如:

//在Kotlin中定義一個方法,引數型別是Java中的Runnable
//Runnable是一個functional interface
fun postDelay(runnable: Runnable) {
    runnable.run()
}

//把lambda當作引數傳遞是不允許的
postDelay{
   println("postDelay")
}
複製程式碼

在 Kotlin 中呼叫 Java 方法,能夠將 lambda 當作引數傳遞,需要滿足兩個條件:

  1. 該 Java 方法的引數型別是 functional interface (只有一個抽象方法)
  2. 該 functional interface 是 Java 定義的,如果是 Kotlin 定義的,就算該介面只有一個抽象方法,也是不行的

如果 Kotlin 定義了方法想要像上面一樣,把 lambda 當做引數傳遞,可以使用高階函式。這個後面會介紹。

Kotlin 允許 lambda 當作引數傳遞,底層也是通過構建匿名內部類來實現的:

fun main(args: Array<String>) {
    val button = Button()
    button.setOnClickListener {
        println("click 1")
    }

    button.setOnClickListener {
        println("click 2")
    }
}

//編譯後對應的 Java 程式碼:

public final class FunctionalInterfaceTestKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Button button = new Button();
      button.setOnClickListener((OnClickListener)null.INSTANCE);
      button.setOnClickListener((OnClickListener)null.INSTANCE);
   }
}

複製程式碼

發現反編譯後對應的 Java 程式碼有的地方可讀性也不好,這是 Kotlin 外掛的 bug,比如 (OnClickListener)null.INSTANCE

所以這個時候需要看下它的 class 位元組碼:

//內部類1
final class lambda/FunctionalInterfaceTestKt$main$1 implements lambda/Button$OnClickListener{
    public final static Llambda/FunctionalInterfaceTestKt$main$1; INSTANCE
    //...
}

//內部類2
final class lambda/FunctionalInterfaceTestKt$main$2 implements lambda/Button$OnClickListener{
    public final static Llambda/FunctionalInterfaceTestKt$main$2; INSTANCE
    //...
}

//main函式
  // access flags 0x19
  public final static main([Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "args"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 10 L1
    NEW lambda/Button
    DUP
    INVOKESPECIAL lambda/Button.<init> ()V
    ASTORE 1
   L2
    LINENUMBER 11 L2
    ALOAD 1
    GETSTATIC lambda/FunctionalInterfaceTestKt$main$1.INSTANCE : Llambda/FunctionalInterfaceTestKt$main$1;
    CHECKCAST lambda/Button$OnClickListener
    INVOKEVIRTUAL lambda/Button.setOnClickListener (Llambda/Button$OnClickListener;)V

複製程式碼

從中可以看出,它會新建 2 個內部類,內部類會暴露一個 INSTANCE 例項供外界使用。

也就是說傳遞 lambda 引數多少次,就會生成多少個內部類

但是不管這個 main 方法呼叫多少次,一個 setOnClickListener,都只會有一個內部類物件,因為暴露出來的 INSTANCE 是一個常量

我們再來調整一下 lambda 體內的實現方式:

fun main(args: Array<String>) {
    val button = Button()
    var count = 0
    button.setOnClickListener {
        println("click ${++count}")
    }

    button.setOnClickListener {
        println("click ${++count}")
    }
}
複製程式碼

也就是 lambda 體裡面使用了外部變數了,再來看下反編譯後的 Java 程式碼:

public static final void main(@NotNull String[] args) {
  Intrinsics.checkParameterIsNotNull(args, "args");
  Button button = new Button();
  final IntRef count = new IntRef();
  count.element = 0;
  button.setOnClickListener((OnClickListener)(new OnClickListener() {
     public final void click() {
        StringBuilder var10000 = (new StringBuilder()).append("click ");
        IntRef var10001 = count;
        ++count.element;
        String var1 = var10000.append(var10001.element).toString();
        System.out.println(var1);
     }
  }));
  button.setOnClickListener((OnClickListener)(new OnClickListener() {
     public final void click() {
        StringBuilder var10000 = (new StringBuilder()).append("click ");
        IntRef var10001 = count;
        ++count.element;
        String var1 = var10000.append(var10001.element).toString();
        System.out.println(var1);
     }
  }));
}
複製程式碼

從中發現,每次呼叫 setOnClickListener 方法的時候都會 new 一個新的內部類物件

由此,我們做一個小結:

  1. 一個 lambda 對應一個內部類
  2. 如果 lambda 體裡沒有使用外部變數,則呼叫方法時只會有一個內部類物件
  3. 如果 lambda 體裡使用了外部變數,則每呼叫一次該方法都會新建一個內部類物件

lambda 表示式賦值給變數

lambda 除了可以當作引數進行傳遞,還可以把 lambda 賦值給一個變數:

//定義一個 lambda,賦值給一個變數
val sum = { x: Int, y: Int, z: Int ->
    x + y + z
}

fun main(args: Array<String>) {
    //像呼叫方法一樣呼叫lambda
    println(sum(12, 10, 15))
}

//控制檯輸出:37

複製程式碼

接下來分析來其實現原理,反編譯檢視其對應的 Java 程式碼:

public final class LambdaToVariableTestKt {
   @NotNull
   private static final Function3 sum;

   @NotNull
   public static final Function3 getSum() {
      return sum;
   }

   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      int var1 = ((Number)sum.invoke(12, 10, 15)).intValue();
      System.out.println(var1);
   }

   static {
      sum = (Function3)null.INSTANCE;
   }
}
複製程式碼

其對應的 Java 程式碼是看不到具體的細節的,而且還是會有 null.INSTANCE 的情況,但是我們還是可以看到主體邏輯。

但由 於class 位元組篇幅很大,就不貼出來了,通過我們上面的分析,INSTANCE 是一個常量,在這裡也是這樣的:

首先會新建一個內部類,該內部類實現了介面 kotlin/jvm/functions/Function3,為什麼是 Function3 因為我們定義的 lambda 只有 3 個引數。

所以 lambda 有幾個引數對應的就是 Function 幾,最多支援 22 個引數,也就是Function22。我們把這類介面稱之為 FunctionN

然後內部類實現了介面的 invoke 方法,invoke 方法體裡的程式碼就是 lambda 體的程式碼邏輯。

這個內部類會暴露一個例項常量 INSTANCE,供外界使用。

如果把上面 Kotlin 的程式碼放到一個類裡,然後在 lambda 體裡使用外部的變數,那麼每呼叫一次 sum 也會建立一個新的內部類物件,上面我們對 lambda 的小結在這裡依然是有效的。

上面 setOnClickListener 的例子,我們傳了兩個 lambda 引數,生成了兩個內部類,我們也可以把監聽事件的 lambda 賦值給一個變數:

val button = Button()
val listener = Button.OnClickListener {
    println("click event")
}
button.setOnClickListener(listener)
button.setOnClickListener(listener)

複製程式碼

這樣對於 OnClickListener 介面,只會有一個內部類。

從這個例子中我們發現,className{} 這樣的格式也能建立一個物件,這是因為介面 OnClickListener 是 SAM interface,只有一個抽象函式的介面。

編譯器會生成一個 SAM constructor,這樣便於把一個 lambda 表示式轉化成一個 functional interface 例項物件。

至此,我們又學到了另一種建立物件的方法。

做一個小結,在 Kotlin 中常規的建立物件的方式(除了反射、序列化等):

  1. 類名後面接括號,格式:className()
  2. 建立內部類物件,格式:object : className
  3. SAM constructor 方式,格式:className{}

高階函式

由於高階函式和 lambda 表示式聯絡比較緊密,在不介紹高階函式的情況下,lambda 有些內容無法講,所以在高階函式這部分,還將會繼續分析lambda表示式。

高階函式的定義

如果某個函式是以另一個函式作為引數或者返回值是一個函式,我們把這樣的函式稱之為高階函式

比如 Kotlin 庫裡的 filter 函式就是一個高階函式:

//Kotlin library filter function
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> 

//呼叫高階函式 filter,直接傳遞 lambda 表示式
list.filter { person ->
    person.age > 18
}

複製程式碼

filter 函式定義部分 predicate: (T) -> Boolean 格式有點像 lambda,但是又不是,傳參的時候又可以傳遞 lambda 表示式。

弄清這個問題之前,我們先來介紹下 function type,它格式如下:

名稱 : (引數) -> 返回值型別

  1. 冒號左邊是 function type 的名字
  2. 冒號右邊是引數
  3. 尖括號右邊是返回值

比如:predicate: (T) -> Boolean predicate 就是名字,T 泛型就是引數,Boolean 就是返回值型別

高階函式是以另一個函式作為引數或者其返回值是一個函式,也可以說高階函式引數是 function type 或者返回值是 function type

在呼叫高階函式的時候,我們可以傳遞 lambda,這是因為編譯器會把 lambda 推導成 function type

高階函式原理分析

我們定義一個高階函式到底定義了什麼?我們先來定義一個簡單的高階函式:

fun process(x: Int, y: Int, operate: (Int, Int) -> Int) {
    println(operate(x, y))
}

編譯後程式碼如下:

public static final void process(int x, int y, @NotNull Function2 operate) {
   Intrinsics.checkParameterIsNotNull(operate, "operate");
   int var3 = ((Number)operate.invoke(x, y)).intValue();
   System.out.println(var3);
}

複製程式碼

我們又看到了 FunctionN 介面了,上面介紹把 lambda 賦值給一個變數的時候講到了 FunctionN 介面

發現高階函式的 function type 編譯後也會變成 FunctionN,所以能把 lambda 作為引數傳遞給高階函式也是情理之中了

這是一個高階函式編譯後的情況,我們再來看下呼叫高階函式的情況:

//呼叫高階函式,傳遞一個 lambda 作為引數
process(a, b) { x, y ->
    x * y
}

//編譯後的位元組碼:
GETSTATIC higher_order_function/HigherOrderFuncKt$main$1.INSTANCE : Lhigher_order_function/HigherOrderFuncKt$main$1;
CHECKCAST kotlin/jvm/functions/Function2
INVOKESTATIC higher_order_function/HigherOrderFuncKt.process (IILkotlin/jvm/functions/Function2;)V

複製程式碼

發現會生成一個內部類,然後獲取該內部類例項,這個內部類實現了 FunctionN。介紹 lambda 的時候,我們說過了 lambda會編譯成 FunctionN

如果 lambda 體裡使用了外部變數,那每次呼叫都會建立一個內部類例項,而不是 INSTANCE 常量例項,這個也在介紹lambda 的時候說過了。

再探 lambda 表示式

lambda 表示式引數和 function type 引數

除了 filter,還有常用的 forEach 也是高階函式:

//list 裡是 Person 集合
//遍歷list集合
list.forEach {person -> 
    println(person.name)
}

複製程式碼

我們呼叫 forEach 函式的傳遞 lambda 表示式,lambda 表示式的引數是 person,那為什麼引數型別是集合裡的元素 Person,而不是其他型別呢?比是集合型別?

到底是什麼決定了我們呼叫高階函式時傳遞的 lambda 表示式的引數是什麼型別呢?

我們來看下 forEach 原始碼:

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}
複製程式碼

發現裡面對集合進行 for 迴圈,然後把集合元素作為引數傳遞給 action (function type)

所以,呼叫高階函式時,lambda 引數是由 function type 的引數決定的

lambda receiver

我們再看下 Kotlin 高階函式 apply,它也是一個高階函式,呼叫該函式時 lambda 引數是呼叫者本身 this

list.apply {//lambda 引數是 this,也就是 List
    println(this)
}
複製程式碼

我們看下 apply 函式的定義:

public inline fun <T> T.apply(block: T.() -> Unit): T 
複製程式碼

發現 apply 函式的的 function type 有點不一樣,block: T.() -> Unit 在括號前面有個 T.

呼叫這樣的高階函式時,lambda 引數是 this,我們把這個 this 稱之為 lambda receiver

把這類 lambda 稱之為帶有接受者的 lambda 表示式 (lambda with receiver)

這樣的 lambda 在編寫程式碼的時候提供了很多便利,呼叫所有關於 this 物件的方法 ,都不需要 this.,直接寫方法即可,如下面的屬於 StringBuilder 的 append 方法:

lambda-receiver.png

除了 apply,函式 with、run 的 lambda 引數都是 this

public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> with(receiver: T, block: T.() -> R): R

複製程式碼

它們三者都能完成彼此的功能:

//apply
fun alphabet2() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}
//with
fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know alphabet!").toString()
}
//run
fun alphabet3() = StringBuilder().run {
    for (c in 'A'..'Z') {
        append(c)
    }
    append("\nNow I know the alphabet!")
}

複製程式碼

高階函式 let、with、apply、run 總結

1) let 函式一般用於判斷是否為空
//let 函式的定義
public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

//let 的使用
message?.let { //lambda引數it是message
    val result = it.substring(1)
    println(result)
}

複製程式碼
2) with 是全域性函式,apply 是擴充套件函式,其他的都一樣
3) run 函式的 lambda 是一個帶有接受者的 lambda,而 let 不是,除此之外功能差不多
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> T.let(block: (T) -> R): R

複製程式碼

所以 let 能用於空判斷,run 也可以:

let-run.png

高階函式的優化

通過上面我們對高階函式原理的分析:在呼叫高階函式的時候 ,會生成一個內部類。

如果這個高階函式被程式中很多地方呼叫了,那麼就會有很多的內部類,那麼程式設計師的體積就會變得不可控了。

而且如果呼叫高階函式的時候,lambda 體裡使用了外部變數,則會每次建立新的物件。

所以需要對高階函式進行優化下。

上面我們在介紹 kotlin 內建的一些的高階函式如 let、run、with、apply,它們都是行內函數,使用 inline 關鍵字修飾

內聯 inline 是什麼意思呢?就是在呼叫 inline 函式的地方,編譯器在編譯的時候會把行內函數的邏輯拷貝到呼叫的地方。

依然以在介紹高階函式原理那節介紹的 process 函式為例:

//使用 inline 修飾高階函式
inline fun process(x: Int, y: Int, operate: (Int, Int) -> Int) {
    println(operate(x, y))
}


fun main(args: Array<String>) {
    val a = 11
    val b = 2
    //呼叫 inline 的高階函式
    process(a, b) { x, y ->
        x * y
    }
}

//編譯後對應的 Java 程式碼:
public static final void main(@NotNull String[] args) {
    int a = 11;
    int b = 2;
    int var4 = a * b;
    System.out.println(var4);
}
複製程式碼

Kotlin泛型

要想掌握 Kotlin 泛型,需要對 Java 的泛型有充分的理解。掌握 Java 泛型後 ,Kotlin 的泛型就很簡單了。

所以我們先來看下 Java 泛型相關的知識點:

Java 泛型:不變性 (invariance)、協變性 (covariance)、逆變性 (contravariance)

我們先定義兩個類:Plate、Food、Fruit

//定義一個`盤子`類
public class Plate<T> {

    private T item;

    public Plate(T t) {
        item = t;
    }

    public void set(T t) {
        item = t;
    }

    public T get() {
        return item;
    }

}

//食物
public class Food {

}

//水果類
public class Fruit extends Food {
}

複製程式碼

然後定義一個takeFruit()方法

private static void takeFruit(Plate<Fruit> plate) {
}
複製程式碼

然後呼叫takeFruit方法,把一個裝著蘋果的盤子傳進去:

takeFruit(new Plate<Apple>(new Apple())); //泛型之不變
複製程式碼

發現編譯器報錯,發現裝著蘋果的盤子竟然不能賦值給裝著水果的盤子,這就是泛型的不變性 (invariance)

這個時候就要引出泛型的協變性

1) 協變性

假設我就要把一個裝著蘋果的盤子賦值給一個裝著水果的盤子呢?

我們來修改下 takeFruit 方法的引數 (? extends Fruit):

private static void takeFruit(Plate<? extends Fruit> plate) {
}
複製程式碼

然後呼叫 takeFruit 方法,把一個裝著蘋果的盤子傳進去:

takeFruit(new Plate<Apple>(new Apple())); //泛型的協變
複製程式碼

這個時候編譯器不報錯了,而且你不僅可以把裝著蘋果的盤子放進去,還可以把任何繼承了 Fruit 類的水果都能放進去:

//包括自己本身 Fruit 也可以放進去
takeFruit(new Plate<Fruit>(new Fruit()));
takeFruit(new Plate<Apple>(new Apple()));
takeFruit(new Plate<Pear>(new Pear()));
takeFruit(new Plate<Banana>(new Banana()));
複製程式碼

在 Java 中把 ? extends Type 類似這樣的泛型,稱之為 上界萬用字元(Upper Bounds Wildcards)

為什麼叫上界萬用字元?因為 Plate<? extends Fruit>,可以存放 Fruit 和它的子類們,最高到 Fruit 類為止。所以叫上界萬用字元

好,現在編譯器不報錯了,我們來看下 takeFruit 方法體裡的一些細節:

private static void takeFruit(Plate<? extends Fruit> plate) {
    //plate5.set(new Fruit());    //編譯報錯
    //plate5.set(new Apple());    //編譯報錯
    Fruit fruit = plate5.get();   //編譯正常
}
複製程式碼

發現 takeFruit() 的引數 plate 的 set 方法不能使用了,只有 get 方法可以使用。如果我們需要呼叫 set 方法呢?

這個時候就需要引入泛型的逆變性

2) 逆變性

修改下泛型的形式 (extends 改成 super):

private static void takeFruit(Plate<? super Fruit> plate){
    plate.set(new Apple());     //編譯正常
    //Fruit fruit = plate.get(); //編譯報錯
    //Fruit pear = plate.get();   //編譯報錯
}
複製程式碼

發現 set 方法可以用了,但是 get 方法“失效”了。我們把類似 ? super Type 這樣的泛型,稱之為下界萬用字元(Lower Bounds Wildcards)

在介紹上界萬用字元 (extends) 的時候,我們知道上界萬用字元的泛型可以存放該型別的和它的子類們

那麼,下界萬用字元 (super) 顧名思義就是能存放 該型別和它的父類們。所以對於 Plate<? super Fruit> 只能放進 Fruit 和 Food。

我們在回到剛剛說到的 set 和 get 方法:set 方法的引數是該泛型;get 方法的返回值是該泛型

也就是說上界萬用字元 (extends),只允許獲取 (get),不允許修改 (set)。可以理解為只生產(返回給別人用),不消費。 下界萬用字元 (super),只允許修改 (set),不允許獲取 (get)。可以理解為只消費 (set 方法傳進來的引數可以使用了),不生產。

可以總結為:PECS(Producer Extends, Consumer Super)

3) 泛型小結

  1. 上界萬用字元的泛型可以存放該型別的和它的子類們,下界萬用字元能存放該型別和它的父類們

generic-.png

  1. PECS(Producer Extends, Consumer Super)

上界萬用字元一般用於讀取,下界萬用字元一般用於修改。比如 Java 中 Collections.java 的 copy 方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

複製程式碼

dest 引數只用於修改,src 引數用於讀取操作,只讀 (read-only)

通過泛型的協變逆變來控制集合是只讀,還是只改。使得程式程式碼更加優雅。

Kotlin 泛型的協變、逆變

掌握了 Java 的泛型,Kotlin 泛型就簡單很多了,大體上是一致的,但還有一些區別。我們挨個的來介紹下:

1) Kotlin 協變

關於泛型的不變性,Kotlin 和 Java都是一致的。比如 List<Apple> 不能賦值給 List<Fruit>

我們來看下 Kotlin 協變:

fun takeFruit(fruits: List<Fruit>) {
}


fun main(args: Array<String>) {
    val apples: List<Apple> = listOf(Apple(), Apple())
    takeFruit(apples)
}

複製程式碼

編譯器不會報錯,為什麼可以把 List<Apple> 賦值給 List<Fruit>,根據泛型不變性 ,應該會報錯的。

不報錯的原因是這裡的 List 不是 java.util.List 而是 Kotlin 裡的 List:

//kotlin Collection
public interface List<out E> : Collection<E> 

//Java Collection
public interface List<E> extends Collection<E>
複製程式碼

發現 Kotlin 的 List 泛型多了 out 關鍵字,這裡的 out 關鍵相當於 java 的 extends 萬用字元

所以不僅可以把 List<Apple> 賦值給 List<Fruit>,Fruit 的子類都可以:

fun main(args: Array<String>) {
    val foods: List<Food> = listOf(Food(), Food())
    val fruits: List<Fruit> = listOf(Fruit(), Fruit())
    val apples: List<Apple> = listOf(Apple(), Apple())
    val pears: List<Pear> = listOf(Pear(), Pear())
    //takeFruit(foods) 編譯報錯
    takeFruit(fruits)
    takeFruit(apples)
    takeFruit(pears)
}
複製程式碼

2) Kotlin 逆變

out 關鍵字對應 Java 中的 extends 關鍵字,那麼 Java 的 super 關鍵字對應 Kotlin 的 in 關鍵字

關於逆變 Kotlin 中的排序函式 sortedWith,就用到了 in 關鍵字:

public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T>
複製程式碼
//宣告 3 個比較器
val foodComparator = Comparator<Food> { e1, e2 ->
        e1.hashCode() - e2.hashCode()
}
val fruitComparator = Comparator<Fruit> { e1, e2 ->
    e1.hashCode() - e2.hashCode()
}
val appleComparator = Comparator<Apple> { e1, e2 ->
    e1.hashCode() - e2.hashCode()
}

//然後宣告一個集合
val list = listOf(Fruit(), Fruit(), Fruit(), Fruit())
//Comparator 宣告成了逆變 (contravariant),這和 Java 的泛型萬用字元 super 一樣的
//所以只能傳遞 Fruit 以及 Fruit 父類的 Comparator
list.sortedWith(foodComparator)
list.sortedWith(fruitComparator)
//list.sortedWith(appleComparator) 編譯報錯
複製程式碼

3) Kotlin和Java在協變性、逆變性的異同點

Java 中的上界萬用字元 extends 和下界萬用字元 super,這兩個關鍵字非常形象

extends 表示 只要 繼承 了這個類包括其本身都能存放

super 表示 只要是這個類的父類包括其本身都能存放

同樣的 Kotlin 中 out 和 in 關鍵字也很相像,這個怎麼說呢?

在介紹 Java 泛型的時候說過,上界萬用字元 extends 只能 get (後者只能做出參,這就是 out),不能 set (意思就是不能引數傳進來)。所以只能出參(out)

下界萬用字元 super 只能 set (意思就是可以入參,這就是 in),不能 get。所以只能入參(in)

Kotlin 和 Java 只是站在不同的角度來看這個問題而已。可能 Kotlin 的 in 和 out 更加簡單明瞭,不用再記什麼 PECS(Producer Extends, Consumer Super) 縮寫了

除了關鍵字不一樣,另一方面,Java 和 Kotlin關於泛型定義的地方也不一樣。

在介紹 Java 泛型的時候,我們定義萬用字元的時候都是在方法上,比如:

void takeExtendsFruit(Plate<? extends Fruit> plate)
複製程式碼

雖然Java支援在類上使用 ? extends Type,但是不支援 ? super Type,並且在類上定義了 ? extends Type,對該類的方法是起不到 只讀、只寫 約束作用的。

我們把 Java 上的泛型變異稱之為:use-site variance,意思就是在用到的地方定義變異

在 Kotlin 中,不僅支援在用到的地方定義變異,還支援在定義類的時候宣告泛型變異 (declaration-site variance)

比如上面的排序方法 sortedWith 就是一個 use-site variance

public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T>

複製程式碼

再比如 Kotlin List,它就是 declaration-site variance,它在宣告List類的時候,定義了泛型協變

這個時候會對該 List 類的方法產生約束:泛型不能當做方法入參,只能當做出參。Kotlin List 原始碼片段如下所示:

public interface List<out E> : Collection<E> {
    
    public operator fun get(index: Int): E

    public fun listIterator(): ListIterator<E>

    public fun listIterator(index: Int): ListIterator<E>

    public fun subList(fromIndex: Int, toIndex: Int): List<E>
    
    public fun indexOf(element: @UnsafeVariance E): Int
    
    //省略其他程式碼
}

複製程式碼

比如 get、subList 等方法泛型都是作為出參返回值的,我們也發現 indexOf 方法的引數竟然是泛型 E,不是說只能當做出參,不能是入參嗎?

這裡只是為了相容 Java 的 List 的 API,所以加上了註解 @UnsafeVariance (不安全的協變),編譯器就不會報錯了。

例如我們自己定義一個 MyList 介面,不加 @UnsafeVariance 編譯器就會報錯了:

generic-out.png

Kotlin 泛型擦除和具體化

Kotlin 和 Java 的泛型只在編譯時有效,執行時會被擦除 (type erasure)。例如下面的程式碼就會報錯:

//Error: Cannot check for instance of erased type: T
//fun <T> isType(value: Any) = value is T

複製程式碼

Kotlin 提供了一種泛型具體化的技術,它的原理是這樣的:

我們知道泛型在執行時會擦除,但是在 inline 函式中我們可以指定泛型不被擦除, 因為 inline 函式在編譯期會 copy 到呼叫它的方法裡,所以編譯器會知道當前的方法中泛型對應的具體型別是什麼, 然後把泛型替換為具體型別,從而達到不被擦除的目的,在 inline 函式中我們可以通過 reified 關鍵字來標記這個泛型在編譯時替換成具體型別

如下面的程式碼就不會報錯了:

inline fun <reified T> isType(value: Any) = value is T
複製程式碼

泛型具體化的應用案例

我們在開發中,常常需要把 json 字串解析成 Java bean 物件,但是我們不是知道 JSON 可以解析成什麼物件,通常我們通過泛型來做。

但是我們在最底層把這個不知道的類封裝成泛型,在具體執行的時候這個泛型又被擦除了,從而達不到程式碼重用的最大化。

比如下面一段程式碼,請求網路成功後把 JSON 解析(反射)成物件,然後把物件返回給上層使用:

泛型具體換應用01

從上面程式碼可以看出,CancelTakeoutOrderResponse 我們寫了 5 遍.

那麼我們對上面的程式碼進行優化下,上面的程式碼只要保證 Type 物件那裡使用是具體的型別就能保證反射成功了

把這個 wrapCallable 方法在包裝一層:

wrap.png

再看下優化後的 cancelTakeoutOrder 方法,發現 CancelTakeoutOrderResponse 需要寫 2 遍:

泛型具體換應用02

我們在使用 Kotlin 的泛型具體換,再來優化下:

因為泛型具體化是一個行內函數,所以需要把 requestRemoteSource 方法體積變小,所以我們包裝一層:

wrap2.png

再看下優化後的 cancelTakeoutOrder 方法,發現 CancelTakeoutOrderResponse 需要寫 1 遍就可以了:

泛型具體換應用03

Kotlin 集合

Kotlin 中的集合底層也是使用 Java 集合框架那一套。在上層又封裝了一層 可變集合不可變集合 介面。

下面是 Kotlin 封裝的可變集合和不可變集合介面:

介面 是否可變 所在檔案
List 不可變 Collections.kt
MutableList 可變 Collections.kt
Set 不可變 Collections.kt
MutableSet 可變 Collections.kt
Map 不可變 Collections.kt
MutableMap 可變 Collections.kt

宣告可變集合

宣告可變集合

宣告不可變集合

宣告不可變集合

通過 Kotlin 提供的 API 可以方便的建立各種集合,但是同時需要搞清楚該 API 建立的集合底層到底是對應 Java 的哪個集合。

Kotlin 集合常用的 API

1) all、any、count、find、firstOrNull、groupBy 函式

collection-api.png

2) filter、map、flatMap、flatten 函式

collection-api2.png

案例分析:list.map(Person::age).filter { it > 18 }

雖然 list.map(Person::age).filter { it > 18 } 程式碼非常簡潔,我們要知道底層做了些什麼?反編譯程式碼如下:

collection-api3.png

發現呼叫 map 和 filter 分別建立了一個集合,也就是整個操作建立了兩個 2 集合。

延遲集合操作之 Sequences

根據上面的分析,list.map(Person::age).filter { it > 18 } 會建立兩個集合,本來常規操作一個集合就夠了,Sequence就是就是為了避免建立多餘的集合的問題。

val list = listOf<Person>(Person("chiclaim", 18), Person("yuzhiqiang", 15),
        Person("johnny", 27), Person("jackson", 190),
        Person("pony", 85))
        
//把 filter 函式放置前面,可以有效減少 map 函式的呼叫次數
list.asSequence().filter { person ->
    println("filter---> ${person.name} : ${person.age}")
    person.age > 20
}.map { person ->
    println("map----> ${person.name} : ${person.age}")
    person.age
}.forEach {
    println("---------符合條件的年齡 $it")
}
複製程式碼

為了提高效率,我們把 filter 呼叫放到了前面,並且加了一些測試輸出:

filter---> chiclaim : 18
filter---> yuzhiqiang : 15
filter---> johnny : 27
map----> johnny : 27
---------符合條件的年齡 27
filter---> jackson : 190
map----> jackson : 190
---------符合條件的年齡 190
filter---> pony : 85
map----> pony : 85
---------符合條件的年齡 85
複製程式碼

從這個輸出日誌我們可以總結出 Sequence 的原理:

集合的元素有序的經過 filte r操作,如果滿足 filter 條件,再經過 map 操作。

而不會新建一個集合存放符合 filter 條件的元素,然後在建立一個集合存放 map 的元素

Sequence 的原理圖如下所示:

Sequence.png

需要注意的是,如果集合的數量不是特別大,並不建議使用 Sequence 的方式來進行操作。我們來看下 Sequence<T>.map 函式

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}
複製程式碼

它是一個高階函式,但是它並沒有內聯,為啥沒有內聯?因為它把 transform 傳遞給了 TransformingSequence,然後TransformingSequence通過屬性將其儲存起來了,並沒有直接使用 transform,所以不能內聯。

根據上面我們對高階函式的分析,如果一個高階函式沒有內聯,每呼叫一次該函式都會建立內部類。

除此之外還有一點也需要注意,下面一段程式碼實際上不會執行:

list.asSequence().filter { person ->
    person.age > 20
}.map { person ->
    person.age
}
複製程式碼

只有用到了該 Sequence 裡的元素才會觸發上面的操作,比如後面呼叫了 forEach、toList 等操作。

對 Sequence 做一個小結:

  1. 如果集合的資料量很大啊,可以使用集合操作的延遲 Sequence
  2. Sequence 的 filter、map 等擴充套件還是是一個非 inline 的高階函式
  3. 集合的 Sequence 只有呼叫 forEach、toList 等操作,才會觸發對集合的操作。有點類似 RxJava。

Koltin 和 Java 互動的一些問題

1) Kotlin 和 Java 互動上關於空的問題

例如我們用 Kotlin 定義了一個介面:

interface UserView {
    fun showFriendList(list: List<User>)
}
複製程式碼

然後在 Java 程式碼裡呼叫了該介面的方法:

public class UserPresenter {
    public void getLocalFriendList() {
        List<User> friends = getFriendList();
        friendView.showFriendList(friends);
    }
}
複製程式碼

看上去是沒什麼問題,但是如果 getFriendList() 方法返回 null,那麼程式就會出現異常了,因為我們定義 UserView 的showFriendList 方法時規定引數不能為空

如果在執行時傳遞 null 進去,程式就會報異常,因為 Kotlin 會為每個定義不為 null 的引數加上非空檢查:

Intrinsics.checkParameterIsNotNull(list, "list");
複製程式碼

而且這樣的問題,非常隱蔽,不會再編譯時報錯,只會在執行時報錯。

2) 關於 Kotlin 基本型別初始化問題

比如我們在某個類裡定義了一個Int變數:

private var mFrom:Int
複製程式碼

預設 mFrom 是一個空,而不是像我們在 Java 中定義的是 0

在用的時候可能我們直接判斷 mFrom 是不是 0 了,這個時候可能就會有問題了。

所以建議,一般基本型別定義為 0,特別是定義 bean 類的時候。這樣也不用考慮其為空的情況,也可以利用 Kotlin 複雜型別的 API 的便捷性。

3) Kotlin 泛型具體化無法被 Java 呼叫

如果我們定義了一個 inline 函式,且使用了泛型具體化,該方法不能被 Java 呼叫。反編譯後發現該方法是私有的。只能Kotlin 程式碼自己呼叫。

4) Kotlin 間接訪問 Java default class

這個問題是碼上開學分享 Kotlin、Jetpack 的微信群裡成員發現的問題:

class JavaPackagePrivate{

}

public class JavaPublic extends JavaPackagePrivate {
}


public class JavaClassForTestDefault {
    public void test(JavaPackagePrivate b){

    }
}

複製程式碼

然後在 Kotlin 中呼叫 JavaClassForTestDefault.test 方法:

fun main(args: Array<String>) {
    JavaClassForTestDefault().test(JavaPublic())
}

Exception in thread "main" java.lang.IllegalAccessError: tried to access class visibility_modifier.modifier_class.JavaPackagePrivate...
複製程式碼

在 Kotlin 看來,test 方法的引數型別 JavaPackagePrivate 是 package-private(default),也就是包內可見。Kotlin 程式碼中在其他包內呼叫該方法,Kotlin 就不允許了。

要麼 Kotlin 程式碼和 JavaPackagePrivate 包名一樣,要麼使用 Java 程式碼來呼叫這樣的 API

總結

本文介紹了關於 Kotlin 的很多相關的知識點:從 Kotlin 的基本資料型別、訪問修飾符、類和介面、lambda 表示式、Kotlin 泛型、集合、高階函式等都做了詳細的介紹。

如果掌握這些技術點,在實際的開發中基本上能夠滿足需要。除此之外,像 Kotlin 的協程、跨平臺等,本文沒有涉及,這也是今後重點需要研究的地方。

Kotlin 在實際的使用過程中,還是很明顯的感覺到編碼效率的提升、程式碼的可讀性提高。

可能一行 Kotlin 程式碼,可以抵得上以前 Java 的好幾行程式碼。也不是說程式碼越少越好,但是我們要知道這幾行簡短的程式碼底層在做什麼。

這也需要開發者對 Kotlin 程式碼底層為我們做了什麼有一個比較好的瞭解。對那些不是很熟悉的 API 最好反編譯下程式碼,看看到底是怎麼實現的。

這樣下來,對 Kotlin 的各種語法糖就不會覺得神祕了,對我們寫的 Kotlin 程式碼也更加自信。

最後,《Kotlin In Action》是可以一讀再讀的書,每次都會有新的收穫。可以根據書中的章節,深入研究其背後相關的東西。


下面是我的公眾號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯絡我:

公眾號:  chiclaim

Document Reference

  1. 《Kotlin In Action》
  2. www.zhihu.com/question/20…
  3. stackoverflow.com/questions/3…

相關文章