前幾個月,在組內分享了關於Kotlin相關的內容。但因爲PPT篇幅的緣由,有些內容講的也不是很詳細。另外本人也參與了 碼上開學,該社區主要用於分享 Kotlin
和 Jetpack
相關的技術, 若是您對Kotlin或者Jetpack使用上有想要分享的地方,也歡迎您一塊兒來完善該社區。java
爲了方便你們對本文有一個大概的瞭解,先總的說下本文主要講 Kotlin 哪些方面的內容(下面的目錄和我在組內分享時PPT目錄是相似的):git
爲何要講下 Kotlin 數據類型和訪問修飾符修飾符呢?由於 Kotlin 的數據類型和訪問修飾符和 Java 的仍是有些區別的,因此單獨拎出來講一下。程序員
咱們知道,在 Java 中的數據類型分基本數據類型和基本數據類型對應的包裝類型。如 Java 中的整型 int 和它對應的 Integer包裝類型。github
在 Kotlin 中是沒有這樣的區分的,例如對於整型來講只有 Int 這一個類型,Int 是一個類(姑且把它當裝包裝類型),咱們能夠說在 Kotlin 中在編譯前只有包裝類型,爲何說是編譯前呢?由於編譯時會根據狀況把這個整型( Int
)是編譯成 Java 中的 int
仍是 Integer
。 那麼是根據哪些狀況來編譯成基本類型仍是包裝類型呢,後面會講到。咱們先來看下 Kotlin和 Java 數據類型對比:json
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中的基本類型仍是包裝類型。下面以整型爲例,其餘的數據類型同理。api
?
),則編譯後是包裝類型//由於能夠爲 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,那麼編譯後時是包裝類型,反之就是基本類型。數組
//集合泛型
//集合裏的元素都是 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};
}
複製代碼
從上面的例子中,關於集合泛型編譯後是包裝類型在 Java 中也是同樣的。若是想要聲明的數組編譯後是基本類型的數組,須要使用 Kotlin 爲咱們提供的方法:安全
//會編譯成一個int[]
fun getAge5(): IntArray {
return intArrayOf(170, 180, 190)
}
固然,除了intArrayOf,還有charArrayOf、floatArrayOf等等,就不一一列舉了。
複製代碼
咱們都知道,Kotlin 是基於 JVM 的一款語言,編譯後仍是和 Java 同樣。那麼爲何不像集合那樣直接使用 Java 那一套,要單獨設計一套這樣的數據類型呢?bash
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 和 Java 在修飾類和修飾類成員的異同點:
表格一:類訪問修飾符:
類訪問修飾符 | Java可訪問級別 | Kotlin可訪問級別 |
---|---|---|
public | 都可訪問 | 都可訪問 |
protected | 同包名 | 同包名也不可訪問 |
internal | 不支持該修飾符 | 同模塊內可見 |
default | 同包名下可訪問 | 至關於public |
private | 當前文件可訪問 | 當前文件可訪問 |
表格二:類成員訪問修飾符:
成員修飾符 | Java可訪問級別 | Kotlin可訪問級別 |
---|---|---|
public | 都可訪問 | 都可訪問 |
protected | 同包名或子類可訪問 | 只有子類可訪問 |
internal | 不支持該修飾符 | 同模塊內可見 |
default | 同包名下可訪問 | 至關於public |
private | 當前文件可訪問 | 當前文件可訪問 |
經過以上兩個表格,有幾點須要講一下。
internal 修飾符意思是隻能在當前模塊訪問,出了當前模塊不能被訪問。
須要注意的是,若是 A 類是 internal 修飾,B 類繼承 A 類,那麼 B 類也必須是 internal 的,由於若是 kotlin 容許 B 類聲明成public 的,那麼 A 就間接的能夠被其餘模塊的類訪問。
也就是說在 Kotlin 中,子類不能放大父類的訪問權限。相似的思想在 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
}
複製代碼
若是 protected 修飾類成員,在 Java 中能夠被同包名或子類可訪問;在 Kotlin 中只能被子類訪問。
這個比較簡單就不贅述了
雖然Kotlin的數據類型和訪問修飾符比較簡單,仍是但願你們可以動手寫些demo驗證下,這樣可能會有意想不到的收穫。你也能夠訪問個人 github 上面有比較詳細的測試 demo,有須要的能夠看下。
在實際的開發當中,常常須要去新建類。在 Kotlin 中有以下幾種聲明類的方式:
這種方式和 Java 相似,經過 class 關鍵字來聲明一個類。不一樣的是,這個類是 public final 的,不能被繼承。
class Person
編譯後:
public final class Person {
}
複製代碼
這種方式和上面一種方式多加了一組括號,表明構造函數,咱們把這樣的構造函數稱之爲 primary constructor。這種方式聲明一個類的主要作了一下幾件事:
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 代碼塊裏的邏輯只有可能被執行一次。
該種方式和上面是等價的,只是多加了 constructor 關鍵字而已
不在類名後直接聲明構造函數 ,在類的裏面再聲明構造函數。咱們把這樣的構造函數稱之爲 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
新建 bean 類的時候,經常須要聲明 equals、hashCode、toString 等方法,咱們須要寫不少代碼。在 Kotlin 中,只須要在聲明類的時候前面加 data 關鍵字就能夠完成這些功能。
節省了不少代碼篇幅。須要注意的是,那麼哪些屬性參與 equals、hashCode、toString 方法呢? primary constructor 構造函數裏的參數,都會參與 equals、hashCode、toString 方法裏。
這個也比較簡單,你們能夠利用 Kotlin 插件,查看下反編譯後的代碼便可。因爲篇幅緣由,在這裏就不貼出來了。
這種方法聲明的類是一個單例類,之前在Java中新建一個單例類,須要寫一些模板代碼,在Kotlin中一行代碼就能夠了(類名前加上object
關鍵字)
在 Kotlin 中 object 關鍵字有不少用法,等介紹完了 Kotlin 新建類方式後,單獨彙總下 object 關鍵字的用法。
在 Kotlin 中內部類默認是靜態的( Java 與此相反),不持有外部類的引用,如:
class OuterClass {
//在 Kotlin 中內部類默認是靜態的,不持有外部類的引用
class InnerStaticClass{
}
//若是要聲明非靜態的內部類,須要加上 inner 關鍵字
inner class InnerClass{
}
}
編譯後代碼以下:
class OuterClass {
public static final class InnerStaticClass {
}
public final class InnerClass {
}
}
複製代碼
當咱們使用 when 語句一般須要加 else 分支,若是添加了新的類型分支,忘記了在 when 語句裏進行處理,遇到新分支,when 語句就會走 else 邏輯
sealed class 就是用來解決這個問題的。若是有新的類型分支且沒有處理編譯器就會報錯。
當 when 判斷的是 sealed class,那麼不須要加 else 默認分支,若是有新的子類,編譯器會經過編譯報錯的方式提醒開發者添加新分支,從而保證邏輯的完整性和正確性
須要注意的是,當 when 判斷的是 sealed class,千萬不要添加 else 分支,不然有新類編譯器也不會提醒
sealed class 其實是一個抽象類且不能被繼承,構造方法是私有的。
除了上面咱們介紹的,object 關鍵字定義單例類外,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...")
}
}
複製代碼
雖然只是一個關鍵字的差異,可是最終編譯出的結果仍是有細微的差異的,在開發中注意下就能夠了。
咱們來看下上面代碼編譯以後對應的 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)
}
複製代碼
以下面的例子,建立一個 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 種用法:
咱們都知道,在 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 就派上用場了
在 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 最基本的語法開始,引用一段 Kotlin in Action 中對 lambda 的定義:
總的來講,主要有 3 點:
咱們再來看上面簡單的 lambda 實例:
button.setOnClickListener{view -> //view是lambda參數
//lambda體
//todo something
}
複製代碼
上面的 OnClickListener
接口和 Button 類是定義在 Java 中的。
該接口只有一個抽象方法,在 Java 中這樣的接口被稱做 functional interface
或 SAM
(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 看成參數傳遞,須要知足兩個條件:
若是 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 一個新的內部類對象
由此,咱們作一個小結:
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 中常規的建立對象的方式(除了反射、序列化等):
因爲高階函數和 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
,它格式以下:
名稱 : (參數) -> 返回值類型
function type
的名字好比: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 的時候說過了。
除了 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 的參數決定的
咱們再看下 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 方法:
除了 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 函數的定義
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)
}
複製代碼
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> T.let(block: (T) -> R): R
複製代碼
因此 let 能用於空判斷,run 也能夠:
經過上面咱們對高階函數原理的分析:在調用高階函數的時候 ,會生成一個內部類。
若是這個高階函數被程序中不少地方調用了,那麼就會有不少的內部類,那麼程序員的體積就會變得不可控了。
並且若是調用高階函數的時候,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 泛型,須要對 Java 的泛型有充分的理解。掌握 Java 泛型後 ,Kotlin 的泛型就很簡單了。
因此咱們先來看下 Java 泛型相關的知識點:
咱們先定義兩個類: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
)
這個時候就要引出泛型的協變性
了
假設我就要把一個裝着蘋果的盤子賦值給一個裝着水果的盤子呢?
咱們來修改下 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 方法呢?
這個時候就須要引入泛型的逆變性
了
修改下泛型的形式 (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)
該類型的和它的子類們
,下界通配符能存放該類型和它的父類們
上界通配符通常用於讀取,下界通配符通常用於修改。好比 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)
經過泛型的協變逆變來控制集合是只讀
,仍是只改
。使得程序代碼更加優雅。
掌握了 Java 的泛型,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)
}
複製代碼
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) 編譯報錯
複製代碼
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
編譯器就會報錯了:
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 解析(反射)成對象,而後把對象返回給上層使用:
從上面代碼能夠看出,CancelTakeoutOrderResponse
咱們寫了 5 遍.
那麼咱們對上面的代碼進行優化下,上面的代碼只要保證 Type 對象那裏使用是具體的類型就能保證反射成功了
把這個 wrapCallable 方法在包裝一層:
再看下優化後的 cancelTakeoutOrder
方法,發現 CancelTakeoutOrderResponse
須要寫 2 遍:
咱們在使用 Kotlin 的泛型具體換,再來優化下:
由於泛型具體化是一個內聯函數,因此須要把 requestRemoteSource 方法體積變小,因此咱們包裝一層:
再看下優化後的 cancelTakeoutOrder
方法,發現 CancelTakeoutOrderResponse
須要寫 1 遍就能夠了:
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 的哪一個集合。
雖然 list.map(Person::age).filter { it > 18 } 代碼很是簡潔,咱們要知道底層作了些什麼?反編譯代碼以下:
發現調用 map 和 filter 分別建立了一個集合,也就是整個操做建立了兩個 2 集合。
根據上面的分析,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 的方式來進行操做。咱們來看下 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 作一個小結:
例如咱們用 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");
複製代碼
並且這樣的問題,很是隱蔽,不會再編譯時報錯,只會在運行時報錯。
好比咱們在某個類裏定義了一個Int變量:
private var mFrom:Int
複製代碼
默認 mFrom 是一個空,而不是像咱們在 Java 中定義的是 0
在用的時候可能咱們直接判斷 mFrom 是否是 0 了,這個時候可能就會有問題了。
因此建議,通常基本類型定義爲 0,特別是定義 bean 類的時候。這樣也不用考慮其爲空的狀況,也能夠利用 Kotlin 複雜類型的 API 的便捷性。
若是咱們定義了一個 inline 函數,且使用了泛型具體化,該方法不能被 Java 調用。反編譯後發現該方法是私有的。只能Kotlin 代碼本身調用。
這個問題是碼上開學分享 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》是能夠一讀再讀的書,每次都會有新的收穫。能夠根據書中的章節,深刻研究其背後相關的東西。
下面是個人公衆號,乾貨文章不錯過,有須要的能夠關注下,有任何問題能夠聯繫我: