Kotlin基础学习(1)
Kotlin基礎學習(1)
本文主要講解kotlin的部分基礎知識,并不是全部基礎。
提示:純屬個人理解,如有理解錯誤,歡迎留言指正批評。
一、Null檢查機制
kotlin對于聲明可為空的參數,使用時是需要進行空判斷的
1.如何聲明一個可為空的參數
var a: String? = "1"var b: String? = null去掉“?”,聲明的是一個不為空的參數,給一個不為空的對象賦值為null,會報錯
2.如何進行空判斷處理
當b字段為null時會拋出空指針異常,可以看下這段kotlin代碼編譯為java源碼時的樣子
從java源碼中看到,上面這段kotlin代碼是會直接拋空指針異常的
當b為null時,會返回null給age,當b不為null時,則返回結果給age。也來看看這段kotlin代碼編譯為java源碼時的樣子
老規矩,看一下java源碼:
可以看到當b為null時,會把a的值賦值給age。
看一下,常規用法:
二、延遲加載
kotlin在類中聲明一個變量(屬性)的時候時必須初始化的,這在開發的時候時不滿足需求的。很多時候我們都需要在使用的時候才進行初始化,于是就有了延遲加載這個功能。
局部變量是可以只聲明而不進行初始化的。全局變量是必須初始化的
上圖可以看到編譯器報錯了。以下討論都是針對全局變量的。
1.lateinit
1、 只能用于var變量的延遲
2、被lateinit修飾的變量不能聲明為可空對象
3、不能用于基本數據類型
4、自定義屬性的getter或setter方法后,就不能用lateinit修飾了
使用時需要初始化該變量,才能使用。初始化語句在哪調用,就在哪開始初始化。
以下是lateinit無法使用的三種情況:
2.by lazy{}
1、僅能用于val變量的延遲
2、自定義屬性的getter或setter方法后,就不能用by lazy{}修飾了
第一次調用get()會執行by lazy{}括號中的所有語句,并且記錄被修飾的變量結果內容,后續再調用get()只是返回記錄的結果,并不會調用by lazy{}括號中的所有語句了。
來看看lazy的源碼: 默認情況下,調用的是SynchronizedLazyImpl方法
從源碼可以看出,當第一次調用get方法時,會執行by lazy {}括號中的所有語句,第二次調用get方法時,就直接獲取第一次得到的結果值。這里還應該注意一點,by lazy{}是線性安全的,它使用了同步鎖:該值只在一個線程中計算,并且所有線程會看到相同的值。
額外知識點
lazy可以傳進來一個LazyThreadSafetyMode變量,一共有三種模式。如下使用:
簡單介紹一下這三種模式:
LazyThreadSafetyMode.SYNCHRONIZED
默認模式。延遲屬性的值,只允許一個線程中修改,其他線程只能使用該線程修改的值,并且代碼塊只有該線程走過一遍,其余線程只能獲取最終結果。
LazyThreadSafetyMode.PUBLICATION
公共模式。延遲屬性的值,只有一個線程能修改,這個使用的不是同步鎖控制的,這個是通過CAS操作,在賦值時進行了CAS原子操作,所以值還是唯一的,不會因為多線程的原因導致值有問題。但是首次執行時,代碼塊是有可能被多個線程(首次同時進入多個線程時)走過多遍的。第二次則會直接獲取第一次得到的結果。
LazyThreadSafetyMode.NONE
None模式。這個模式是線性不安全的,多個線程在同一時刻可進入初始化塊,也可修改成功多次。
3.兩個延遲加載的區別
區別:
1、lateinit只能修飾var ; by lazy 只能修飾val
2、lateinit不能修飾可空屬性且不能修飾基本數據類型 ; by lazy 可以修飾可空屬性也可以修飾基本數據類型
3、lateinit初始化是依據初始化代碼在哪調用即在哪進行初始化賦值的 ; by lazy是在第一次調用該變量獲取值時就開始初始化賦值。
相同點:
自定義屬性都不能使用lateinit和 by lazy進行修飾
三、頂層聲明
在java中有很多工具類,類中會定義很多靜態方法以及靜態屬性。kotlin認為根本不需要創建這些無意義的類。可以直接將代碼放入文件的頂層,不用附屬于任何一個類。
在kotlin中有class與file文件的區別。頂層聲明就是寫在一個file文件里。
1.頂層函數
在Kotlin中,頂層函數屬于包內成員,包內可以直接使用,包外只需要import該頂層函數,即可使用。
kotlin中使用:
java中使用:
在java中,系統編譯后會生成一個文件名+kt的類,用該類進行調用,方法會變成該類的靜態方法。看下反編譯后的java源碼:
額外知識點
@file:JvmName("xxxx")
@file:JvmName("xxxx") 的使用:系統編譯后,文件名為xxxx。可用xxxx.方法名()調用
@file:JvmName("Text") package com.demo.kotlindemofun moxiaoting(){ }java中調用:
public class Day2 {public void mian(){//Text 為@file:JvmName("Text")中的名字Text.moxiaoting();} }kotlin調用還是一樣的,不影響kotlin中的使用方式。只是改變了java中的調用方式
@file:JvmMultifileClass
@file:JvmMultifileClass的使用:系統編譯后,會把多個自定義名字相同的文件強制合并在一起
Text文件:
TextOther文件:
@file:JvmName("TextName") @file:JvmMultifileClass package com.demo.kotlindemofun candyGood(){ }java中調用:
可以看到在編譯后,java已經把兩個文件Text和TextOther進行了合并。統一使用TextName可調用這兩個文件的所有方法。
在kotlin中使用不受影響,直接調用方法名即可。
@JvmName("xxxx")相同時如果不使用JvmMultifileClass注解,編譯會報錯。
2.頂層屬性
和頂層函數一樣,屬性也可以放在文件的頂層,不附屬與任何一個類。這種屬性叫頂層屬性。
kotlin中使用:
java中使用:
和頂層函數調用是一樣的,也是編譯后生成一個文件名+kt的一個類,里面有屬性的getter與setter方法,直接調用就可以了。細心的同學可以看到,有些屬性是可以直接通過屬性名調出來的,如例子中的b和e屬性。有些屬性卻只能通過其getter與setter方法調用。具體我們可以看下編譯后的java源碼:
可以看到被lateinit修飾的延遲屬性編譯后是public的,const修飾的屬性編譯后也是public的。我們從這里還可以知道,頂層屬性被編譯后是java中的靜態屬性,頂層方法被編譯后是java中的靜態方法。
- 謎之操作(我也不太清楚原因):
在添加@file:JvmMultifileClass之后編譯,被lateinit修飾的延遲屬性依然是public。但是java中調用時,只能通過其getter與setter方法調用。const修飾的屬性依然是public,在java中調用時可以直接通過屬性名調用出來。
來看下編譯后的java代碼:
從編譯后的java源碼可以看出,合并后的類TextName里只有const修飾的屬性,因此可以直接通過屬性名調用。其他屬性都需要通過其getter與setter方法調用。至于為什么合并后要這么操作,我也不太清楚,如果有明白的大神或者同學可以在評論區留言告知一下,萬分感謝~
四、類
1.數據類
在kotlin中一個數據類對應于java中的一個實例類bean。使用data修飾符。
data class Bean(){}例如:
Java中有這樣一個實例類
在kotlin中就可以寫成如下這樣
data class Bean(var name: String , var age: String, var bean: DataBean) : Serializable {data class DataBean( var sex: String ) : Serializable {}}數據類會自動為該類生成一些自帶的函數,其中最重要的就是componentN()函數以及copy()函數。現在來講一下這自動生成的兩類函數:
解構聲明
只要某個類有componentN()函數,就可以對該類進行解構聲明。解構聲明簡單來說就是把一個對象變成多個變量的形式
解構聲明的表現形式: var (變量名 , 變量名,…) = 類對象
data類都會有componentN()函數,函數個數就是主構函數中屬性的個數,順序與屬性聲明一樣。有多少個componentN()函數解構聲明中的變量名就可以寫多少個。
看個栗子:
該數據類,會有兩個組件函數:component1()、component2(),因為主構函數中有兩個屬性name和屬性sex。不會為age屬性生成一個組件函數,因為其聲明不在主構函數中。
data class Person(val name: String , var sex : String) {var age: Int = 0 }fun main(){val person1 = Person("John" , "女")//解構聲明var (name , sex) = person1//等價于val name2 = person1.namevar sex2 = person1.sex }現在應該可以理解這句話的意思了吧:解構聲明簡單來說就是把一個對象變成多個變量的形式,解構聲明之后,我們就可以直接使用name和sex這兩個變量了。
可能這塊有點疑問,這個解構聲明和component1()、component2()函數有什么關系呢?
我們看一下這個data類的反編譯java源碼:
public final class Person {private int age;@NotNullprivate final String name;@NotNullprivate String sex;....@NotNullpublic final String component1() {return this.name;}@NotNullpublic final String component2() {return this.sex;}.... }可以看到系統為Person類生成了兩個componentX()函數,分別對應name屬性和sex屬性。這就是我前面說的:data類都會有componentN()函數,函數個數就是主構函數中屬性的個數,順序與屬性聲明一樣。 可以看到這個順序component1對應name,component2對應sex,有興趣的可以試下把sex和name在主構函數中換個位置,component1就對應sex,component2就對應name了。
copy()函數
現在來看下數據類自動生成的第二個函數copy()。和componentN()函數一樣,也只是復制主構函數中的屬性,不在主構函數中的屬性不進行復制。
copy()函數的實現會類似下面這樣:
data class Person(val name: String , var sex : String) {var age: Int = 0 }fun copy(name: String = this.name, sex : String= this.sex ) = Person(name, sex)會類似一個帶有默認值的函數。默認值就是其調用者對應的屬性的值。
data class Person(val name: String , var sex : String) {var age: Int = 0 } fun main(){val person1 = Person("John" , "女")var person = person1.copy("ni")person1.age = 20 }person 的age值還是0,name值為"ni" ,sex值是person1的sex值;person1的age值是20,上面這個栗子實際上只復制了sex屬性。
額外知識點
1、自定義帶有解構聲明的類
前面講的組件函數是data類自動生成的,那么可不可以自定義一個可解構聲明的類呢?當然是可以的。
使用 operator 修飾組件函數
class Point(var x: Int , var y: Int){operator fun component1() = x;operator fun component2() = y; }這樣就為一個自定義的Point類聲明了兩個組件函數,對該類就可以使用解構聲明了。
需要注意一點,data類是自動按照主構函數中的屬性,對每個屬性生成一個屬于它自己的組件函數,所以data類會自動生成很多方法,而我們安卓是有方法數限制的。這塊是需要注意的一點。
2、工具JsonToKotlinClass插件
這個插件可以幫助我們把一個json串自動轉成data類。不過生成的data類是一個kt文件,需要手動把二級data類移動為內部類即可。使用方式和我們經常使用的GsonFormatPlus插件是一樣的,這里就不講述使用方式了,不會的同學可以百度一下。
3、標準數據類(元組)
在kotlin中,只有二元元組(Pair)和三元元組(Tripe)。這兩個類也是data類。是kotlin自定義的且繼承Serializable,這意味這可以進行頁面傳遞。
來看下這兩個類的源碼:Pair
Tripe類
可以看到這兩個類也是data類,且繼承Serializable,很簡單不做過多的解釋,直接看例子就明白了。
這個是kotlin中自定義的data類,如果不在乎命名的話,可以直接使用比較方便。使用元組還是建議和解構聲明一起使用,否則不建議單獨獲取其屬性值,畢竟這個命名有點雷人。
2.密封類
使用sealed修飾的類為密封類
sealed class SealedDemo{}看下這行代碼的Java源碼:
public abstract class sealedDemo {private sealedDemo() {} }可以看出,密封類實際是一個抽象類,它是無法被實例化的(構造方法是private),自身是沒有對象的。但是它是類而且是抽象類,所以可以被繼承。
密封類在kotlin1.5以上,允許子類定義在不同的文件中,只要保證子類和父類在同一個Gradle module且同一個包下即可。密封類是不允許外部(別的library)有其子類的,這個性質在開發工具類,如jar包或第三方框架時你會特別喜歡。由于java的單繼承性,所以kotlin1.5以上新出來了一個密封接口,在一定程度上彌補了密封類的不足。密封接口和密封類是一樣的性質,只是一個是接口一個是類而已。
使用
密封類是不能夠被實例化的,它的取值范圍是其子類。廢話不多說,來看個例子
sealed class SealedDemo{} //密封類的子類 class Test2Activity : SealedDemo() {} //密封類的子類 class Test3Activity : SealedDemo() {} //密封類的子類 open class Test4Activity : SealedDemo() {} //間接子類,不算密封類的取值范圍 class Test5Activity : Test4Activity() {}var sealed1 : SealedDemo = Test2Activity() fun eval(e: SealedDemo): String = //需要書寫出全部子類的情況,建議寫成這樣,可以避免新增加一個子類忘記補全when表達式的情況when (e) { is Test2Activity -> "111"is Test3Activity -> "2222"is Test4Activity -> "2222"}//或者fun eval(e: SealedDemo): String =when (e) { //不需要寫出全部子類的情況,缺少的子類進入else語句is Test2Activity -> "111"is Test3Activity -> "2222"else -> "2222"}sealed1是SealedDemo 的對象,可以看到其實例化是通過子類進行的。所以,我們說密封類的取值范圍是其子類,就是這個意思。間接子類不算密封類的取值范圍
密封類最常用的用法就是與when語句搭配使用。當when作為表達式的時候是需要填寫完整邏輯的,即必須包含else語句。
可以看到當when作為表達式的時候必須要有else,否則會報錯。但是與密封類一起使用就可以不用寫else,但是需要書寫全部子類的各個情況,如果缺少一種子類的情況,編譯器會報錯。利用這個特性,可以幫我們避免在項目中,新增加一個子類后忘記補全when表達式的結果情況。
與枚舉類的區別
相同點:
密封類與枚舉類的取值范圍都是固定的。密封類取值是其子類,枚舉類的取值是其對象
區別:
枚舉類的值都是其對象,每個對象都只能被實例化一次。而密封類的取值是其子類,每個子類可以有多個實例,并且每個子類可以有不同的狀態參數以及行為方法來記錄更多的實現信息以完成更多的功能。這是枚舉不具備的,枚舉的方法和參數都是其值共同擁有的,不具備獨立性。
密封接口
所有的枚舉都繼承自java.lang.Enum類,所以枚舉類是不能再繼承其他類了,但是卻可以實現接口。所以kotlin1.5以上新出了一個特性,密封接口。專門用來彌補不能繼承密封類的缺憾。
sealed interface Language { }enum class HighLevelLang : Language {Java, Kotlin, CPP, C }enum class MachineLang : Language {ARM, X86 }//使用:雙層嵌套when語句。枚舉中的每個實例都是密封接口的對象 fun eval(lang : Language): String = when (lang) {is MachineLang ->when (lang) {MachineLang.ARM -> "111"MachineLang.X86 -> "111"}is HighLevelLang ->when (lang) {HighLevelLang.CPP -> "111"HighLevelLang.Java -> "111"HighLevelLang.Kotlin -> "111"HighLevelLang.C -> "111"}}3.內聯類
溫馨提示:建議先看本篇文中的內聯函數,弄懂 inline 關鍵詞后再返過來看這節。本節默認您已經知道了 inline 關鍵詞
Kotlin 引入了一種特殊的類,叫做內聯類,它是通過在類名前放置一個 Inline 修飾符來聲明
使用時和普通類是一樣,只是在編譯的時候,并不會給這個類創建它的實例,而是直接使用里面的屬性進行賦值(和編譯時常量一樣的概念)。除非你打印了該類的實例時,就會進行創建一個實例,只是調用屬性和方法是不會創建該類的實例。
//調用 fun main(){val hous = Bean("24")println(hous.length) } //編譯后public static final void main() {String hous = Bean.constructor-impl(); //這里是調用了內聯類的靜態方法,為了打印init塊中的語句int var1 = Bean.getLength-impl(hous); //這里是調用了內聯類的靜態方法,為了打印后面的語句System.out.println(var1); }//編譯后的bean類 public final class Bean {@NotNullpublic static String constructor_impl(@NotNull String name) {Intrinsics.checkNotNullParameter(name, "name");String var1 = getStr-impl(name);System.out.println(var1);return name;}@NotNullpublic static final String getStr_impl(String $this) {return "asda";}public static final int getLength_impl(String $this) {return $this.length();}public static final int toMinutes_impl(String $this) {return getStr-impl($this).length() * 60;} }從上面可以看到,并未給內聯類Bean進行初始化實例,都是直接調用靜態方法直接獲取值。
當我們實例化一個對象時,該對象就存儲在 JVM 堆上。我們在存儲和使用對象實例時會有性能損失。堆分配和內存提取的性能代價很高,雖然看起來每個對象的內存開銷都微不足道,但是積累起來,它對代碼運行速度就會產生一定的影響。內聯類就可以解決這樣的問題
創建一個內聯類的限制
1、內聯類主構造器必須有且僅有一個只讀val屬性
2、不能繼承父類,也不能被繼承,但可以實現接口
3、內聯類的內部是允許成員屬性的,但成員屬性只能是自定義的val屬性且值必須是常量或者基于構造器中那個基礎值計算
4、內聯類必須在頂層聲明,嵌套、內部類不能內聯的。
5、目前,也不支持內聯類枚舉。
4.對象表達式(個人理解:匿名類)
語法格式
object[:若干個父類型,中間用逗號隔開]{}要創建一個繼承自某個(或某些)類型的匿名類的對象,我們就會使用對象表達式。我個人理解為匿名類。
open class A(x: Int) {public open val y: Int = x }interface B { …… }val ab: A = object : A(1), B {override val y = 15 }這是一個繼承A類,實現B接口的一個匿名類,ab是該類的實例。如果我們只需要“一個對象而已”,并不需要特殊超類型,那么我們可以簡單地寫:
val adHoc = object {var x: Int = 0var y: Int = 0}這是一個不繼承任何父類的匿名類,adHoc是它的一個實例。
來看下,java中的匿名內部類:
使用kotlin的對象表達式為:
這是繼承View.OnClickListener類的一個匿名類,其實例傳入了setOnClickListener()方法中。所以我叫對象表達式為匿名類。但它和java中的匿名類還有點區別。
與java中的匿名內部類有點區別,在java中匿名內部類修改外部類的變量時,需要該變量為final修飾。但是在kotlin中對象表達式里并沒有這個限制。
另外請注意,匿名對象可以用作只在本地和私有作用域中聲明的類型。如果你使用匿名對象作為公有函數的返回類型或者用作公有屬性的類型,那么該函數或屬性的實際類型會是匿名對象聲明的超類型,如果你沒有聲明任何超類型,就會是 Any。在匿名對象中添加的成員將無法訪問。
五、函數
1.高階函數
在 Kotlin 里面,函數的參數也可以是函數類型的
fun candy_new(funParam: (Int) -> String): String {return funParam(1) }在上述代碼中,參數 funParam 具有函數類型 (Int) -> String,因此 candy_new 接受一個函數作為參數,該函數接受類型為Int的參數并返回一個 String 類型的值。
函數類型不只可以作為函數的參數類型,還可以作為函數的返回值類型。
fun candy(param: Int): (Int) -> Unit {return ::candy_new }這種「參數或者返回值為函數類型的函數」,在 Kotlin 中就被稱為「高階函數」
引用
除了作為函數的參數和返回值類型,你把它賦值給一個變量也是可以的。但是需要加雙冒號,這個雙冒號的寫法叫做函數引用。
1、函數引用
1、成員函數:類名::函數名
2、擴展函數:擴展類名::函數名
3、頂層函數:::函數名
4、成員擴展函數:沒有引用
變量d是函數b的引用。
Kotlin 里「函數可以作為參數」這件事的本質,是函數在 Kotlin 里可以作為對象存在——因為只有對象才能被作為參數傳遞啊。賦值也是一樣道理,只有對象才能被賦值給變量啊。但 Kotlin 的函數本身的性質又決定了它沒辦法被當做一個對象。那怎么辦呢?Kotlin 的選擇是,那就創建一個和函數具有相同功能的對象。怎么創建?使用雙冒號。
在 Kotlin 里,一個函數名的左邊加上雙冒號,它就不表示這個函數本身了,而表示一個對象,或者說一個指向對象的引用,但,這個對象可不是函數本身,而是一個和這個函數具有相同功能的對象。
每個函數類型的對象都有一個自帶的invoke() 函數,你對一個函數類型的對象加括號、加參數,這種寫法是 Kotlin 的語法糖,實際上它真正調用的是這個對象的 invoke() 函數。
2、屬性引用
屬性引用的用法與函數(方法)引用的用法是完全一致,都是通過::形式來引用的。
頂層屬性引用
package com.demo.kotlindemo const val pase = 3 fun main() {println(::pase) // 打印屬性類型: val pase: kotlin.Intprintln(::pase.get()) // 打印屬性值: 3println(::pase.name) // 打印屬性名:pase }這里,::pase 表示一個 KProperty 對象。通過這個 KProperty 訪問指定屬性的具體信息,相當于 java 的Field。
KProperty 是一個接口,代表一個屬性(val 或 var)。
對于 var 屬性,則使用 KMutableProperty。這個接口繼承自 KProperty 但具有 set 方法。
類的公共屬性引用
class Person(val name: String){}fun main(){val prop = Person::nameprintln(prop.get(Person("Android"))) }其實這個屬性引用就類似于在Java反射屬性的方式差不多,目的都是通過反射來操作屬性的一些東東, 其中Person::name其實表示的是類型KProperty的屬性對象,那們我們可以通過get()來獲取其值,也可以通過name屬性來獲取其名字。
如果屬性屬于具體類,那么所對應的屬性引用必須依賴于具體的實例存在,因此 get 方法必須傳遞該類的一個實例。如:prop.get(Person("Android"))
通過實例獲得屬性引用
fun main() {var str = "abcde"var getMethod = str::get //通過實例獲取其方法的引用println(getMethod(1))var getField = "str"::length //通過實例獲取其屬性的引用println(getField.get()) }注意,上述代碼實際上等效于:
fun main() {var str = "abcde"var getMethod = String::get //通過類名獲取方法的引用,這種形式不知道調用者是誰,所以需要傳遞調用者println(getMethod(str , 1))var getField = String::length //通過類名獲取屬性的引用,這種形式不知道調用者是誰,所以需要傳遞調用者println(getField.get("str")) }屬性引用在函數的應用
其實屬性引用也是可以用在函數上的
fun main() {val strings = arrayListOf("View", "TextView", "AppCompatAutoCompleteTextView")var a : (String) -> Int = String::lengthprintln(strings.map(a))println(strings.map(String::length)) }其實map方法接收的是一個函數transform: (T) -> R ,其中T參數就代表集合中的每一個String的元素,而R則為整個函數返回的值,在這里我們應該將一個函數引用傳遞給它,但這里我們實際上傳遞了一個屬性引用,這充分說明了屬性引用和函數引用本質上是相同的。那咱們傳的是一個屬性引用其執行的機制是咋樣的呢?
其實是這樣的:這里如果是函數引用,map 會將數組中的元素傳遞給函數引用,但如果是屬性引用,map 會將元素傳遞給屬性引用的接收者,即變成了 it.length,這會調用數組元素的 length 方法。
3、構造函數引用
構造方法其實也是一個方法嘛,也存在引用,所以下面來看一下它如何來用
class Bean{constructor(any: String){}constructor( age : Int , sex: String){} }fun main(){//調用的是構造函數call(::Bean)call_new(::Bean)var param : (String) -> Bean = ::Beanvar param1 : (Int , String) -> Bean = ::Beanparam("qweqwe")param1(0,"qweqwe") }fun call(param: (String) -> Bean){} fun call_new(param: (Int , String) -> Bean){}1、函數對象的參數要與構造方法的參數保持一致(體現在參數個數與參數類型上)。
2、函數對象的返回結果與構造方法所在類的類型保持一致。
在多構造函數的類中,直接使用(::Bean)去調用構造方法會報錯,因為不知道該引用的函數類型,所以需要先定義一個變量去接收,而且需要明確寫出函數類型,然后再用改變量去調用方法,就可以了
4、類引用
1、通過類引用 KClass
fun main() {val c : KClass<String> = String::class // 獲取 KClassprintln(c) // 打印:class kotlin.Stringval c2 : Class<String> = String::class.java // 獲取 Java Classprintln(c2) // 打印:class java.lang.String }KClass 引用了 kotlin 類(具有內省能力)。類似于 Java 的 class 。要獲取一個類的 KClass,通過類型名::class獲得,而對應的 Java class 則通過類型名::class.java獲得
2、通過實例引用 KClass
open class Parent class Son:Parent() class Daughter: Parent()fun main(){val son:Parent = Son()val daughter: Parent = Daughter()var str : String = "asdasd"println(son::class) // 打印:com.demo.kotlindemo.Sonprintln(son::class.java) // 打印:com.demo.kotlindemo.Sonprintln(daughter::class) //打印:class com.demo.kotlindemo.Daughterprintln(daughter::class.java) //打印:class com.demo.kotlindemo.Daughterprintln(str::class) //打印:class class kotlin.Stringprintln(str::class.java) //打印:class class java.lang.String }可以看到雖然 對象聲明時使用的是父類型,但它的 KClass 仍然是具體的子類型。此外,對于自定義的類(java中沒有的類),KClass 和 java class 的輸出是一樣的。java與kotlin都有的共同類,KClass 和 java class 的輸出是不一樣的。
匿名函數
要傳一個函數類型的參數,或者把一個函數類型的對象賦值給變量,除了用雙冒號來拿現成的函數使用,你還可以直接把這個函數挪過來寫:
fun param(funParam: (Int) -> String): String {return funParam(1) }fun main(){param(fun (param: Int): String {return param.toString()})//aaaa 是一個函數引用val aaaa = fun(param: Int): String {return param.toString()}}這種寫法叫做匿名函數。匿名函數還能更簡化一點,寫成 Lambda 表達式的形式
Lambda 表達式
如果 Lambda 是函數的最后一個參數,你可以把 Lambda 寫在括號的外面
fun param(funParam: (String , Int) -> String): String {return funParam("11", 1) }fun main(){param(fun (param: String , intger : Int): String {return param.toString()})// => 如果 Lambda 是函數的最后一個參數,你可以把 Lambda 寫在括號的外面param(){ param : String , intger : Int -> Stringparam.toString()}// => 而如果 Lambda 是函數唯一的參數,你還可以直接把括號去了:param{ param : String , intger : Int -> Stringparam.toString()}// => 另外,如果這個 Lambda 是多參數,但有些參數未使用,使用“_”下劃線代替param{ param : String , _ -> Stringparam.toString()}// => 還可以省略參數的類型聲明以及返回值的類型param{ param , _ ->param.toString()} }另外,如果這個 Lambda 是單參數的,它的這個參數也省略掉不寫, Kotlin 的 Lambda 對于省略的唯一參數有默認的名字:it
//這里有明確的函數聲明 fun param(funParam: (String) -> String): String {return funParam("11") }fun main(){param{ param ->param.toString()}// => 如果這個 Lambda 是單參數的,可以把參數省略,Lambda 對于省略的唯一參數有默認的名字:itparam{it.toString()} }需要注意一點,就是允許這么寫的前提是,函數a的聲明有明確函數類型的參數信息
//這里有明確的函數聲明 fun param(funParam: (String) -> String): String {return funParam("11") }所以 Lambda 才不用寫的。當你要把一個匿名函數賦值給變量而不是作為函數參數傳遞的時候:
val b = fun(param: Int): String {return param.toString()}//=>寫成 Lambda出錯val b = {it.toString() //it報錯}這里不能省略掉 Lambda 的參數類型,因為它無法從上下文中推斷出這個參數的類型
可以這么寫:
因為 Lambda 是個代碼塊,它總能根據最后一行代碼來推斷出返回值類型,所以它的返回值類型確實可以不寫。 另外 Lambda的返回值不是用 return 來返回,而是直接取最后一行代碼的值。這個一定注意,Lambda 的返回值別寫 return,如果你寫了,它會把這個作為它外層的函數的返回值來直接結束外層函數。當然如果你就是想這么做那沒問題啊,但如果你是只是想返回 Lambda,這么寫就出錯了。
Kotlin 的匿名函數和 Lambda 表達式的本質,它們都是函數類型的對象。在 Kotlin 里「函數并不能傳遞,傳遞的是對象」且「匿名函數和 Lambda 表達式其實都是對象」
六個常用的高階函數
作用域函數是Kotlin比較重要的一個特性。簡單來說,就是在此作用域中,可以訪問該對象而無需其名稱。這些函數稱為作用域函數。
我們在寫java中adapter類的時候,經常有這樣的寫法:
holder.mBinding.itemTitle.setText("xxxx");holder.mBinding.itemContent.setText(mQuestionsDTO.getChecklistName());holder.mBinding.itemLine.setVisibility(View.GONE);面對這樣的書寫方式,我們可以使用kotlin中的作用域函數,等價于下面這個:
holder.mBinding.let{ //這塊就是holder.mBinding對象的作用域,在這個作用域中,可以使用it來對mBinding對象做任何操作it.itemTitle.setText("xxxx");it.itemContent.setText(mQuestionsDTO.getChecklistName());it.itemLine.setVisibility(View.GONE); }簡單來說,作用域函數是為了方便對一個對象進行訪問和操作,你可以對它進行空檢查或者修改它的屬性或者直接返回它的值等操作。
1、T.let{}
public inline fun <T, R> T.let(block: (T) -> R): R {return block(this) }let函數是參數化類型 T 的擴展函數。在let塊內可以通過 it 指代該對象。返回值為let塊的最后一行或指定return表達式。在Kotlin中,如果let塊中的最后一條語句是非賦值語句,則默認情況下它是返回語句。
- let塊中的最后一條語句如果是非賦值語句,則默認情況下它是返回語句,反之,則返回的是一個 Unit類型
- let可用于空安全檢查。
- let可進行鏈式調用。
由于最后一句表達式為let函數的返回值,可以利用這點進行鏈式調用。
- let可以將“It”重命名為一個可讀的lambda參數。
2、T.run{}
public inline fun <T, R> T.run(block: T.() -> R): R {return block() }run函數是參數化類型 T 的擴展函數。在run塊內可以通過 this 指代該對象,且它的調用方式與let一致。返回值為run塊的最后一行或指定return表達式。
class Book() {var name = "《數據結構》"var price = 60fun displayInfo() = print("Book name : $name and price : $price") }fun main(){Book().run {//在run塊內可以通過 this 指代該對象,可以省略thisname = "《計算機網絡》"price = 30displayInfo()}//與let的區別。Book().let {it.name = "《計算機網絡》"it.price = 30it.displayInfo()} }3、run{}
public inline fun <R> run(block: () -> R): R {return block() }run不是擴展函數,是一個普通的高階函數。其返回值是函數塊里的最后一句話或者指定return表達式。
fun main(){var animal = "cat"run {var animal = "dog"println(animal) // dog}println(animal) //cat }在這個簡單的main函數當中我們擁有一個單獨的作用域,在run函數中能夠重新定義一個animal變量,并且它的作用域只存在于run函數當中。目前對于這個run函數看起來貌似沒有什么用處,但是在run函數當中它不僅僅只是一個作用域,他還有一個返回值。他會返回在這個作用域當中的最后一個對象。
利用返回值的這個特點,我們可以有一下操作
run {var animal: String? = "dog"animal ?: "cat"}.length獲得字符串的長度。當animal不為空時返回animal的長度,為空則返回"cat"的長度。這樣比if和else看起來優雅很多。在我們項目業務中經常遇見:如果用戶沒有登錄進入登錄頁面,如果已經登錄進入其他頁面。這樣的場景就可以使用。
4、with(object){}
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {return receiver.block() }with不是擴展函數,只是一個普通的高階函數。但是它傳入的高階函數是一個擴展函數。在with塊內可以通過 this 指代該對象,且它的調用方式與run一致。返回值為with塊的最后一行或指定return表達式。
class Book() {var name = "《數據結構》"var price = 60fun displayInfo() = print("Book name : and price : ") }fun main(){with(Book()) {name = "《計算機網絡》"price = 30displayInfo()} }with和T.run其實做的是同一種事情,對上下文對象都稱之為“this”,但是他們又存在著不同。
fun main(){val book: Book? = nullwith(book){this?.name = "《計算機網絡》"this?.price = 40}book?.run{name = "《計算機網絡》"price = 40} }相比較with來說,run函數更加簡便,空安全檢查也沒有with那么頻繁。
5、T.also{}
public inline fun <T> T.also(block: (T) -> Unit): T {block(this)return this }also是 T 的擴展函數,also函數的用法類似于let函數,將對象的上下文引用為“it”而不是“this”以及提供空安全檢查方面。但是它和let的區別是返回值,它返回的是本身的調用者,let的返回值是函數塊最后一條語句。
val book : Book = Book().also {it.name = "《計算機網絡》"it.price = 40}print(book.name)also的返回值就是其調用者本身,因此可以利用這個特點進行鏈式結構
Book().also {it.name = "《計算機網絡》"it.price = 40}.let {//這里it是 Book() 對象println(it.name)}6、T.apply{}
public inline fun <T> T.apply(block: T.() -> Unit): T {block()return this }apply是 T 的擴展函數,與run函數有些相似,它將對象的上下文引用為“this”而不是“it”,并且提供空安全檢查,不同的是,apply不接受函數塊中的返回值,返回的是自己的T類型對象。
Book().apply {name = "《計算機網絡》"price = 40}.let {println(it.name)}apply返回的是自己的T類型對象。因此它也可以利用這個特點進行鏈式結構。
總結
| T.let{} | it | 函數塊中的最后一句代碼或指定的return語句 | T?.let{} |
| T.run{} | this | 函數塊中的最后一句代碼或指定的return語句 | T?.run{} |
| run{} | 無 | 函數塊中的最后一句代碼或指定的return語句 | 函數塊中自檢 |
| with(object){} | this | 函數塊中的最后一句代碼或指定的return語句 | 函數塊中自檢 |
| T.also{} | it | T(調用者本身) | T?.also{} |
| T.apply{} | this | T(調用者本身) | T?.apply{} |
2.擴展函數
擴展函數與成員函數的區別:確切地說,不是任何一個類的成員函數——但我要限制只有通過某個 類的對象才能調用你。這就是擴展函數的本質。
擴展函數
在聲明一個函數的時候在函數名的左邊寫個類名再加個點,你就能對這個類的對象調用這個函數了。
這種函數就叫擴展函數。
注意:擴展并不是真正的修改他們所擴展的類,通過定義一個擴展,你并沒有在一個類中插入新成員,僅僅只是可以通過該類型的變量用點表達式去調用這個新函數而已。
1、頂層擴展函數
package com.demo.kotlindemofun String.method1(i: Int) {}fun main(){"rengwuxian".method1(1) }頂層擴展函數不屬于某個類,但是僅有指定類的對象可以調用它。頂層擴展函數寫在kt文件里的。
2、成員擴展函數
擴展函數也可以寫在某個類里:
open class Father{open fun D1.foo(){println("Father -> D1")}fun caller(d: D1){//只能在該類里調用這個擴展函數,在這里類外就無法調用d.foo()} }就可以在這個類里調用這個函數,但必須使用那個前綴類的對象來調用它。類外就無法調用這個擴展函數,該擴展函數是Father類的成員函數,也是D1類的擴展函數。
這種擴展函數不存在函數引用。Kotlin 不許我們引用既是成員函數又是擴展函數的函數。
3、伴生對象擴展函數
class Bean {companion object{} }fun Bean.companion.foo( a : Int){print("伴生對象的擴展函數")}fun main(){Bean.foo(1) }伴生對象的擴展函數使用:用類名調用該擴展函數。
4、從java代碼看擴展函數
class Bean {}fun Bean.foo(a : Int){}反編譯后,可以看下java中的樣子
可以看出這是一個頂層擴展函數,在java中頂層聲明會自動生成一個類名+kt的類。可以看到擴展函數foo在Beankt類中屬于成員函數,第一個參數是Bean的對象(前綴類的對象),第二個參數是擴展函數正常需要傳參的變量a
所以主要看擴展函數寫在哪個位置,寫在某個類里,則為那個類的成員函數,寫在頂層里,則為文件名+kt該類的成員函數。
擴展函數的引用
擴展函數引用: 擴展類名::函數名
1、通過引用調用擴展函數
fun String.method1(i: Int) {}(String::method1)("rengwuxian",1) // 擴展函數: 等價于 "rengwuxian".method1(1) //相當于String::method1.invoke("rengwuxian", 1)//等價于"rengwuxian".method1(1)當拿著一個函數的引用去調用的時候,不管是一個普通的成員函數還是擴展函數,你都需要把 Receiver 也就是接收者或者調用者作為第一個參數填進去。從前面講的《從java代碼看擴展函數》可以看到擴展函數其實就是某個類的成員函數,第一個參數就是其接收者或者調用者。
(String::method1)("rengwuxian", 1) // 擴展函數: 等價于 "rengwuxian".method1(1) (Int::toFloat)(1) //成員函數 : 等價于 1.toFloat()2、把擴展函數的引用賦值給變量
val a: String.(Int) -> Unit = String::method1然后再拿著這個變量去調用,或者再次傳遞給別的變量,都是可以的
"rengwuxian".a(1) //實際調用的是a.invoke() a("rengwuxian", 1) //實際調用的是a.invoke() a.invoke("rengwuxian", 1) //實際調用的是a.invoke()val b = ab("aa",1) //實際上調用的是a.invoke("aa", 1);b是等于a的,a是指向擴展函數method1的引用,因此b也是這個擴展函數的引用
3、有無 Receiver 的變量的互換
我們叫擴展函數就是一個有 Receiver的函數;普通的函數就是一個無 Receiver的函數。同理,接收擴展函數引用的變量為有Receiver的變量,接收普通函數引用的變量為無Receiver的變量。
//有Receiver的變量 val a: String.(Int) -> Unit = String::method1 //無Receiver的變量 val b: (String, Int) -> Unit = String::method1成員函數和擴展函數,它的引用都可以賦值給兩種不同的函數類型變量:一種是有 Receiver 的,一種是沒有 Receiver 的。
個人理解:這里只是定義了一個普通函數引用的變量,把它的函數引用指向了擴展函數的引用,因此b變量引用的函數的函數體就是這個擴展函數的函數體。b不是擴展函數的引用,是(String, Int) -> Unit函數的引用。
(String::method1)("rengwuxian", 1) //可以調用b("rengwuxian", 1) //可以調用 這就是普通函數調用而已"rengwuxian".b(1) //不允許調用,報錯,b不是擴展函數的引用"rengwuxian".a(1) //可以調用 ,a是擴展函數的引用這兩種類型的變量也可以互相賦值來進行轉換
val a: String.(Int) -> Unit = String::method1val b: (String, Int) -> Unit = String::method1val c: String.(Int) -> Unit = bval d: (String, Int) -> Unit = ab("rengwuxian", 1) //可以調用 這就是普通函數調用而已 等價b.invoke("rengwuxian", 1)c("rengwuxian", 1) //可以調用 這是擴展函數的引用調用"rengwuxian".c(1) // 可以調用但是他們的含義不一樣。b和d是普通函數,a和c是擴展函數。
注意,擴展函數的引用是:擴展類::方法名 , 所以如果該擴展函數即是成員函數又是擴展函數,則這種擴展函數不存在函數引用,Kotlin 不許我們引用既是成員函數又是擴展函數的函數。
擴展屬性
除了擴展函數,Kotlin 的擴展還包括擴展屬性。它跟擴展函數是一個邏輯,就是在聲明的屬性左邊寫上類名加點,這就是一個擴展屬性了。
val Float.dpget() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,this,Resources.getSystem().displayMetrics)//使用 val RADIUS = 200f.dp它的用法和擴展函數一樣。它沒有默認的getter和setter方法,需要自己實現。由于擴展沒有實際的將成員插入類中,因此對于擴展屬性來說幕后字段是無效的。所以擴展屬性不能有初始化器。它們的行為只能由顯示提供的getter和setter方法定義
3.內聯函數
Java 里有個概念叫編譯時常量。這種編譯時常量,會被編譯器以內聯的形式進行編譯,也就是直接把你的值拿過去替換掉調用處的變量名來編譯。
const val ACTION = "學習kotlin"//調用處 fun main(){var any = ACTION }//實際編譯的代碼 public static final void main() {String any = "學習kotlin"; }讓變量內聯用的是 const;而除了變量,Kotlin 還增加了對函數進行內聯的支持。
個人理解:因為函數體通常比常量復雜多了,而函數內聯會導致函數體被拷貝到每個調用處,如果函數體比較大而被調用處又比較多,就會導致編譯出的字節碼變大很多。我們都知道編譯結果的壓縮是應用優化的一大指標,而函數內聯對于這項指標是明顯不利的。所以靠inline 來做性能優化是不存在的,也不能無腦的使用 inline
inline的使用
在 Kotlin 里,你給一個函數加上 inline 關鍵字,這個函數就會被以內聯的方式進行編譯。上面提到,如果使用不當,反而會變成負優化。那這個 inline 的意思在哪?
事實上,inline 關鍵字不止可以內聯自己的內部代碼,還可以內聯自己內部的內部的代碼。什么叫「內部的內部」?就是自己的函數類型的參數。
Java 里并沒有對函數類型的變量的原生支持,Kotlin 需要想辦法來讓這種自己新引入的概念在 JVM 中落地。那就是用一個 JVM 對象來作為函數類型的變量的實際載體,讓這個對象去執行實際的代碼。
fun hello(postAction: () -> Unit){println("asdad")postAction() }//調用處 fun main(){hello{println("asdasdasd")} }程序在每次調用 hello() 的時候都會創建一個對象來執行 Lambda表達式里的代碼,雖然這個對象是用一下之后馬上就被拋棄,但它確實被創建了。如果這種函數被放在循環里執行或者界面刷新之類的高頻場景里,這一類函數就全都有了性能隱患,內存一下就彪起來了。
這就是 inline 關鍵字出場的時候了。
//加上內聯關鍵字 inline fun hello(postAction: () -> Unit){println("asdad")postAction() }//調用處 fun main(){hello{println("asdasdasd")} }//實際編譯的代碼 public static final void main() {String var1 = "asdad";System.out.println(var1);String var3 = "asdasdasd";System.out.println(var3); }經過這種優化,就避免了函數類型的參數所造成的臨時對象的創建了,就不怕在循環或者界面刷新這樣的高頻場景里調用它們了
inline 可以讓你用內聯——也就是函數內容直插到調用處——的方式來優化代碼結構,從而減少函數類型的對象的創建;
什么時候使用它?如果你寫的是高階函數,會有函數類型的參數,加上 inline 就對了;如果不怕麻煩,也可以只在會被頻繁調用的高階函數才使用 inline
noinline的使用
noinline 的意思很直白:inline 是內聯,而 noinline 就是不內聯。不過它不是作用于函數的,而是作用于函數的參數。
對于一個標記了 inline 的內聯函數,你可以對它的任何一個或多個函數類型的參數添加 noinline 關鍵字
添加了之后,這個參數就不會參與內聯了
//返回一個函數對象 inline fun hello(postAction: () -> Unit) : () -> Unit{println("asdad")postAction()return postAction } //調用處 fun main(){hello{println("asdasdasd")} } //實際調用情況 fun main(){println("asdad")println("asdasdasd")postAction //這個是返回函數對象,但是由于已經被內聯了,這個對象是不存在的,所以這塊是錯誤 }所以當你要把一個這樣的參數當做對象使用的時候,Android Studio 會報錯,告訴你這沒法編譯
加上noinline以后就可以了,這個參數就不會參與內聯了
那么,我們應該怎么判斷什么時候用 noinline 呢?很簡單,比 inline 還要簡單:你不用判斷,Android Studio 會告訴你的。當你在內聯函數里對函數類型的參數使用了風騷操作,Android Studio 拒絕編譯的時候,你再加上 noinline就可以了。
crossinline的使用
crossinline 也是一個用在參數上的關鍵字。
Kotlin 制定了一條規則:Lambda 表達式里不允許使用 return,除非——這個 Lambda 是內聯函數的參數。
但是如果為內聯函數,就可以使用return,但是返回的最外層函數
那這樣的話規則就簡單了:
1、Lambda 里的 return,正常是不可以使用的,除非使用return@xxxx指定返回地方就可以使用
2、只有內聯函數的 Lambda 參數可以使用 return,但結束的不是直接的外層函數,而是外層再外層的函數
有一個疑問:如果內聯函數里是間接調用的函數參數,還能不能用return呢?
可以看到,間接調用會報錯。kotlin中內聯函數里的函數類型的參數是不允許這種間接調用。
如果我真的需要間接調用,就使用 crossinline。需要注意——Kotlin 增加了一條額外規定:內聯函數里被 crossinline 修飾的函數類型的參數,將不再享有「Lambda 表達式可以使用 return」的福利。
什么時候需要 crossinline?只要在看到 Android Studio 給你報錯的時候把它加上就行了。
六、泛型
1、書寫規范
規范
在java中使用泛型,我們常常是這么寫的:
//在類方面 class Nick<T , Z >{} //在函數方面public <Y , Z , U> Y fun(List<? super T> a , U b ){}在kotlin中我們書寫泛型的形式與java是一樣的
//在類方面 class Nick<T , Z >{} //在函數方面public <Y , Z , U> fun( a : List<? super T>, b : U ) : Y {}類型擦除
我們知道,一個子類型是可以賦值給父類型的,比如:
Object obj = "nanchen2251"; // 這是多態Object 作為 String 的父類,自然可以接受 String 對象的賦值,這樣的代碼我們早已司空見慣,并沒有什么問題。
但當我們寫下這串代碼:
上面發生了賦值錯誤,Java 里面認為 List< String > 和 List< Object > 類型并不一致,也就是說,子類的泛型 List< String > 不屬于泛型 List< Object > 的子類。
Java 的泛型本身是一種 「偽泛型」,Java 為了兼容 1.5 以前的版本,不得以在泛型底層實現上使用 Object 引用,所以我們聲明的泛型在編譯時會發生「類型擦除」,泛型類型會被 Object 類型取代。只是說等同于object,類型并沒有真正擦除
class Demo<T> {void func(T t){} } //會被編譯成: class Demo {void func(Object t){} }編譯器會根據我們聲明的泛型類型進行提前的類型檢查,然后再進行類型擦除,擦除為 Object,但在字節碼中其實還存儲了我們的泛型的類型信息(類型并沒有真正被擦除),在使用到泛型類型的時候會把擦除后的 Object 自動做類型強轉操作。
List<String> list = new ArrayList<>(); list.add("nanchen2251"); String str = list.get(0);//雖然我們沒有進行類型轉換,但是實際上本身就是一個經過強轉的 String 對象了。2、通配符
如果想要這樣的代碼不報錯,我們可以使用通配符
List<String> list = new ArrayList<String>(); List<Object> objects = list;-
<? extends T>與< out T>
java中的上界通配符,可以使 Java 泛型具有「協變性 」。
<? extends T> : 能夠接受指定類及其子類類型的數據
kotlin中的上界通配符:< out T>
< out T>:「只能讀取不能修改」,這里的修改僅指對泛型集合添加元素,如果是 remove(int index) 以及 clear 當然是可以的。
open class Animal interface Middle open class Cat : Animal() , Middlevar one: MutableList<Int> = ArrayList() //在具體類中使用 var list: MutableList< out Number> = oneMutableList< out Number> 能夠接受Number類以及它子類類型的數據,所以把MutableList< Int >類型數數據賦值給它是完全沒有問題的。等價于java中的<? extends T>
- out自己的特點
修飾泛型時,只能定義為返回值,不能定義為參數,否則編譯器會報錯。
被out修飾的泛型,只能作為方法的返回值類型,不能作為參數的類型。因為它「只能讀取不能修改」,kotlin會進行檢測,認為傳入的參數可能會被修改。 - @UnsafeVariance的使用
如果你確定被out修飾的泛型,你不會進行修改,可以使用 @UnsafeVariance進行修飾,這樣編譯器就不會報錯,但需要自己保證不會去寫入數據,否則會導致類型轉換異常
-
<? super T>與< in T >
java中的下界通配符,可以使 Java 泛型具有「逆變性 」。
<? super T> : 能夠接受指定類及其父類類型的數據
kotlin中的下界通配符:< in T>
< in T>:「只能修改不能讀取」,這里說的不能讀取是指不能按照泛型類型讀取,你如果按照 Object 讀出來再強轉當然也是可以的。
open class Animal interface Middle open class Cat : Animal() , Middle class Dod : Cat()fun callmy(){var dwmo : MutableList<Animal> = mutableListOf()//用在具體的類上var ee : MutableList<in Cat> = dwmo//如果按照 Object 讀出來再強轉當然也是可以的。var frist : Animal = ee[0] as Animal//會報錯,因為不能讀取,這里說的不能讀取是指不能按照具體類型讀取,但可以按照Any類型讀取var frist : Animal = ee[0]var frist : Any? = ee[0] //可以 }MutableList<in Cat> 能夠接受Cat類以及它父類類型的數據,所以把MutableList<Animal>類型數數據賦值給它是完全沒有問題的。等價于java中的<? super T>
- in自己的特點
修飾泛型時,只能定義為參數,不能定義為返回值,否則編譯器會報錯。
這里也是可以使用 @UnsafeVariance的,逆變中也可以使用@UnsafeVariance注解來強行讓泛型T作為輸出位置,編譯是完全正常的沒有報錯,但是需要自己保證不會按照具體類型讀取,否則會報類轉換異常。
-
<?>與<*>
Java中單個 <?>也能作為泛型通配符使用,這是一個無邊界通配符,能接受未知類型的數據,相當于<? extends object>
kotlin中的等效寫法:<*>相當于<out Any?>
3、泛型上界約束
單個上界約束
java中的單個上界約束
public class Day{} class Nick<T extends Day >{}kotlin中的單個上界約束
open class Animal{} class Dogw< T : Animal>{}Dogw類支持的泛型類型必須是Animal的子類;Nick支持的泛型類型必須是Day的子類。
多個上界約束
java中的多個上界約束
public class Day{} interface Middle{} class Nick<T extends Day & Middle > {}kotlin中的多個上界約束
open class Animal interface Middle class PetShop<T> where T : Animal , T : Middle {}PetShop類支持的泛型類型必須是Animal 和 Middle的子類;Nick支持的泛型類型必須是Day和 Middle的子類
4、多個泛型參數聲明
多個泛型,可以通過 , 進行分割多個聲明
class Dogw< T : Animal , Z, Y>{}多個泛型參數聲明,java與kotlin是一樣的方式。
結語
提示:純屬個人理解,歡迎各位大神或同學進行指教批評
以上是我對kotlin語言的個人理解,如果理解錯誤的地方,歡迎各位大神或同學進行指教批評。本人比較懶,如果有時間會繼續出kotlin最基礎的知識點總匯,本篇文章不太適合剛接觸kotlin的小白學習。
總結
以上是生活随笔為你收集整理的Kotlin基础学习(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 10分钟学会发送邮件到指定邮箱
- 下一篇: 最好听的钢琴曲排行榜 世界上最好听的钢琴