函數類型
在 Kotlin 中 函數是一等公民。
// 定義
fun greetPeople(name: String, makeGreet: (String) -> Unit)
// 調用
greetPeople("Jimy", :: greetingWithChinese)
在上面的定義中,markGreet 是個什麼?是個函數類型對象的引用,只有對象才能被作爲函數的參數。也就是說,我們需要把一個函數類型的一個具體 “對象” 當做函數參數傳遞。
這個 makeGreet 被稱之爲函數引用,Function Reference,這也是 Kotlin 官方的說法。
這也說明,在 Kotlin 中,函數也是一個對象。
函數引用
在 Kotlin 裏,一個函數名的左邊加上雙冒號,它就不表示這個函數本身了,而表示一個對象,或者說一個指向對象的引用,但,這個對象可不是函數本身,而是一個和這個函數具有相同功能的對象。
// 這是函數的定義
fun greetingWithChinese(name: String) {
println("早上好, $name !")
}
:: greetingWithChinese // 表示函數對象
既然它表示一個函數的對象,那麼,我們也可以把它賦值給一個變量。
val a = :: greetingWithChinese
val b = a
所以,我們也可以把這個變量作爲函數參數來調用
greetPeople("Jimy", a)
greetPeople("Jimy", b)
甚至,我們還可以直接通過這個引用調用 invoke()
來執行這個函數對象。
(:: greetingWithChinese).invoke("老王")
a.invoke("Jimy")
b("Lucy") // 還可以這樣調用,這個是 Kotlin 的語法糖,實際上調用的 invoke 方法
我們可以對一個函數對象調用 invoke()
方法, 但是,不能對一個函數調用 invoke()
方法
greetingWithChinese("Jimy") // 這個編譯報錯
高階函數
事實上,函數不僅可以當做一個函數的參數,還能作爲函數的返回值,
定義:一個函數如果參數類型是函數或者返回值類型是函數,那麼這就是一個高階函數。
換個角度看函數類型。
fun greetingWithChinese(name: String) {
println("早上好, $name !")
}
如果將這個函數的參數類型和返回值類型,抽象出來,它的函數類型就是 (String) -> Unit
,
那麼,我們就可以直接聲明一個變量,指定這個變量的函數類型是它,
val a = (String) -> Unit -> Int = :: greetingWithChinese
注意
前面說了,可以把這個變量,當做參數傳遞給另一個函數。那麼思考一下,有沒有一種可能:我把這個函數本身直接挪過來作爲參數使用呢?什麼意思?
// 這個是之前說的調用方式
greetPeople("Jimy", a)
我能不能向下面這麼寫?
greetPeople(
"Lucy",
fun greetingWithChinese(name:String) {
println("早上好, $name !")
}
)
如圖:
咦,編輯器報錯了,Anonymous functions with names are prohibited
禁止使用帶有名稱的匿名函數。提示我把函數名稱移除。
greetPeople(
"Lucy",
fun(name: String) {
println("早上好, $name !")
}
)
這樣就可以了。這也很好理解,函數名稱是給其他地方調用的,當前這種情況下這個函數作爲參數,並不會在其它地方調用,這個函數名稱也就沒有什麼作用,不允許有函數名稱,這種寫法叫做匿名函數。
而匿名函數實質上是一個表達式。
【】【】圖【】【】!!!
既然是表達式,那也就可以賦值給一個變量:
// 把匿名函數賦值給變量 c
val c = fun(name: String) {
println("早上好, $name !")
}
// 將 c 作爲參數傳遞,調用 greetPeople
greetPeople("Lucy", c)
那又有人會問了,能否把一個常規有名字的函數賦值給一個變量。比如
// 編譯報錯
val d = fun greetingWithChinese(name: String) {
println("早上好, $name !")
}
這是不允許的。
講道理,我都要把它賦值給一個變量了,那麼它本身的名字完全沒有必要。
有名字的函數不能賦值給一個變量,但是前面提到了,有名字的函數的引用,用雙冒號的形式,則可以。
只有對象纔可以賦值給一個變量,仔細想一想,匿名函數,其實不是函數,是一個對象。它是一個表達式,本質上是一個與它形式相同(參數,返回值一致)的函數類型的具體對象。
它和一個函數前面加上雙冒號是一樣的。
Lambda 表達式
對於一個函數類型的變量,我們除了可以把一個函數對象或者一個匿名函數賦值給它,還可以賦值一個 lambda 表達式。
什麼是 lambda 表達式?
// 匿名函數
val c = fun(name: String) {
println("早上好, $name !")
}
// 等價於下面的 lambda 表達式
{ name: String -> Unit
println("早上好, $name !")
}
一個完整的 Lambda 表達式聲明如下:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
由於 Kotlin 支持類型推導,所以它可以簡化爲
val sum: (Int, Int) -> Int = { x, y -> x + y }
或者:
val sum = { x: Int, y: Int -> x + y }
這個 lambda 表達式,也是一個函數類型的具體對象。所以也可以當做另一個函數的參數傳入。
回到前面的例子,調用 greetPeople()
就可以寫爲
greetPeople(
"Lucy",
{ name: String -> Unit
println("早上好, $name !")
})
而 Kotlin 中的函數,如果最後一個參數是 lambda 表達式,則可以將 lambda 表達式寫到參數外面:
greetPeople("Lucy") { name: String -> Unit
println("早上好, $name !")
}
就是這麼個形式,還有,lambda 表達式支持類型推導,前面定義了函數
fun greetPeople(name: String, makeGreet: (String) -> Unit)
從這個函數定義處,已經知道,調用時 lambda 函數表達式應該是什麼樣的函數類型,它的參數,返回值類型都確定了,所以可以在 lambda 表達式中省略,如下:
greetPeople("Lucy") { name ->
println("早上好, $name !")
}
再來看 Android 中常用的一個例子, 給 View 設置點擊事件:
public interface OnClickListener {
void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
this.listener = listener;
}
java 中調用:
view.setOnClickListener(new OnClickListener() {
@Override
void onClick(View v) {
doSomething();
}
});
到 Kotlin 中就可以這麼寫:
fun setOnclickListener(onClick: (View) -> Unit) {
this.onClick = onClick
}
view.setOnclickListener({view: View -> Unit
doSomething()
})
將 lambda 表達式移到括號外面:
view.setOnclickListener() {view: View -> Unit
doSomething()
}
參數和返回值類型可推導,省略參數和返回值類型:
view.setOnclickListener() {view ->
doSomething()
}
如果 lambda 表達式是唯一參數,則括號都可以直接省略,同時 lambda 表達式如果是單參數,則這個參數也可以直接省略不寫。因爲對於 Kotlin 的 Lambda,單參數有一個默認的名稱:it
, 使用的時候用 it
替代。
view.setOnclickListener() {
doSomething()
}
所以就變成我們經常看到的樣子了。
對於多參數的 lambda 表達式,我們不能省略掉參數,但是如果參數沒有被用到,可以用 _
來代替。
所以 Kotlin lambda 表達式本質是什麼呢?
其實和匿名函數一樣,本質上也是一個函數類型的具體對象。塔可以作爲函數的參數傳入,也可以賦值給一個變量。
另外 lambda 表達式也可以進行自調用
{x: Int, y: Int -> x + y}(1, 2)
總結一下
lambda 表達式語法:
1. lambda 表達式必須通過 {} 來包裹
2. 如果 lambda 聲明瞭參數部分的類型,且返回值支持類型推導,則 lambda 表達式變量就可以省略函數類型聲明
3. 如果 lambda 變量聲明瞭函數類型,那麼 lambda 表達式的參數部分的類型就可以省略
4. 如果 lambda 表達式返回的不是 Unit 類型,則默認最後一行表達式的值類型就是返回值類型。
lambda 表達式和函數的區別
- fun 在沒有等號、只有花括號的情況下,就是代碼塊函數體,如果返回值非 Unit,必須帶 return
fun foo(x: Int) {
print(x)
}
fun foo(x: Int, y: Int): Int {
return x + y
}
- fun 帶有等號,沒有花括號,是單表達式函數體,可以省略 return
fun foo(x: Int, y: Int) = x + y
- 不管是用 val 還是 fun 聲明,如果是等號加花括號的語法,就是聲明一個 lambda 表達式。
val foo = { x: Int, y: Int ->
x + y
}
// 調用方式: foo.invoke(1, 2) 或者 foo(1, 2)
fun foo(x: Int) = { y: Int ->
x + y
}
// 調用方式: foo(1).invoke(2) 或者 foo(1)(2)
類、接口與對象
類
Kotlin 中用 'class' 聲明類,如果沒有實體類,可以省略 {}
Kotlin 多個類可以聲明在同一個文件中
構造函數
在 Kotlin 中的一個類可以有一個主構造函數以及一個或多個次構造函數。主構造函數是類頭的一部分:它跟在類名(與可選的類型參數)後。
class Student constructor(id: Int, name: String, age: Int) {
}
如果主構造函數沒有任何註解或者可見性修飾符,可以省略這個 constructor 關鍵字。
class Student(id: Int, name: String, age: Int) {
constructor(name: String, age: Int): this(0, name, age) // 次構造函數
}
init 代碼塊
類中允許存在多個 init
代碼塊,對於不同的業務初始化可以放在不同的 init 代碼塊中,按代碼順序一次執行。
類似於 java 類中的靜態代碼塊。
init 代碼塊中可以直接訪問構造函數的參數,其它地方不可以。
成員變量
聲明
- 和 java 類似,可以在類代碼塊中直接聲明。
- 構造方法參數加上
var
或者val
修飾,則可以視爲類中聲明瞭一個同名的成員變量。也可以加上可見性修飾符。
初始化
成員變量聲明時需進行初始化,或者顯式聲明延遲初始化,使用關鍵字 lateinit
顯示聲明延遲初始化。
class Student(id: Int, var name: String, var age: Int) {
constructor(name: String, age: Int): this(0, name, age)
init {
println("student name is $name")
}
fun printInfo() {
age += 1
}
lateinit var gender: String // 延遲初始化
}
繼承
使用 :
open class Person(id: Int, name: String)
class Student(id: Int, var name: String, var age: Int): Person(id,name) {
// ...
}
被繼承的類需要用 open
修改,同樣父類的中法,默認是不可被重寫的,若允許被重寫,需要使用 open
修飾。
接口
interface MyInterface {
fun bar()
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
接口允許有屬性,接口中的方法允許有默認實現,接口不需要用 open
修飾,因爲接口就是用來讓子類實現,然後被被子類重寫的。包括屬性也是可以被重寫的。接口可以繼承自其它接口。
內部類
java 中在類中再聲明類,稱之爲內部類,如果使用 static 修飾,則爲靜態內部類。
class A {
private String a
class B {
}
static class C {
}
}
請問 B 和 C 中能訪問 A 的成員 a 嗎?
在 Kotlin 中沒有 static 關鍵字,默認聲明就是靜態內部類,如要聲明內部類,需要添加 inner
關鍵字
class A {
// 聲明內部類需要使用 inner 關鍵字
inner class B {
}
// 靜態內部類
class c {
}
}
伴生對象
Kotlin 中沒有了 static 關鍵字,但是有伴生對象的概念,類似於 java 中的 static
class Student {
companion object {
const val TAG = "Student" // 常量
var count = 0 // 靜態變量
// 靜態方法
fun test() {
}
}
}
object
是在 Kotlin 中的用法。
- 在類中與
companion
一起,表示伴生對象。 - 聲明類。Kotlin 中除了用 class 聲明類意外,還可以用 object 來聲明一個類,用 object 聲明的類,天生單例。
object SystemUtils {
...
}
- 實現某個接口,或者抽象類:
interface ICount {
fun count(num: Int): Int
}
val myCount = object: ICount {
override fun count(num: Int): Int {
TODO("Not yet implemented")
}
}
可見性修飾符
- public 公開,可見性最大,哪裏都可以引用
- private 私有,可見性最小,根據聲明位置可以分爲類中可見和文件中可見。
- protected 保護, 相當於 private + 子類可見。
- internal 內部,同一 module 可見
java 中 protected 表示包內可見 + 子類可見
Kotlin protected 的可見範圍收窄了,原因是 相比於包, Kotlin 更注重 module。
另外 private 修飾 java 中的內部類對外部類可見,Kotlin 中的內部類對外部類不可見。
Kotlin 中的類和方法,不寫時,默認是 public + final 的。
數據類
data class Student(val name: String, var age: Int = 18)
- 自動生成
getter()/setter()
方法 !!!??? - 編譯器自動生成
equals()/hashCode()
方法 - 編譯器自動生成
toString()
方法 - 編譯器自動生成 `componentN()`` 函數(解構)
- 編譯器自動生成
copy
函數
data class Student(val name: String, var age: Int = 18) {
var isBoy = true // 該屬性不會在生成的方法中
}
密封類
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
可以用來代替枚舉,使用 When 表達式時如果能覆蓋所有,則無需 else 分支。
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 `else` 子句,因爲我們已經覆蓋了所有的情況
}
其它方便的特性:
函數參數默認值
fun sayHi(name: String = "world") = println("Hello " + name)
重載函數再也不用寫很多個了。
本地函數(嵌套函數)
本地函數是個啥玩意?
我們知道在函數中可以聲明局部變量(這不是廢話嗎?)在 Kotlin 中我們甚至還可以在方法中類似聲明局部變量一樣聲明一個方法,稱之爲本地函數。
fun login(user: String, password: String, illegalStr: String) {
// 驗證 user 是否爲空
if (user.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 驗證 password 是否爲空
if (password.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 執行登錄
}
校驗參數這不部分代碼有些冗餘,我們可以抽成一個方法,但是我們又沒有必要暴露到其它地方,因爲只有這個 login 方法會用到,則可以在 login 方法內部聲明一個嵌套函數
fun login(user: String, password: String, illegalStr: String) {
fun validate(value: String, illegalStr: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
validate(user, illegalStr)
validate(password, illegalStr)
// 執行登錄
}
try-catch 表達式
Kotlin 中 try-catch 語句也可以是一個表達式,允許代碼塊的最後一行作爲返回值
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
其它知識:
數組、集合(可變集合、不可變集合)、泛型(逆變和協變)、枚舉、委託、註解、反射
內聯函數/中綴函數
== 和 ===
Kotlin 中用 ==
判斷 equals 相等,對象內容相等
===
判斷相等引用的內存地址相等
拓展函數和拓展熟悉
顧名思義,可以給一個類“增加”額外的方法和屬性。
舉個例子
data class Student(val name: String)
這個類本身沒有“上課”這個方法,我現在給它增加一個拓展函數。
fun Student.attendClass() {
println("${this.name} 正在上課")
}
這樣,Student 這個類的實例對象就可以調用這個拓展方法了。
val student = Student("Jimy")
student.attendClass()
拓展方法直接定義在文件中。
拓展屬性類似。
想一想:三方庫裏面的類,不能直接修改,是不是可以增加拓展方法和拓展屬性了?有沒有很激動。
Top-level
kotlin 文件已 .kt
爲後綴,在 Kotlin 文件中,我們可以直接聲明的變量,方法,也就是不在 class 內部聲明,稱之爲 top-level declaration 頂層聲明。
// 屬於 package,不在 class/object 內
const val KEY = "123456"
fun test() {
println("---test---")
}
它不屬於任何 class , 而是直接屬於 package, 它和靜態變量一樣是全局的,使用起來更方便,調用它的時候連類名都不用寫:
import com.sharpcj.demo.test
import com.sharpcj.demo.KEY
test()
val x = KEY
結合前面說的,寫工具類一般就有三種方式:
// companion object
class Util {
companion object {
fun test1() {
}
fun test2() {
}
}
}
// object
object Util {
fun test1() {
}
fun test2() {
}
}
// top level 方法
fun test1() {
}
fun test2() {
}
建議:
- 如果想寫工具類的功能,直接創建文件,寫 top-level「頂層」函數。雖然沒有 class 的概念,但是相關的方法,寫在同一個文件中,方便閱讀管理。
- 如果需要繼承別的類或者實現接口,就用
object
或companion object
。
by lazy (委託)
前面提到,如果一個屬性需要延遲初始化,可以使用 lateinit
進行修飾,另外還有另外一種方式,就是使用 by lazy
方法初始化。
在給一個變量賦值的時候使用 by lazy
代碼塊,可以做到單例,在第一次使用時初始化。by lazy 實際上使用了 Kotlin 的委託特性,底層原理和 DCL 的單例模式類似。
val student: Student by lazy {
Student(name = "Lucy", age = 18)
}
這裏順便說一下用 Kotlin 實現單例,看看有多有多方便了。
前面提到,用 object 聲明一個類,即是單例的。
object Singleton
再看看 DCL 的方式,java 實現:
public class Singleton {
private Singleton(){}
private volatile static Singleton instance;
private static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
再看看 Kotlin 實現
class Singleton private constructor() {
companion object {
val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
Singleton()
}
}
}
這樣是不是很方便,當然上述方式也有一個缺點,就是獲取單例的時候沒有辦法傳參,那麼改爲和 java 一樣的方式看看:
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance(): Singleton {
return instance?: synchronized(this) {
instance?:Singleton().also {
// 初始化工作
instance = it
}
}
}
}
}
這次可以傳參進來了,代碼依然簡潔很多。
其它:
數組、集合(可變集合和不可變集合,集合操作符)、泛型(協變和逆變)、註解、反射。
委託、內聯函數、中綴函數、攜程等等。