更多文章可以訪問我的博客Aengus | Blog
概覽OverView
- Kotlin的變量或方法聲明和Java相比是“反”的签夭,也就是類型聲明在后,變量或方法名在前椎侠;
- Kotlin更嚴格第租,這點體現(xiàn)在方法聲明時就確定變量是否可變,類默認不可繼承我纪;
- Kotlin更安全慎宾,變量聲明的類都是非空的,如果允許為空浅悉,則需要在類后面加
?
趟据; - Kotlin用
println()
函數(shù)進行打印,并且每行結(jié)束不用加;
术健; - Kotlin注釋格式與Java相同汹碱;
- Kotlin支持字符串模板表達式,使用
"${expression}"
的格式可以對expression
進行運算并將其轉(zhuǎn)為字符串荞估;
變量與函數(shù)
Kotlin變量聲明格式如下:
val/var a: Type = someThing
其中val
代表不可變咳促,對應(yīng)Java中的final Type
;var
代表可變變量泼舱,就等同于Java中普通的變量等缀。Type
是變量類型,不可空娇昙,如果允許為空尺迂,則用Type?
表示。在聲明時便要求聲明變量為val
或var
冒掌,是Kotlin安全性上的體現(xiàn)噪裕,一般情況下,不可變對象都是線程安全的股毫。
Kotlin一般函數(shù)聲明如下:
fun funcationName(arg: Type): ReturnType {
// 函數(shù)體
}
可以看到參數(shù)的聲明也是后置類型膳音,ReturnType
是函數(shù)的返回類型,也可以不寫返回類型代表無返回值铃诬。Kotlin函數(shù)還有很多用法祭陷,我們將在后面再提到。
在變量或函數(shù)聲明最開始都可以加private
等關(guān)鍵字進行修飾趣席。
循環(huán)兵志、List和Map
Kotlin只有for循環(huán)和Java稍有不同,for循環(huán)基本結(jié)構(gòu)為:
for (item: Type in collection) { /* ... */ } // 可以省略類型
若使用數(shù)字迭代宣肚,則常用以下兩種方式:
// 區(qū)間想罕,左閉右閉。下面i = 1,2,3
for (i in 1..3) { /* ... */ }
// until函數(shù)霉涨,左閉右開按价,符合常用習(xí)慣惭适。下面i = 1,2
for (i in 1 until 3) { /* ... */ }
// 可以用 step 和 downTo 控制步長與順序。下面i = 10,8,6,4,2
for (i in 10 downTo 2 step 2) { /* ... */ }
Kotlin使用listOf()
生成List楼镐,使用MapOf()
生成Map癞志,用法如下:
val list = listOf<String>("a", "b", "c")
print(list[1]) // b
val map = mapOf<String, Int>("key1" to 1, "key2" to 2)
print(map["key1"]) // 1
之后會對集合進行更詳細說明。
一切皆對象
Kotlin相對于Java面向?qū)ο蟮某潭雀呖虿T贙otlin中今阳,Java的boolean
,int
,char
,double
等數(shù)據(jù)類型的首字母全部變成大寫,即Boolean
,Int
,Char
等茅信,這意味著這些數(shù)據(jù)類型都是對象,可以使用下面這種方法進行操作:
1.toChar()
true.and(false)
1.2.compareTo(1)
類Class
在Java中墓臭,所有對象都繼承自Object
蘸鲸,而在Kotlin中,所有對象均繼承自Any
(如果可能是null
窿锉,則繼承Any?
)酌摇。Any
類中有三個方法,分別為hashCode()
嗡载,equals()
與toString()
窑多。類聲明與創(chuàng)建方法如下:
class Name {
// 類主體
}
// 如果類中沒有body體,可以用這種方式
class Empty
// 創(chuàng)建類洼滚,Kotlin中沒有 new 關(guān)鍵字
val person: Person = Person()
// Kotlin擁有類型推導(dǎo)機制埂息,可以省略變量類型聲明
val person = Person()
構(gòu)造函數(shù)Constructor
每個類都可以有一個主構(gòu)造函數(shù)和次構(gòu)造函數(shù)。
主構(gòu)造函數(shù)
主構(gòu)造函數(shù)如下所示:
class Person constructor(firstName: String) { /* ... */ }
// 構(gòu)造函數(shù)無注解時也可以省略 constructor 關(guān)鍵字
class Person(firstName: String) { /* ... */ }
// 若有注解遥巴,則 constructor 關(guān)鍵字不能省略
class Person public @Inject constructor(firstName: String) { /* ... */ }
// 推薦使用
class Person(val firstName: String) { /* ... */ }
// 或者
class Person(private val firstName: String) { /* ... */ }
主構(gòu)造函數(shù)沒有函數(shù)體千康,如果想進行初始化工作(就像Java中的構(gòu)造函數(shù)一樣),可以放在init
塊中铲掐,init
塊中的函數(shù)會在創(chuàng)建對象時被調(diào)用:
class Person(firstName: String) {
init {
println("構(gòu)造函數(shù)")
}
}
Kotlin的主構(gòu)造函數(shù)有點像是語法糖拾弃,當你調(diào)用構(gòu)造函數(shù)時,Kotlin編譯器會自動將參數(shù)賦值給主構(gòu)造函數(shù)中的聲明的參數(shù)摆霉,這個過程類似于Java中的:
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
Person person = new Person("Bob");
所以豪椿,Kotlin主構(gòu)造函數(shù)中的參數(shù)可以在類body中任意的地方使用。Kotlin中類携栋,字段和函數(shù)默認修飾符都是public
搭盾,protected
和private
關(guān)鍵字作用和Java中保持一致,但是使用internal
表示同一module下可見刻两。
如果沒有指定主構(gòu)造函數(shù)增蹭,則編譯器會為其自動生成一個無參的主構(gòu)造函數(shù),這點和Java是一致的磅摹。
次構(gòu)造函數(shù)
次構(gòu)造函數(shù)是在類body中使用constructor
函數(shù)進行表示:
class Person {
constructor(name: String) { /* ... */ }
}
一個類可以有多個次構(gòu)造函數(shù)滋迈,但只能有一個主構(gòu)造函數(shù)霎奢。如果一個類既有主構(gòu)造函數(shù)又有次構(gòu)造函數(shù),那么每個次構(gòu)造函數(shù)必須直接或間接調(diào)用主構(gòu)造函數(shù)饼灿,使用this
關(guān)鍵字:
class Person(val name: String) {
constructor(name: String, age: Int) : this(name) { /* ... */ }
constructor(name: String, age: Int, sex: String) : this(name, age) { /* ... */ }
}
主構(gòu)造函數(shù)調(diào)用在此構(gòu)造函數(shù)之前幕侠。注意,只有主構(gòu)造函數(shù)中的參數(shù)可以在類body中的任何地方調(diào)用碍彭,此構(gòu)造函數(shù)的參數(shù)只能在構(gòu)造函數(shù)中使用晤硕。下面是一個較為完整的例子:
class Person(val name: String) {
init {
println("name: ${this.name}")
}
constructor(name: String, age: Int) : this(name) {
println("name: $name, age: $age")
}
constructor(name: String, age: Int, sex: String) : this(name, age) {
println("name: $name, age: $age, sex: $sex")
}
}
fun main() {
val person = Person("張三")
}
// 輸出:
// name: 張三
// name: 張三, age: 18
// name: 張三, age: 18, sex: 男
Kotlin構(gòu)造函數(shù)參數(shù)支持默認值,如下:
class Person(val name: String = "李四") {
init {
println("name: ${this.name}")
}
constructor(name: String = "王五", age: Int = 20) : this(name) {
println("name: $name, age: $age")
}
constructor(name: String = "趙六", age: Int = 21, sex: String = "男") : this(name, age) {
println("name: $name, age: $age, sex: $sex")
}
}
fun main() {
val person = Person()
val person1 = Person(name="甲")
val person2 = Person(name="乙", age=10)
val person3 = Person(age=30)
val person4 = Person(sex="女")
}
// 第一行代碼輸出:
// name: 李四
// 第二行代碼輸出:
// name: 甲
// 第三行代碼輸出:
// name: 乙
// name: 乙, age: 10
// 第四行代碼輸出:
// name: 王五
// name: 王五, age: 30
// 第五行代碼輸出:
// name: 趙六
// name: 趙六, age: 21
// name: 趙六, age: 21, sex: 女
可以看到Kotlin會優(yōu)先使用參數(shù)較少的構(gòu)造方法庇忌,當傳入的參數(shù)不在此構(gòu)造方法時舞箍,Kotlin才會選擇參數(shù)更多的構(gòu)造方法。注意次構(gòu)造函數(shù)中參數(shù)不允許使用val/var
修飾皆疹。
繼承Inheritance
上面已經(jīng)說到疏橄,所有的類都直接或間接繼承自Any
類。在Kotlin中略就,所有的類默認都是final
的捎迫,因為類默認是不可繼承的,如果允許其被其他類繼承表牢,則需要使用open
關(guān)鍵字進行修飾:
open class Person { /* ... */ }
父類指定了主或次構(gòu)造函數(shù)時:如果子類有主構(gòu)造函數(shù)窄绒,則父類必須被在繼承時立即初始化:
open class Father(val lastName: String) {
init {
println("父親姓 ${lastName}")
}
}
class Son(val lastNameSon: String) : Father(lastNameSon) {
init {
println("兒子姓 $lastNameSon")
}
}
父類指定了主或次構(gòu)造函數(shù)時:如果子類沒有主構(gòu)造函數(shù),那么它的每個次構(gòu)造函數(shù)都必須使用super
初始化父類崔兴,或者委派給另外一個有此功能的此構(gòu)造函數(shù):
open class Father(val lastName: String)
class Son : Father {
constructor(familyName: String) : super(familyName) {
println("這里用的是父類的字段lastName=$lastName")
}
// 委派給子類的第一個次構(gòu)造函數(shù)
constructor(familyName: String, age: Int) : this(familyName) {
println("通過兒子的姓我們知道父親姓 $lastName彰导,兒子 $age 歲了")
}
}
fun main() {
val son = Son("王")
val son2 = Son("王", 20)
}
// 第一行代碼輸出:
// 這里用的是父類的字段lastName=王
// 第二行代碼輸出:
// 這里用的是父類的字段lastName=王
// 通過兒子的姓我們知道父親姓 王,兒子 20 歲了
父類沒有指定主或次構(gòu)造函數(shù)時:如果子類沒有指定主構(gòu)造函數(shù)和次構(gòu)造函數(shù)恼布,那么在繼承時使用父類默認的構(gòu)造函數(shù)螺戳;否則在主構(gòu)造函數(shù)處繼承父類構(gòu)造函數(shù)或者次構(gòu)造函數(shù)處使用super
實例化父類:
open class Father
class Son : Father()
class Son2 : Father {
constructor() : super() { /* ... */ }
}
Kotlin與Java一樣,僅僅支持單繼承折汞,但是可以實現(xiàn)多個接口倔幼。
重寫override
在Java中重寫父類方法使用的是@Override
注解,而在Kotlin中則使用override
關(guān)鍵字爽待,除此以外函數(shù)默認也是final
的损同,如果需要被在子類中重寫同樣需要使用open
關(guān)鍵字進行修飾:
open class Father {
open fun eat() { /* ... */ }
fun sleep() { /* ... */ }
}
class Son : Father() {
override fun eat() { /* .... */ }
}
使用final
關(guān)鍵字可以使方法不再次被子類重寫(只被自己重寫):
class Son : Father() {
final override fun eat() { /* .... */ }
}
Kotlin也支持屬性重寫,用法和函數(shù)重寫類似:
open class Father() {
open val age: Int = 40
}
class Son1(override val age: Int = 22) : Father() // age不可更改
class Son2 : Father() {
override var age = 20; // age此時可以更改
}
抽象類Abstract Class
Kotlin中的抽象類與Java類似鸟款,使用abstract
關(guān)鍵字進行修飾膏燃,需要注意的是當繼承抽象類時,需要對父類(抽象類)進行實例化:
abstract class Father
class Son : Father()
抽象類默認也是open
的何什。
接口Interface
Kotlin使用interface
聲明接口组哩,接口中的方法默認為open
,且支持默認方法實現(xiàn);接口中也可以有屬性:
interface Worker {
val count: Int // 抽象的屬性伶贰,不允許賦值
fun run() {
println("Worker中的方法")
}
}
注意蛛砰,實現(xiàn)類中對接口中的屬性進行重寫,不可以將var
屬性變?yōu)?code>val屬性黍衙,但可以將val
變?yōu)?code>var泥畅。
當子類同時實現(xiàn)或繼承接口與父類時,重寫規(guī)則如下:
open class Machine {
open fun run() {
println("Machine中的方法")
}
}
class Computer : Machine(), Worker {
val app = "QQ"
override fun run() {
println("Computer中的方法")
}
fun runAll() {
run()
// 通過這種方式調(diào)用父類或接口的方法
super<Machine>.run()
super<Worker>.run()
}
}
fun main() {
val com = Computer()
com.runAll()
}
// 輸出
// Computer中的方法
// Machine中的方法
// Worker中的方法
內(nèi)部類Inner Class
嵌套類就是在類中的類:
class Computer {
val app = "QQ"
class Desktop {
fun print() {
println("嵌套類中的方法")
}
}
}
嵌套類無法訪問外部類的方法或?qū)傩岳欧鞘褂?code>inner關(guān)鍵字修飾嵌套類時位仁,嵌套類就變成了內(nèi)部類,此時就可以訪問到外部類的屬性或方法了方椎,這是因為此時內(nèi)部類含有一個外部類的引用(編譯器自動生成的):
open class Machine {
open fun playOuter() {
println("父類方法")
}
}
class Computer : Machine() {
val app = "QQ"
override fun playOuter() {
println("外部類的方法")
}
inner class Desktop {
fun print() {
println(app)
}
fun play() {
playOuter() // 調(diào)用外部類方法
super@Computer.playOuter() // 調(diào)用外部類的父類的方法
}
}
}
fun main() {
val desktop = Computer().Desktop()
desktop.print()
desktop.play()
}
// 輸出:
// QQ
// 外部類的方法
// 父類方法
數(shù)據(jù)類Data Class
在Java中寫JavaBean時聂抢,我們常常需要對其重寫equals()
,hashCode()
,toString()
等方法以便于使用,但這些都是一些重復(fù)的工作棠众,我們也可以借助Lombok中的@Data
注解幫我們完成這些工作涛浙,而在Kotlin中,這些工作都可以借助關(guān)鍵字data
幫我們實現(xiàn)摄欲。數(shù)據(jù)類的聲明如下:
data class User(val name: String, val age: Int)
另外,數(shù)據(jù)類還給我們提供了函數(shù)copy()
幫我們實現(xiàn)數(shù)據(jù)類的復(fù)制疮薇,copy()
函數(shù)會針對每個數(shù)據(jù)類生成其對應(yīng)的構(gòu)造方法胸墙,以上面的數(shù)據(jù)類為例,它生成的copy()
方法與調(diào)用方式如下:
copy(val name: String, val age: Int)
// 如何使用
val oldUser: User = User("李四", 20)
val newUser: User = oldUser.copy(name="王五")
數(shù)據(jù)類還提供了componentN()
函數(shù)返回數(shù)據(jù)類的第N個屬性按咒,以上面的數(shù)據(jù)類為例迟隅,componentN()
函數(shù)使用如下:
val user = User("張三", 20)
val name1 = user.component1() // name1 = "張三"
val age1 = user.component2() // age1 = 20
// 或者(這里的component1(),component2()被自動調(diào)用)
val (name2, age2) = user // (name2 = "張三", age2 = 20)
// 還可以這么用
val collection = listOf(User("a", 1), User("b", 2), User("3", 3))
for ((name, age) in collection) {
println("name is $name")
println("age is $age")
}
數(shù)據(jù)類也可以有類body,我們同樣可以使用自己的方式重寫toString()
等方法励七。如果想讓數(shù)據(jù)類擁有無參的構(gòu)造方法智袭,可以給所有參數(shù)指定默認值。如果不想讓編譯器自動將參數(shù)添加到構(gòu)造方法中掠抬,可以將其放在body中:
data class User(val name: String) {
var age: Int = 18
}
數(shù)據(jù)類目前有以下幾種限制:
主構(gòu)造函數(shù)中至少有一個參數(shù)吼野;
主構(gòu)造函數(shù)中的參數(shù)必須使用
val
/var
修飾;數(shù)據(jù)類的父類中的
final
方法不會被重寫两波;數(shù)據(jù)類不能被
abstract
,open
,sealed
或inner
修飾瞳步;若父類有
open componentN()
并且返回兼容的類型,則會被重寫腰奋;若返回不兼容類型单起,則報錯;
密封類Sealed Class
密封類只能被和它同文件的類可見劣坊,對其他文件的類都不可見(但是它的子類可以被其他文件中的類可見)嘀倒。密封類使用sealed
關(guān)鍵字修飾:
sealed class Example
密封類是抽象的,而且不可以被實例化,但是可以擁有抽象屬性测蘑。密封類不允許有非private
的構(gòu)造方法灌危。
下面的是常用用途:
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
data class Other(val number: Double) : Expr()
// when相當于Java中的switch,但是有更強大的功能如類型轉(zhuǎn)換帮寻,我們將在函數(shù)一節(jié)中說明
fun eval(expr: Expr): Double {
return when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
else -> Double.NaN
}
}
單例類Object Class
在Java中使用單例類是常用的操作乍狐,在這里我們就不給出Java中單例的實現(xiàn)(可以看這篇)。Kotlin中原生支持了單例類的實現(xiàn):
object Singleton {
fun doSomeThing() { /* ... */ }
}
這種方式聲明的對象是線程安全的固逗,且在第一次訪問時才會被實例化浅蚪,由JVM保證單例。
調(diào)用其方法可以通過類似Java中的靜態(tài)訪問的方式進行:
Singleton.doSomeThing()
單例類也可以繼承其他父類烫罩。單例類可以作為嵌套類在其他類中惜傲,但是不可以在內(nèi)部類中(即不能在inner class
):
class Test {
object Singleton1 // 可以
inner class Inner {
object Singleton2 // 不可以
}
class Inner2 {
object Singleton3 // 可以
}
}
object
關(guān)鍵字還有一個作用是作為匿名內(nèi)部類的生成,用法如下:
interface Teacher {
fun speak()
}
fun giveAClass(t: Teacher) {
t.speak()
}
fun main() {
giveAClass(object : Teacher {
override fun speak() {
println("臨時老師開始說話")
}
})
// 還可以這么用
val point = object {
var x: Int = 100
var y: Int = 100
}
println(point.x)
}
這種通過匿名內(nèi)部類間接實現(xiàn)傳輸函數(shù)的方法在Java中比較常見贝攒,但是Kotlin中還有剛好的方法盗誊,我們將在函數(shù)一節(jié)中繼續(xù)說明。
除此之外隘弊,object
還可以作為伴生類的關(guān)鍵字哈踱。
伴生類Companion Class
伴生類的聲明方法如下:
class MyClass {
companion object Factory {
val type = "A"
fun create(): MyClass {
return MyClass()
}
}
}
伴生類可以讓我們用類似Java中靜態(tài)方法調(diào)用的形式掉用某些方法,上面的例子中我們就可以這樣調(diào)用:
val type = MyClass.type
val instance = MyClass.create()
// 或者
val type = MyClass.Factory.type
val instance = MyClass.Factory.create()
伴生類的類名聲明時可以去掉梨熙,這時候Kotlin會自動用Companion
作為伴生類的名稱开镣。注意,一個類中只能有一個伴生類咽扇。伴生類同樣可以繼承邪财。
內(nèi)聯(lián)類Inline Class
內(nèi)聯(lián)類目前還在測試中,可能之后會有變化质欲。內(nèi)聯(lián)類表示如下:
inline class Wrapper(val value: Type)
在業(yè)務(wù)中树埠,有時候因為業(yè)務(wù)邏輯需要對某些“東西”進行抽象,而單獨創(chuàng)建對象表示這種“東西”在運行時可能又比較重嘶伟,加大了系統(tǒng)負擔怎憋,內(nèi)聯(lián)類便是用來解決這種問題的,內(nèi)聯(lián)類可以看作一種包裝(Wrapper)九昧,它是對需要包裝的東西的抽象盛霎,在編譯時,Kotlin會為其生成一個包裝器耽装,在運行時愤炸,它要么用這個包裝器代表,要么用它主構(gòu)造函數(shù)中的參數(shù)類型代表掉奄,因此內(nèi)聯(lián)類有且只能有一個主構(gòu)造函數(shù)屬性规个,這個屬性將可能在運行時代替這個內(nèi)聯(lián)類凤薛。有個簡單的例子:
inline class Password(val vaule: String)
這樣便為密碼字符串生成了一個內(nèi)聯(lián)類,既可以方便開發(fā)诞仓,又不用增加系統(tǒng)創(chuàng)建對象的開銷缤苫。關(guān)于“代表”也有個例子,比如Kotlin中的Int
墅拭,它既可以用Java中的原始類型int
表示活玲,也可以用Integer
表示。
內(nèi)聯(lián)類可以繼承其他類谍婉,但是不能被其他類所繼承舒憾,內(nèi)聯(lián)類中可以定義函數(shù)或者其他屬性,但是這些調(diào)用在運行時都將變成靜態(tài)調(diào)用穗熬。
委派Delegation
Kotlin原生支持委派模式镀迂,所謂委派模式,是指將一個對象的工作委派給另一個對象唤蔗,在Kotlin中探遵,委派有類委派與屬性委派湖苞,通過by
關(guān)鍵字實現(xiàn)囱晴,通過委派,我們可以將父類的初始化工作交給子類接受的參數(shù):
interface Base {
val value: Int
fun printValue()
fun print()
}
class BaseImpl : Base {
override val value = 10
override fun printValue() {
println("BaseImpl value=${this.value}")
}
override fun print() {
println("BaseImpl print")
}
}
class Delegation(val base: Base) : Base by base {
override val value = 20
override fun print() {
println("Delegation print")
}
}
fun main() {
val baseImpl = BaseImpl()
val delegation = Delegation(baseImpl)
delegation.printValue()
delegation.print()
}
// 輸出:
// BaseImpl value=10
// Delegation print
可以看到使用委派對象后噪径,調(diào)用方法時會優(yōu)先調(diào)用重寫的方法棍掐,但是委派對象只能訪問自己的接口成員(雖然在Delegation
中重寫了接口成員规哪,但是委派對象在調(diào)用printValue()
時只能訪問自己的接口成員,并不能訪問Delegation
中的接口成員)塌衰。
Kotlin提供了函數(shù)lazy()
實現(xiàn)了屬性委托,通過by lazy({...})
的方式為屬性提供委托并實現(xiàn)了懶加載:
val lazyValue: String by lazy {
println("進行某些操作")
"Hello"
}
這里lazy
使用了lambda表達式蝠嘉,如果你還不清楚lambda表達式最疆,這里你可以簡單的理解為當代碼運行到這里時,會自動執(zhí)行塊中的代碼蚤告,并將最后一行的值作為塊的返回值努酸。通過lazy
函數(shù)進行委托,只有在第一次訪問lazyValue
時才會對其進行初始化并記住其值杜恰,之后的訪問將會直接返回這個值获诈。
lazy()
函數(shù)默認是線程安全的,如果需要多個線程同時訪問可以傳參LazyThreadSafetyMode.PUBLICATION
心褐;如果不需要線程同步可以傳參LazyThreadSafetyMode.NONE
舔涎。
還有一個常用的委派方法是使用Map進行傳參,這個方法常常用于解析JSON逗爹,用法如下:
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf(
"name" to "張三",
"age" to 20
))
println(user.name) // 張三
println(user.age) // 20