如果你會 Java, 那麼來看一看 Kotlin , 基礎入門。 如果你不理解 Kotlin 的lambda 表達式,那麼來看一看,幫助你真正理解函數類型,lambda 表達式。 ...
目錄
一、基礎語法
Koltin 是一種靜態強類型語言,它要求在編譯時確定變數類型,併在運行時嚴格執行類型檢查。
- 聲明變數時必須指定變數類型,無法隱式地將不同類型的值分配給變數。
- 有助於減少類型錯誤,使得代碼更加穩健和可維護。
1.1 常見數據類型
Byte、 Short、 Int、 Long、 Float、 Double、 Char、 Boolean、 String
特殊的類型: Unit, 類似 java 中的 Void, 函數或者表達式沒有返回值時用 Unit
無 java 中的 int、 Integer 等,避免裝箱拆箱的性能損耗。
在某些情況下,Kotlin 仍然需要進行裝箱拆箱操作,例如將基本類型的值傳遞給需要對象類型的函數時,或者將基本類型的值放入集合或數組中時。
轉換:
3.toFloat()
val a = 5.6f
a.toInt()
字元串 String
val name: String = "Lucy"
// 可迴圈
for (c in name) {
}
// 可拼接
println(name + "abc") // Lucyabc
// 字面值
val s = "Hello, world!\n" // 反斜杠轉義
// 三引號內部不轉義,可以包含任何字元,包括換行符
val text = """
for (c in "abc") {
println(c)
}
"""
// 字元串模板
val name = "James"
println("$name is a basketball player!")
// 用 ${} 引用表達式
println("$name 這個單詞的長度是 ${name.length}")
1.2 變數
1.2.1 變數聲明
var um: Int = 0 // 使用 var 聲明一個可讀可寫的變數
val name: String = "Jimy" // 使用 val 聲明一個引用不可變的變數,對象成員可變,Java 中 final 的效果, 儘可能採用 val 聲明變數。
1.2.2 類型推斷
val name = "Lucy" // name 可以推斷出 name 是 String 類型
name.uppercase() // 正確
// fails to compile
name.inc() // inc() 是 Int 類型的的運算浮, String 類型無法調用
1.2.3 Null 安全
可空類型
// Fails to complie
val name: String = null
要使變數持有 null 值,它必須是可為 null 類型,在變數類型後面加上一個 ?
尾碼。
val name: String? = null
指定 String?
類型後,可以為 name 賦予 String
值或者 null
安全調用
安全調用 ?.
非空斷言 !!
// java
String name = "Lucy"
if (name != null ) {
name.toUpperCase()
}
// Koltin
val name: String? = null
name?.uppercase() // name 不為 null 時執行方法調用
name!!.uppercase() // name 為 null 時,拋出異常
多層判斷可以鏈式調用。
Elvis操作符
// java
int nameLength = name!=null ? name.length : -1
// Koltin
val nameLength = name?.length?:-1
註意: 上面 Kotlin 版本中 nameLength 類型為 Int
。
val name: String? = "Lucy"
val length = name?.length
上面這段代碼中 length 是什麼類型? ----- Int?
1.2.4 面向對象語言
Any ---- 類似於 java 中的 Object, 所有非空類型的根類型
Any? ---- 所有類型的根類型。 自然也是 Any 的跟類型。
Nothing ---- 所有類型的最底層,包含一個值 null
1.3 流程式控制制
1.3.1 if 表達式
// 傳統寫法
var max: Int
if (a > b) {
max = a
} else {
max = b
}
// 表達式寫法
var max: Int = if (a > b) a else b
註意: if 作為表達式而不是語句時,需要有 else 分支。
1.3.2 When 表達式
when(x) {
1 -> { println("x == 1") }
is Int -> { println("x is a Int") }
2, 3 -> {println("x == 2 or x == 3")} // 多個分支條件放在一起用 , 隔開
else -> { println("x maybe a string") }
}
多個分支條件放在一起用 , 隔開。
when 將它的參數與所有的分支條件順序比較,直到某個分支滿足條件(匹配第一個滿足條件的分支)。
when 既可以被當做表達式使用也可以被當做語句使用。如果它被當做表達式, 符合條件的分支的值就是整個表達式的值。必須有 else 分支, 除非編譯器能夠檢測出所有的可能情況都已經覆蓋了。(例如 枚舉 或者 密封類子類。
When 進階用法,不提供參數:
if (age > 60) {
println("老年人,可以享受6折優惠")
} else if (age > 0) {
if (identity is Student) {
println("學生,可以享受半價優惠")
} else {
println("需要全價")
}
} else {
throw AgeIlligalException("age must be greater than 0")
}
等價於
when {
age < 0 -> throw AgeIlligalException("age must be greater than 0")
age > 60 -> println("老年人,可以享受6折優惠")
identity is Student -> println("學生,可以享受半價優惠")
else -> println("需要全價")
}
1.3.3 For 迴圈
// rangeTo [0, 10]
for(item in 0 .. 10) {
...
}
// [0, 10) 中綴函數
for (i in 0 until 10) {
}
for(item in list) {
}
for ((index, value) in list.withIndex()) {
}
1.3.4 While 迴圈
while (x > 0) {
x--
}
do {
val y = 5
y--
} while (y > 0) // y 在此處可見的
二、函數與 lambda 表達式
2.1 函數聲明
Kotlin 函數用 fun
聲明,預設是 public 的。
代碼塊函數體:
fun sum(x: Int, y: Int): Int {
return x + y
}
表達式函數體:
fun sum(x: Int, y: Int) = x + y
Kotlin 支持類型推導,使用表達式函數體,一般情況下可以不聲明返回值類型。在一些諸如遞歸等複雜情況下,即使是使用表達式函數體,也必須顯示聲明返回值類型。
總結:
- 函數參數必須顯示聲明類型
- 非表達式函數體,函數參數必須顯示聲明類型, 返回值除了類型是 Unit,可以省略,其它情況,返回值都必須顯示聲明類型
- 如果它是一個遞歸的函數,必須顯示聲明參數和返回值類型
- 如果它是一個公有方法,為了代碼的可讀性,儘量顯示聲明函數參數和返回值類型
2.2 函數類型
2.2.1 示例引入
先看一個例子:
// 定義一個打招呼的方法
fun greetPeople(name: String) {
// ... 做一些額外的事情
greetingWithEnglish(name)
}
private greetingWithChinese(name: String) {
println("早上好, $name!")
}
假設現在要進行國際化,加入了英語打招呼的方式
fun greetingWithEnglish(name: String) {
println("Good morning, $name!")
}
需要對 greetPeople
方法進行改造
enum class Language {
Chinese, English
}
fun greetPeople(name: String, lang: Languge) {
// ... 做一些額外的事情
when(lang) {
Chinese -> greetingWithChinese(name)
English -> greetingWithEnglish(name)
}
}
這樣是不是拓展性很差,有沒有一種可能,我把要調用的函數單做一個參數傳進去,我傳什麼參數,就調用什麼方法?
我希望的樣子,大概如下:
// 定義 greetPeople 方法
fun greetPeople(name: String, makeGreet: ***) {
makeGreet(name)
}
註意看,這個 *** 所在的這個位置應該是放置參數的類型,那這個應該是什麼類型呢,很多編程語言裡面有,很遺憾,在 java 裡面沒有這樣的類型。但是 java 我們可以有一種變通方案來實現 —— 介面回調。
用介面包裝一下方法,先看一下用 java 如何實現
// 定義一個介面
public interface IGreet {
void makeGreet(String name);
}
// 把介面作為參數
public void greetPeople(String name, IGreet: greetMethod) {
greetMethod.makeGreet(name);
}
// 調用
greetPeople("Jimy", new IGreet() {
@Override
public void makeGreet(String name) {
greetingWithEnglish("Jimy")
}
});
// java 8 lambda 表達式
greetPeople("Jimy", name -> {
greetingWithEnglish("Jimy")
});
2.2.2 Koltin 函數類型
從上面例子來看,實際上我希望把一個函數當做參數傳入一個方法,但是又找不到合適的類型來聲明這個參數,幸運的是,Kotlin 提供了這樣的類型——函數類型。
在 Kotlin 中,聲明函數類型必須遵循一下幾點:
Kotlin 中,函數類型聲明必須遵循以下幾點:
- 通過 -> 符號來組織參數類型和返回值類型。左邊是參數類型,右邊是返回值類型
- 必須用一個括弧來包裹參數類型
- 返回值類型即使是 Unit, 也必須顯示聲明。
() -> Unit // 代表無參數,無返回值的函數類型
(Int, String) -> Boolean // 代表第一個參數是 Int 類型,第二個參數是 String 類型,返回值類型是 Boolean 類型的函數類型
在 Kotlin 中 函數是一等公民。
再看上面的例子,我們需要的參數 makeGreet
的類型應該是只有一個參數,類型為 String, 且無返回值的函數類型,如下:
(String) -> Unit
Kotlin 實現上面的代碼 :
// 定義 greetPeople 方法
fun greetPeople(name: String, makeGreet: (String) -> Unit) {
makeGreet(name)
}
// 調用
greetPeople("Jimy", :: greetingWithChinese)
在上面的定義中,markGreet 是個什麼?是個函數類型對象的引用,只有對象才能被作為函數的參數。也就是說,我們需要把一個函數類型的一個具體 “對象” 當做函數參數傳遞。
這個 makeGreet 被稱之為函數引用,Function Reference,這也是 Kotlin 官方的說法。
這也說明,在 Kotlin 中,函數也可以是一個對象。
2.2.3 函數引用
在 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 方法
2.2.4 高階函數
事實上,函數不僅可以當做一個函數的參數,還能作為函數的返回值
定義:一個函數如果參數類型是函數或者返回值類型是函數,那麼這就是一個高階函數。
換個角度看函數類型
fun greetingWithChinese(name: String) {
println("早上好, $name !")
}
如果將這個函數的參數類型和返回值類型,抽象出來,它的函數類型就是 (String) -> Unit,
那麼,我們就可以直接聲明一個變數,指定這個變數的函數類型是它,
val a = (String) -> Unit = :: 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 !")
}
這是不允許的。
講道理,我都要把它賦值給一個變數了,那麼它本身的名字完全沒有必要。
有名字的函數不能賦值給一個變數,但是前面提到了,有名字的函數的引用,用雙冒號的形式,則可以。
只有對象才可以賦值給一個變數,仔細想一想,匿名函數,其實不是函數,是一個對象。它是一個表達式,本質上是一個與它形式相同(參數,返回值一致)的函數類型的具體對象。
它和一個函數前面加上雙冒號是一樣的。
2.3 lambda 表達式
2.3.1 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 表達式,也是一個函數類型的具體對象。所以也可以當做另一個函數的參數傳入。
2.3.2 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 表達式,我們不能省略掉參數,但是如果參數沒有被用到,可以用 _
來代替。
2.3.3 lambda 表達式自調用
另外 lambda 表達式也可以進行自調用
{x: Int, y: Int -> x + y}(1, 2)
2.3.4 總結
所以 Kotlin lambda 表達式本質是什麼呢?
其實和匿名函數一樣,本質上也是一個函數類型的具體對象。它可以作為函數的參數傳入,也可以賦值給一個變數。
總結一下
lambda 表達式語法:
- lambda 表達式必須通過 {} 來包裹
- 如果 lambda 聲明瞭參數部分的類型,且返回值支持類型推導,則 lambda 表達式變數就可以省略函數類型聲明
- 如果 lambda 變數聲明瞭函數類型,那麼 lambda 表達式的參數部分的類型就可以省略
- 如果 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)
三、介面、類與對象
3.1 類
Kotlin 中用 class
或者 object
聲明類,如果沒有類體,可以省略 {}
區別在於用 object
聲明的類是一個單例類,無法被實例化。
Kotlin 多個類可以聲明在同一個文件中。
3.1.1 構造函數
在 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) // 次構造函數
}
3.1.2 init 代碼塊
類中允許存在多個 init
代碼塊,對於不同的業務初始化可以放在不同的 init
代碼塊中,按代碼順序依次執行。
類似於 java 類中的靜態代碼塊。
init 代碼塊中可以直接訪問構造函數的參數,其它地方不可以。
3.1.3 成員變數
聲明
- 和 java 類似,可以在類代碼塊中直接聲明。
- 構造方法參數加上
var
或者val
修飾,則可以視為類中聲明瞭一個同名的成員變數。也可以加上可見性修飾符。
初始化
成員變數聲明時需進行初始化,或者顯式聲明延遲初始化,使用關鍵字 lateinit 顯示聲明延遲初始化
class Student(id: Int, name: String, var age: Int) {
constructor(name: String, age: Int): this(0, name, age)
// init 代碼塊中訪問構造函數的參數
init {
println("student name is $name")
}
fun printInfo() {
age += 1
}
lateinit var gender: String // 延遲初始化
}
3.1.4 繼承
使用 :
open class Person(id: Int, name: String)
class Student(id: Int, var name: String, var age: Int): Person(id,name) {
// ...
}
被繼承的類需要用 open
修改,同樣父類的中法,預設是不可被重寫的,若允許被重寫,需要使用 open
修飾。
3.2 介面
interface MyInterface {
fun bar()
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
介面允許有屬性,介面中的方法允許有預設實現,介面不需要用 open 修飾,因為介面就是用來讓子類實現,然後被被子類重寫的。包括屬性也是可以被重寫的。介面可以繼承自其它介面。
3.3 數據類
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 // 該屬性不會在生成的方法中
}
3.4 內部類
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 {
}
}
3.5 密封類
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` 子句,因為我們已經覆蓋了所有的情況
}
3.6 伴生對象
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")
}
}
3.7 可見性修飾符
- public 公開,可見性最大,哪裡都可以引用
- private 私有,可見性最小,根據聲明位置可以分為類中可見和文件中可見。
- protected 保護, 相當於 private + 子類可見。
- internal 內部,同一 module 可見
java 中 protected 表示包內可見 + 子類可見
Kotlin protected 的可見範圍收窄了,原因是 相比於包, Kotlin 更註重 module。
另外 private 修飾 java 中的內部類對外部類可見,Kotlin 中的內部類對外部類不可見。
Kotlin 中的類和方法,不寫時,預設是 public + final 的。
四、 Kotlin 的一些其它特性
4.1 函數參數預設值
fun sayHi(name: String = "world") = println("Hello, " + name)
重載函數再也不用寫很多個了。
4.2 本地函數(嵌套函數)
本地函數是個啥玩意?
我們知道在函數中可以聲明局部變數(這不是廢話嗎?)在 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)
// 執行登錄
}
4.3 try-catch 表達式
Kotlin 中 try-catch
語句也可以是一個表達式,允許代碼塊的最後一行作為返回值
val a: Int? = try {
parseInt(input)
} catch (e: NumberFormatException) {
null
}
4.4 == 和 ===
Kotlin 中用 ==
判斷 equals 相等,對象內容相等
===
判斷相等引用的記憶體地址相等
4.5 拓展函數和拓展屬性
顧名思義,可以給一個類“增加”額外的方法和屬性
舉個例子:
data class Student(val name: String)
這個類本身沒有“上課”這個方法,我現在給它增加一個拓展函數。
fun Student.attendClass() {
println("${this.name} 正在上課")
}
這樣,Student 這個類的實例對象就可以調用這個拓展方法了。
val student = Student("Jimy")
student.attendClass()
拓展方法直接定義在文件中。
拓展屬性類似。
想一想:三方庫裡面的類,不能直接修改,是不是可以增加拓展方法和拓展屬性了?有沒有很激動。
4.6 Top-level
kotlin 文件已 .kt
為尾碼,在 Kotlin 文件中,我們可以直接聲明的變數,方法,也就是不在 class 內部聲明,稱之為 top-level declaration
頂層聲明。
// 屬於 package,不在 class/object 內
const val KEY = "123456"
fun test() {
println("---test---")
}
它不屬於任何 class , 而是直接屬於 package, 它和靜態變數一樣是全局的,使用起來更方便,調用它的時候連類名都不用寫:
import com.chongjiu.demo.test
import com.chongjiu.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。
4.7 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
}
}
}
}
}
這次可以傳參進來了,代碼依然簡潔很多。
五、 More
Kotlin 其它內容還有很多:
數組
集合(可變集合、不可變集合、集合操作符)
協程
泛型(泛型類、泛型函數、逆變和協變)
註解
反射
委托
內聯函數、中綴函數等等
...