方法
Kotlin 中方法和函數(shù)其實是統(tǒng)一的衡未,但是我們這么理解區(qū)別:
函數(shù):直接定義在文件中的 fun
辈挂。
方法:定義在 class
中的 fun
召川。
方法和函數(shù)一樣冰蘑,也是可以賦值給其他對象,也使用雙冒號::
運算符评汰。
函數(shù):::函數(shù)名
纷捞。
方法:類名::方法名
。
fun main() {
// 以下兩種函數(shù)類型等價
val a1: (Int) -> Int = ::test
val a2: Function1<Int, Int> = ::test
// 以下三種函數(shù)類型等價
val b1: (Dog, String) -> String = Dog::eat
val b2: Dog.(String) -> String = Dog::eat
val b3: Function2<Dog, String, String> = Dog::eat
// 注:FunctionN<A, B, ...>:N為參數(shù)加返回值個數(shù)被去,<>中依次寫入 類名主儡,參數(shù)類型(0~n)個,返回值類型(0~1)個
println(a1.invoke(2)) //4
println(a2(3)) //9
println(b1.invoke(Dog(), "bone1")) //Dog is eating bone1
println(b2(Dog(), "bone2")) //Dog is eating bone2
println(b3.invoke(Dog(), "bone3")) //Dog is eating bone3
}
// 定義函數(shù)
fun test(a: Int) = a * a
// 定義類
class Dog {
// 定義方法
fun eat(food: String) = "Dog is eating $food"
}
注:函數(shù)類型均可省略惨缆,因為編譯器可以自動推斷出來糜值。
FunctionN<A, B, ...>
N
:參數(shù)加返回值個數(shù)
<>:依次寫入 類名,參數(shù)類型(0 ~ n)個坯墨,返回值類型(0 ~ 1)個
中綴表示法
Koltin 中的方法也可以使用 infix
來修飾寂汇,寫法就和雙目運算符一樣。
需要注意的是:infix
方法只能有一個參數(shù)捣染,因為雙目運算符的后面只能帶一個參數(shù)骄瓣。
componentN 方法與解構(gòu)
Kotlin 允許將一個對象的 N 個屬性 “解構(gòu)” 給多個變量,寫法如下:
var (name, age) = user
Kotlin 實際會執(zhí)行的代碼:
var name = user.component1()
var age = user.component2()
如果希望將對象解構(gòu)成多個變量的話耍攘,那就需要定義多個 componentN()
方法榕栏,且該方法需要使用 operator
修飾。
class User(private var name: String, private var age: Int) {
operator fun component1() = this.name
operator fun component2() = this.age
}
fun main() {
// 解構(gòu) User
val (name, age) = User("張三", 28)
println("$name 今年的年齡是 $age 歲") //張三 今年的年齡是 28 歲
// 只解構(gòu) User 的部分屬性蕾各,如果想忽略前面屬性扒磁,可使用 _ 來代替
val (name2) = User("李四", 29)
println("用戶的名字是 $name2") //用戶的名字是 李四
val (_, age2) = User("王五", 30)
println("用戶的年齡是 $age2 歲") //用戶的年齡是 30 歲
}
如果只想解構(gòu)部分屬性,那忽略前面屬性可以使用 _
來占位式曲。
數(shù)據(jù)類 data class
數(shù)據(jù)類專門用來封裝數(shù)據(jù)妨托,由于簡化 Java 中某些只定義field
,getter
和setter
方法的類吝羞。
數(shù)據(jù)類型支持解構(gòu)始鱼,從而可以實現(xiàn)返回多個值的函數(shù)。
數(shù)據(jù)類需要滿足如下要求:
- 主構(gòu)造器中至少需要一個參數(shù)脆贵;
- 主構(gòu)造器的所有參數(shù)需要使用
val
或var
聲明為屬性; - 不能使用
abstract
起暮、open
卖氨、sealed
修飾会烙,也不能定義成內(nèi)部類; - 可以實現(xiàn)接口筒捺,也可繼承其他類柏腻。
數(shù)據(jù)類會自動生成如下內(nèi)容:
- 生成 equals()/hashCode() 方法;
- 自動重寫 toString() 方法系吭;
- 為每個屬性自動生成 operator 修飾的 componentN() 方法五嫂;
- 生成 copy() 方法,用于完成對象復制肯尺。
數(shù)據(jù)類定義
data class DataClass(
val result: Int,
val status: Int
)
數(shù)據(jù)類使用
fun main() {
val (result, status) = DataClass(1, 2)
println("result: $result, status: $status") //result: 1, status: 2
val d1 = DataClass(1, 1)
val d2 = d1.copy()
println(d1) //DataClass(result=1, status=1)
println(d2) //DataClass(result=1, status=1)
println(d1 === d2) //false沃缘,說明 copy() 出來的不是同一個的對象
}
Kotlin 標準庫中提供了
Pair
(支持兩個任意類型的屬性) 和Triple
(支持三個任意類型的屬性) 兩個數(shù)據(jù)類。
Lambda 表達式中解構(gòu)
如果 Lambda 表達式的參數(shù)是支持解構(gòu)的類型则吟,那么就可以在括號中引入多個新參數(shù)來替代單個參數(shù)槐臀。
map.mapValues { entry -> "${entry.value}" }
可以寫成
map.mapValues { (_, value) -> "$value" }
Lambda 表達式參數(shù)與解構(gòu)的區(qū)別:
{ a -> ... }
:一個參數(shù)
{ a, b -> ... }
:兩個參數(shù)
{ (a, b) -> ... }
:一個參數(shù),并解構(gòu)成了兩個變量
{ (a, b), c -> ... }
:兩個參數(shù)氓仲,第一次參數(shù)解構(gòu)成了兩個變量
注意:Lambda 表達式中的參數(shù)是不需要圓括號的水慨,如果出現(xiàn)圓括號,那就是使用解構(gòu)
屬性和字段
屬性是 Kotlin 中的一個重要特色敬扛,相當于 Java 中的field
加上getter
和setter
方法(只讀屬性沒有setter
方法)晰洒,且開發(fā)者不需要自己實現(xiàn)getter
和setter
。
在定義 Kotlin 的普通屬性時啥箭,必須顯示指定初始值谍珊,要么在定義時指定,要么在構(gòu)造器中指定捉蚤。
// 屬性a需要在構(gòu)造器中初始化抬驴,屬性b,c直接在定義時初始化
// a缆巧,b是只讀屬性布持,只有g(shù)etter方法
// c是讀寫屬性,有g(shù)etter和setter方法
class Item(val a: String) {
val b = "bbb"
var c = "ccc"
}
在 Kotlin 類中定義屬性后陕悬,Kotlin 中只能使用點語法來訪問屬性题暖,Java 中只能使用 getter 和 setter 方法來訪問屬性。
自定義 getter 和 setter
雖然定義了屬性之后捉超,系統(tǒng)會自動生成getter
和setter
方法胧卤,但是你還可以自定義這兩個方法。無須使用 fun
關(guān)鍵字拼岳。
getter
:get() {}
(可使用單表達式)枝誊,應該是一個無參,有返回值的方法惜纸;
setter
:set(value) {}
(可使用單表達式)叶撒,應該帶一個參數(shù)绝骚,無返回值。
class UserInfo(var first: String, var last: String) {
// 自定義getter和setter
var fullName: String
// 由于fullName是通過first和last計算出來的祠够,所以不需要生成field压汪,所以就不能設(shè)置初始值
get() = "$first.$last"
set(value) {
if ("." !in value && value.indexOf(".") != value.lastIndexOf(".")) {
throw IllegalArgumentException("您輸入的名稱不合法")
} else {
val names = value.split(".")
first = names[0]
last = names[1]
}
}
// 可直接在getter和setter方法名前修改可見性和添加注解,并不修改默認實現(xiàn)
var school = "清華大學"
private set
@Inject get
}
val user1 = UserInfo("張", "三")
println("first: ${user1.first}, last: ${user1.last}, full: ${user1.fullName}") //first: 張, last: 三, full: 張.三
val user2 = UserInfo("張", "三")
user2.fullName = "李.四"
println("first: ${user2.first}, last: ${user2.last}, full: ${user2.fullName}") //first: 李, last: 四, full: 李.四
幕后字段
當定義完屬性后古瓤,系統(tǒng)自動為屬性生成的field
字段就成為幕后字段(backing field)止剖。
只要滿足以下條件,系統(tǒng)就會為屬性生成幕后字段:
- 該屬性使用系統(tǒng)自動生成的
getter
和setter
落君。 - 重寫
getter
和setter
時穿香,使用field
顯式引用了幕后字段。
class BackingField(name: String, age: Int) {
var name = name
set(value) {
if (value.length < 2 || value.length > 6) {
println("您輸入的姓名不合法叽奥!")
} else {
field = value
}
}
var age = age
set(value) {
if (value < 0 || value > 100) {
println("您輸入的年齡不合法扔水!")
} else {
field = value
}
}
}
val backingField = BackingField("張三", 29)
backingField.name = "張三三三三三三"
println(backingField.name) //張三
backingField.name = "李四"
println(backingField.name) //李四
在getter
和setter
方法中,需要通過field
關(guān)鍵字來引用幕后字段朝氓。
幕后屬性
如果需要自定義field魔市,并自定義getter和setter,就可以使用幕后屬性(backing property)赵哲。
幕后屬性就是用private修飾的屬性待德,Kotlin不會為幕后屬性提供getter和setter方法。
class BackingProperty(name: String) {
// 定義幕后屬性 _name
private var _name: String = name
// name賦值取值都是通過幕后屬性 _name 進行的
var name
get() = _name
set(value) {
_name = value
}
}
val backingProperty = BackingProperty("Kotlin")
println(backingProperty.name) //Kotlin
backingProperty.name = "Java"
println(backingProperty.name) //Java
延遲初始化屬性
設(shè)置延遲初始化后枫夺,就可以在定義時和構(gòu)造方法里不設(shè)置初始值了将宪。使用lateinit
關(guān)鍵字修飾。
對lateinit
修飾符有以下限制:
- 只能修飾在類體中聲明的可變屬性橡庞,即
lateinit var
是固定搭配较坛; - 修飾的屬性不能有自定義的
getter
和setter
方法; - 修飾的屬性必須是非空類型扒最;
- 修飾的屬性不能是原生類型(即Java的8種類型對應的類型)丑勤。
注意:使用
lateinit
修飾后,Kotlin不會為屬性執(zhí)行默認初始化吧趣,如果在賦值之前調(diào)用法竞,則會引發(fā)lateinit property name has not been initialized
異常。
class LateInit {
lateinit var a: String
lateinit var b: String
}
val lateInit = LateInit()
lateInit.a = "aaa"
lateInit.b = "bbb"
println("${lateInit.a}, ${lateInit.b}") //aaa, bbb
內(nèi)聯(lián)屬性
可以使用inline
修飾符修飾沒有幕后字段的屬性的getter
或setter
方法强挫,也可以修飾屬性本身岔霸,這就相當于同時修飾該屬性的getter
和setter
。
被修飾的getter
或setter
方法在調(diào)用時會執(zhí)行內(nèi)聯(lián)化俯渤。
class InlineProp {
// 定義普通屬性呆细,由于有幕后字段,故不能被inline修飾
var name: String = ""
// inline get八匠,不能有幕后字段field
val firstName: String
inline get() = name.split(".")[0]
// inline set絮爷,不能有幕后字段field
var lastName: String
inline set(value) {
name = "${name.split(".")[0]}.$value"
}
get() = name.split(".")[1]
// inline prop <=> inline get & set诡曙,不能有幕后字段field
inline var userName: String
get() = name
set(value) {
if ("." !in value && value.indexOf(".") != value.lastIndexOf(".")) {
println("您輸入的名稱不合法")
return
}
name = value
}
}
深入構(gòu)造器
Kotlin 類可以定義0 ~ 1個主構(gòu)造器和0 ~ N個次構(gòu)造器。
如果主構(gòu)造器沒有任何注解或可見性修飾符略水,則可以省略constructor
關(guān)鍵字
主構(gòu)造器和初始化塊
主構(gòu)造器的作用:
初始化塊可以使用主構(gòu)造器定義的形參;
在聲明屬性時可以使用主構(gòu)造器定義的形參劝萤;
class ConstructorTest(name: String) {
// 初始化塊中可以直接調(diào)用主構(gòu)造器中定義的參數(shù)
init {
println(name)
}
}
// 定義一個private的主構(gòu)造器渊涝,不可省略constructor關(guān)鍵字
class ConstructorTest private constructor(name: String) {
// 初始化塊中可以直接調(diào)用主構(gòu)造器中定義的參數(shù)
init {
println(name)
}
}
次構(gòu)造器和構(gòu)造器重載
初始化塊必定在每個構(gòu)造器之前調(diào)用,因為次構(gòu)造器是委托的主構(gòu)造器床嫌,即委托調(diào)用初始化塊跨释。
: this()
// 定義一個無參的主構(gòu)造器
class ConstructorOverload() {
var a: String = ""
var b: String = ""
init {
println("這是初始化塊")
}
// 構(gòu)造器重載,定義有一個參數(shù)的次構(gòu)造器厌处,委托主構(gòu)造器鳖谈,即委托調(diào)用初始化塊,使用 : this()
constructor(a: String): this() {
println("有一個參數(shù)的構(gòu)造器:$a")
this.a = a
}
// 構(gòu)造器重載阔涉,定義有兩個參數(shù)的次構(gòu)造器缆娃,委托主構(gòu)造器,即委托調(diào)用初始化塊瑰排,使用 : this()
constructor(a: String, b: String): this() {
println("有兩個參數(shù)的構(gòu)造器:a = $a贯要,b = $b")
this.a = a
this.b = b
}
}
val constructorOverload = ConstructorOverload()
//這是初始化塊
val constructorOverload1 = ConstructorOverload("param1")
//這是初始化塊
//有一個參數(shù)的構(gòu)造器:param1
val constructorOverload2 = ConstructorOverload("param1", "param2")
//這是初始化塊
//有兩個參數(shù)的構(gòu)造器:a = param1,b = param2
主構(gòu)造器聲明屬性
在主構(gòu)造器參數(shù)上直接加上 var 或 val 即可聲明屬性椭住,也可為參數(shù)設(shè)上默認值
// 在主構(gòu)造器參數(shù)上直接加上 var 或 val 即可聲明屬性崇渗,也可為參數(shù)設(shè)上默認值
class ConstructorParam(var p1: String = "p1", var p2: String = "p2") {}
繼承
修飾符 class SubClass: SuperClass { ... }
Kotlin 的類默認是final
的,不能派生子類京郑,所以如果需要讓一個類能派生子類宅广,需要使用open
修飾該類。
open class SuperClass(name: String) {
constructor(): this("nnnn")
init {
println(name)
}
}
// 定義一個無參子類些举,必須立即調(diào)用父類構(gòu)造器
class SubClass1: SuperClass("foo") {
}
// 定義一個參數(shù)的子類跟狱,必須立即調(diào)用父類構(gòu)造器
class SubClass2(name: String): SuperClass(name) {
}
class SubClass3: SuperClass {
// 定義一個次構(gòu)造器,隱式委托調(diào)用父類無參的構(gòu)造器
constructor()
// 定義一個參數(shù)的次構(gòu)造器金拒,顯式委托父類帶參數(shù)的構(gòu)造器
constructor(name: String): super(name)
// 定義兩個參數(shù)的次構(gòu)造器兽肤,顯式委托本類帶參數(shù)的構(gòu)造器
constructor(name: String, name1: String): this(name)
}
重寫父類的方法
方法也需要使用open
來修飾,才可以被子類重寫绪抛。
子類重寫父類的方法必須使用override
來修飾资铡;
open class Bird(name: String) {
init {
println(name)
}
// 該方法可被子類重寫,故需要使用 open 修飾
open fun fly() {
println("我能飛")
}
}
// 麻雀
class Sparrow : Bird("麻雀")
// 鴕鳥
class Ostrich : Bird("鴕鳥") {
// 重寫父類的 fly 方法
override fun fly() {
println("我不能飛")
}
}
val sparrow = Sparrow() //麻雀
sparrow.fly() //我能飛
val ostrich = Ostrich() //鴕鳥
ostrich.fly() //我不能飛
重寫父類的屬性
父類中需要被重寫的屬性幢码,也需要用open
修飾笤休;
子類重寫屬性必須用override
修飾;
屬性重寫有兩個限制:
- 類型要兼容症副;
- 訪問權(quán)限要更大或相等店雅;只讀屬性
val
可被讀寫屬性var
重寫政基;讀寫屬性var
不可被只讀屬性val
重寫;
open class Book {
internal open var price: Double = 10.9
open var publisher: String = ""
open val name: String = ""
}
class KotlinBook: Book() {
// 重寫父類price屬性闹啦,可擴大訪問權(quán)限
public override var price: Double = 20.9
// 重寫父類publisher屬性沮明,不能將var改成val,讀寫屬性不能被只讀屬性重寫
override var publisher: String = "機械工業(yè)出版社"
// 重寫父類name屬性窍奋,能將val改成var荐健,只讀屬性能被讀寫屬性重寫
override val name: String = "Kotlin編程實踐"
}
val kotlinBook = KotlinBook()
println("《${kotlinBook.name}》是由“${kotlinBook.publisher}”出版,售價${kotlinBook.price}元") //《Kotlin編程實踐》是由“機械工業(yè)出版社”出版琳袄,售價20.9元
super
訪問被子類重寫的屬性或調(diào)用被子類重寫的方法時江场,默認會訪問子類中的屬性或執(zhí)行子類中的方法,若仍想訪問父類中被重寫的屬性或調(diào)用父類中被重寫的方法窖逗,需要使用super
址否。
open class Book {
internal open var price: Double = 10.9
open var publisher: String = "出版社"
open val name: String = "書名"
open fun test1() {
println("Book test1")
}
open fun test2() {
println("Book test2")
}
}
class KotlinBook: Book() {
// 重寫父類price屬性,可擴大訪問權(quán)限
public override var price: Double = 20.9
// 重寫父類publisher屬性碎紊,不能將var改成val佑附,讀寫屬性不能被只讀屬性重寫
override var publisher: String = "機械工業(yè)出版社"
// 重寫父類name屬性,能將val改成var矮慕,只讀屬性能被讀寫屬性重寫
override val name: String = "Kotlin編程實踐"
// 訪問被重寫的屬性帮匾,默認只會訪問當前子類中定義的屬性
fun getSelfName() = name
// 如果想訪問父類中的屬性,使用super
fun getParentName() = super.name
override fun test2() {
println("KotlinBook test2")
}
fun test3() {
// test2 被當前子類重寫痴鳄,默認子類的test2
test2() //KotlinBook test2
// 使用super來調(diào)用父類的test2
super.test2() //Book test2
// 由于當前子類未重寫test1瘟斜,故會調(diào)用父類的test1
test1() //Book test1
}
}
強制重寫
如果子類從多個直接超類型(接口或類)繼承了同名成員,那么Kotlin要求子類必須重寫該成員痪寻;
如果要訪問超類中的成員螺句,可使用super<超類型名>
來進行引用。
open class MandatoryOverride {
open fun test() {
println("class MandatoryOverride test")
}
}
interface IMandatoryOverride {
fun test() {
println("interface IMandatoryOverride test")
}
}
class Mandatory: MandatoryOverride(), IMandatoryOverride {
// 由于MandatoryOverride和IMandatoryOverride中都有test方法橡类,所以子類必須強制重寫該方法
override fun test() {
// 可使用super<超類型名>來引用指定超類的test方法
super<MandatoryOverride>.test() //class MandatoryOverride test
super<IMandatoryOverride>.test() //interface IMandatoryOverride test
}
}
多態(tài)
把一個子類對象賦值給父類變量蛇尚,這就是多態(tài)。
變量在編譯階段只能調(diào)用其編譯時類型所具有的方法顾画,但在運行時則執(zhí)行其運行時所具有的方法取劫。
open class BaseClass {
open var book = 6
fun base() {
println("父類中的普通方法")
}
open fun test() {
println("父類中可以被覆蓋的方法")
}
}
class SubClass: BaseClass() {
override var book = 60
fun sub() {
println("子類中的普通方法")
}
override fun test() {
println("子類的覆蓋父類的方法")
}
}
//把一個子類對象賦值給父類變量,這就是多態(tài)
val baseClass: BaseClass = SubClass()
println(baseClass.book) //60
baseClass.base() //父類中的普通方法
baseClass.test() //子類的覆蓋父類的方法
// baseClass.sub() 由于聲明的是BaseClass研侣,沒有sub方法谱邪,故此處使用會編譯錯誤
使用is檢查類型
Kotlin提供了類型檢查運算符is
和!is
,來保證類型轉(zhuǎn)換不會出錯庶诡。
is
運算符前面操作數(shù)的編譯時類型要么與后面的類相同惦银,要么與后面的類具有父子繼承關(guān)系,否則編譯時程序就會報錯。
val hello: Any = "Hello"
//由于Any是所有類的基類扯俱,所以可以使用 is String书蚪,is Date
println("hello 是String:${hello is String}") //hello 是String:true
println("hello 是Date:${hello is Date}") //hello 是Date:false
val str = "Hello"
// println(str is Date) 編譯錯誤,因為String與Date沒有繼承關(guān)系
Kotlin的is
和!is
是非常智能的迅栅,只要你對其進行了判斷殊校,變量就會自動轉(zhuǎn)換為目標類型。
val hello: Any = "Hello"
if (hello is String) {
println(hello.length) //hello自動轉(zhuǎn)換為String读存,可直接調(diào)用String相關(guān)的方法箩艺。
}
when
分支也可進行智能轉(zhuǎn)換。
fun isTest(x: Any) {
when (x) {
is String -> println(x.length)
is Int -> println(x.toDouble())
}
}
isTest(3) //3.0
isTest("abcd") //4
使用as運算符轉(zhuǎn)型
除了使用is
進行類型檢查宪萄,還可使用as
或as?
進行強制轉(zhuǎn)型。
as
:不安全的強制轉(zhuǎn)型運算符榨惰,若轉(zhuǎn)換失敗拜英,會引發(fā)ClassCastException
異常;
as?
:安全的強制轉(zhuǎn)型運算符琅催,若轉(zhuǎn)換失敗居凶,不會引發(fā)異常,而是返回null
藤抡。
val obj: Any = "Hello"
//使用as強制轉(zhuǎn)型侠碧,成功
val objStr = obj as String
println(objStr)
// val objInt = obj as Int 轉(zhuǎn)型失敗,會引發(fā)ClassCastException異常
// val num: Number = objStr as Number 由于objStr是String缠黍,String和Number沒有繼承關(guān)系弄兜,所以編譯器會提示轉(zhuǎn)換不可能成功
val objStrNullable = obj as? String
println(objStrNullable?.length) //5
val objIntNullable = obj as? Int
println(objIntNullable?.toDouble()) //null