從 Java 角度深入理解 Kotlin

前言

前幾個月,在組內分享了關於 Kotlin 相關的內容。但由於PPT篇幅的原因,有些內容講的也不是很詳細。

所以通過一篇文字來詳解介紹 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 數據類型和訪問修飾符修飾符呢?因爲 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》是可以一讀再讀的書,每次都會有新的收穫。可以根據書中的章節,深入研究其背後相關的東西。


如果你覺得本文幫助到你,給我個關注和讚唄!

另外,我爲 Android 程序員編寫了一份:超詳細的 Android 程序員所需要的技術棧思維導圖

如果有需要可以移步我的 GitHub -> AndroidAll,裏面包含了最全的目錄和對應知識點鏈接,幫你掃除 Android 知識點盲區。 由於篇幅原因只展示了 Android 思維導圖:
超詳細的Android技術棧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章