1. 類與繼承
1.1 類的用法
與java一樣使用class聲明類,後面可以跟着大括號:
class Invoice { /*……*/ }
class Empty
1.2 構造函數
kotlin可以有一個構造函數以及多個次構造函數。主構造函數是類頭的一部分,跟在類頭後面:
class Person constructor(firstName: String) { /*……*/ }
如果主構造函數沒有任何註解或者可見性修飾符,那麼可以省略constructor。
class Person(firstName: String) { /*……*/ }
主構造函數不能包含任何的代碼,初始化的代碼可以放到Init初始化代碼塊中。主構造函數的參數可以在屬性的初始化器中使用。初始化是按類體中出現的順序執行:
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
如果沒有定義主構造函數,類照樣會定義一個無參構造函數。也會去調用Init初始化模塊:
fun main() {
InitOrderDemo()
}
class InitOrderDemo{
init {
println("== init ==")
}
}
//打印== init ==
1.3 次構造函數
次構造函數是在函數體中使用constructor構造一個構造函數。如果當前有定義主構造函數,那麼次構造函數必須委託給主構造函數:
class Person {
var children: MutableList<Person> = mutableListOf<>()
constructor(parent: Person) {
parent.children.add(this)
}
}
class Person(val name: String) {
var children: MutableList<Person> = mutableListOf<>()
constructor(name: String, parent: Person) : this(name) {//有構造主構造函數,則次構造函數必須調用this
parent.children.add(this)
}
}
1.4 創造類的實例
類可以主構造函數或者次構造函數構建對象。對於定義了主構造函數的類,那麼無參的默認構造函數就不能使用了。
fun main() {
InitOrderDemo("Hello","parent")
InitOrderDemo("Hello")
InitOrderDemo() //這一行會報錯:none of the following functions can be called with the arguments supplied
}
class InitOrderDemo(name: String) {
init {
println("== initializer block == ")
}
constructor(name: String, parent: String):this(name){
println("== constructor ==")
}
}
1.5 類成員
類可以包含:
-
構造函數與初始化塊
-
函數
-
屬性
-
嵌套類與內部類
-
對象聲明
1.6 繼承
首先,所有的類都繼承自一個共同的超類。他提供了三個基本的函數:equals()
、 hashCode()
與 toString()
。
其次,和java不一樣。kotlin類默認是final修飾的,也就是說如果想要開放繼承那麼就需要open修飾。
最後,對於子類的構造函數,分爲兩種情況。
- 子類有主構造函數
如果子類有主構造函數, 則基類必須在主構造函數中立即初始化。
open class Person(var name : String, var age : Int){// 基類
}
class Student(name : String, age : Int, var no : String, var score : Int) : Person(name, age) {
}
- 子類沒有主構造函數
open class Person(name:String){
init{
println("父類主構造函數:姓名:${name}")
}
}
class Student:Person{
constructor(name:String,age:Int):super(name){//使用 super 關鍵字初始化其基類型
println("次構造函數1:學生名: ${name},年齡:${age}")
}
constructor(name:String,age:Int,no:String):this(name,age){//委託給另一個構造函數
println("次構造函數2:學生名: ${name},年齡:${age},學號:${no}")
}
}
fun main(args: Array<String>) {
Student("Runoob", 18, "S12345")
println("*************************")
Student("Runoob", 18)
}
如果子類沒有主構造函數,那麼他可以調用父類的構造函數或者委託給其他的次構造函數。
1.6 覆蓋方法
和java一樣,在重寫的函數前面要加上override關鍵字。但是被重寫的函數必須是open修飾的。
open class Shape {
open fun draw() {
println("Shape::draw")
}
fun fill() {
println("Shape::fill")
}
}
class Circle() : Shape() {
override fun draw() {
println("Circle::draw")
}
override fun fill() {//由於fill並不是open修飾的。error: 'fill' in 'Shape' is final and cannot be overridden
println("Circle::fill")
}
}
1.7 覆蓋屬性
覆蓋屬性和覆蓋方法在操作上差不多。主要是val和var在覆蓋上有差別:
open class Shape {
open var value1: Int = 0
open val value2: Int = 0
}
class Rectangle : Shape() {
override val value1 = 4//val覆蓋var是不可以的
override var value2 = 4//var覆蓋val是可以的
}
1.8 派生類初始化順序
這個比較容易理解。顯示初始化基類的,再去初始化派生類。
open class Base() {
init { println("Initializing Base") }
}
class Derived() : Base() {
init { println("Initializing Derived") }
}
fun main(args: Array<String>) {
Derived()
}
1.9 調用超類實現
跟java差不多。有一個區別就是kotlin的可以使用@來使用外部類的函數:super@Outer
:
class FilledRectangle: Rectangle() {
fun draw() { /* …… */ }
val borderColor: String get() = "black"
inner class Filler {
fun fill() { /* …… */ }
fun drawAndFill() {
super@FilledRectangle.draw() // 調用 Rectangle 的 draw() 實現
fill()
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 所實現的 borderColor 的 get()
}
}
}
1.10 覆蓋規則
與java的重寫比較大的差別是他可以通過super< >來指定要調用的方法所在的類
open class Rectangle {
open fun draw() {
println("Rectangle|draw")
}
}
interface Polygon {
fun draw() {
println("Polygon|draw")
} // 接口成員默認就是“open”的
}
class Square() : Rectangle(), Polygon {
override fun draw() {
super<Rectangle>.draw() // 調用 Rectangle.draw()
super<Polygon>.draw() // 調用 Polygon.draw()
}
}
fun main(args: Array<String>) {
Square().draw()
}
2. 屬性與字段
kotlin中的關鍵字可以用var聲明爲可變的,也可以用val聲明爲不可變的。
2.1 Getters 與 Setters
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
getter和setter是可選的。val的時候是屬於只讀屬性,是不允許setter。而property_initializer初始化器是必須實現的。
假如說只是想改變下getter或setter的可見性修飾或者註解, 你可以定義訪問器而不定義其實現:
var setterVisibility: String = "abc"
private set // 此 setter 是私有的並且有默認實現
var setterWithAnnotation: Any? = null
@Inject set // 用 Inject 註解此 setter
2.2 幕後字段
在kotlin中如果一個屬性需要一個幕後字段的時候,kotlin會自動提供。這個幕後字段可以使用field
標識符在訪問器中引用:
var counter = 0 // 注意:這個初始器直接爲幕後字段賦值
set(value) {
if (value >= 0) field = value//field自能在屬性的訪問器中使用
}
2.3 幕後屬性
如果覺得幕後字段的field不可控,那麼可以自己定義一個屬性用來充當field。
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // 類型參數已推斷出
}
return _table ?: throw AssertionError("Set to null by another thread")
}
2.4 編譯期常量
首先注意有一點val並不是kotlin的常量。比如說下面一個例子:
val currentTimeMillis: Long get() {return System.currentTimeMillis()}
這個val只能說明屬性是隻讀的,而讀出來的數據可能是變化的。kotlin提供了const 定義一個編譯期常量。他需要滿足一下要求:
-
位於頂層或者是 object 聲明 或 companion object 的一個成員
-
以 String 或原生類型值初始化
-
沒有自定義 getter
2.5 延遲初始化屬性與變量
如果在定義一個變量的時候不想馬上初始化變量或者屬性,那麼可以使用lateinit來修飾,以便稍後賦值。該修飾符只能用於類體中或者頂層屬性與局部變量。
需要注意的是,在初始化之前使用該修飾符修飾的變量或拋出一個特定異常,該異常明確標識該屬性被訪問及它沒有初始化的事實。所以個人感覺還是儘量少用。
如果檢測一個 lateinit var 是否已初始化,可以調用屬性的.isInitialized來判斷:
if (foo::bar.isInitialized) {
println(foo.bar)
}
3. 接口
kotlin的接口既可以包含方法的聲明也可以包含方法的實現。但是不可以保存屬性狀態。
3.1 接口的使用
一個類或者對象可以實現一個或多個接口:
interface MyInterface {
val name: String//屬性必須是抽象的
val name_2: String
get() = "foo"//提供訪問器的實現
fun bar()
fun foo() {
println("MyInterface::foo")
}
}
class Child : MyInterface {
override val name_2 = "get name_2"
override val name: String get() = "get name"
override fun bar() {
// 方法體
println("Child::bar")
}
}
fun main(args: Array<String>) {
Child().bar()
Child().foo()
}
3.2 解決覆蓋衝突
如果碰到多個接口的方法名是同一個,那麼可以用super<類名>來分別調用:
interface A {
fun foo() { print("A") }
}
interface B {
fun foo() { print("B") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
}
4. 可見性修飾符
類、對象、接口、構造函數、方法、屬性和它們的 setter 都可以有 可見性修飾符。在kotlin中有四中修飾符:private
、 protected
、 internal
和 public
。more的話是public。
和java不同的是多了個internal
,它是針對模塊範圍的。所謂的模塊就是對應AndroidStudio工程中的module了。
4.1 包
所謂的包,也就是對應在頂層中聲明。
-
public:意味着你的聲明將隨處可見
-
private:作用域只會在文件內
-
internal:作用域只會在相同模塊內
-
protected:對此不適用
4.2 類和接口
和4.1包的區別:一個是protect,類和接口是可以用protect修飾,就是說對類或接口的子類可見。另外就是添加了類的限制:
-
private
意味着只在這個類內部(包含其所有成員)可見; -
internal
—— 能見到類聲明的 本模塊內 的任何客戶端都可見其internal
成員; -
public
—— 能見到類聲明的任何客戶端都可見其public
成員。
5. 數據類
kotlin專門有個數據類來存放數據,着極大方便的有大量的數據對象的場景。kotlin需要在數據類前面添加data關鍵字修飾。
編譯器自動從主構造函數中聲明的所有屬性導出以下成員:
equals()
/hashCode()
對;toString()
格式是"User(name=John, age=42)"
;componentN()
函數按聲明順序對應於所有屬性;copy()
函數(見下文)。
爲了確保生成的代碼的一致性以及有意義的行爲,數據類必須滿足以下要求:
-
主構造函數需要至少有一個參數;
-
主構造函數的所有參數需要標記爲
val
或var
; -
數據類不能是抽象、開放、密封或者內部的;
如果生成類需要用到無參構造函數,那麼需要給參數指定默認值:
data class User(val name: String = "", val age: Int = 0)
5.1 在類體中聲明的屬性
對於kotlin自動識別的函數,比如說toString()、
equals()、
hashCode()以及
copy()只會用到參數中定義的變量。顯然兩個數據類對象對比的時候也只會對比主構造函數中定義的參數:
data class Person(val name: String) {
var age: Int = 0
}
fun main(args: Array<String>) {
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println(person1 == person2)
}
5.2 複製
複製的過程中也可以隨便改變其中的值
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
5.3 數據類與解構聲明
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // 輸出 "Jane, 35 years of age"
6. 密封類
密封類用sealed修飾。他自身是抽象的,並且可以有抽象成員。
密封類只能有private構造函數。
這邊重點提一下和枚舉類的卻別:每個枚舉常量只存在一個實例,而密封類的一個子類可以有可包含狀態的多個實例。
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
}
fun main(args: Array<String>) {
println(eval(Sum(Const(1.0),Const(2.3))))
}
其中Sum可以包含其他狀態的實例,這是枚舉做不到的。
7.嵌套類與內部類
首先介紹下嵌套類。調用的方式是:外部類.內部類.方法:
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
fun main() {
val demo = Outer.Nested().foo() // == 2
}
可以看到內部類的方法是可以靜態調用的。這點很奇怪,反編譯看看。
public final class Outer {
private final int bar = 1;
@Metadata(
...
)
public static final class Nested {
public final int foo() {
return 2;
}
}
}
反編譯出來的結果可以看到Nested其實是內部靜態類,難怪可以直接調用foo
方法。
其次是內部類。內部類會帶有一個對外部類的對象的引用。
最後說下匿名內部類,匿名內部類其實其實是用對象表達式來創建的。下面將會介紹。
8. 枚舉類
8.1 初始化
每一個枚舉是枚舉類的實例,所以他們可以通過方法來初始化:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
8.2 匿名類
枚舉常量還可以聲明其帶有相應方法以及覆蓋了基類方法的匿名類。
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
8.3 在枚舉類中實現接口
枚舉類實現接口,只要將接口實現放在枚舉聲明後即可。可以爲接口提供統一實現,也可以在匿名類中分別做實現。
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};
override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
如果使用枚舉常量,kotlin
提供了 enumValues<T>()
與 enumValueOf<T>()
函數以泛型的方式訪問枚舉類中的常量 :
9. 對象表達式與對象聲明
如果想要對某個類對象做輕微的改動,那麼可以用對象表達式。
對象表達式可以有多個超類,他們用逗號隔開。如果是某個類對象,那麼構造函數需要傳進適當的參數:
open class A(x: Int) {
public open val y: Int = x
}
interface B { /*……*/ }
val ab: A = object : A(1), B {
override val y = 15
}
如果沒有超類的話,如果只是想定義一個對象而已。那麼可以:
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}
如果作爲返回值的時候,可分爲兩種情況:
class C {
// 私有函數,所以其返回類型是匿名對象類型
private fun foo() = object {
val x: String = "x"
}
// 公有函數,所以其返回類型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 沒問題
val x2 = publicFoo().x // 錯誤:未能解析的引用“x”
}
}
對象表達式中的代碼可以訪問來自包含它的作用域的變量。
9.1 對象聲明
對象聲明即在 object 關鍵字後跟一個名稱,就像變量聲明一樣。他是線程安全的並且在首次訪問時進行。所以他經常用來作爲單例模式來使用,這些對象同時也是可以有超類。
9.2 伴生對象
伴生對象使用companion關鍵字來標記,伴生對象的名字也是可以省略的。如下所示:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
val instance = MyClass.create()//伴生對象有名稱的情況
class MyClass {
companion object { }
}
val x = MyClass.Companion//伴生對象沒有名稱的情況
顧名思義,其自身所用的類的名稱(不是另一個名稱的限定符)可用作對該類的伴生對象 (無論是否具名)的引用:
class MyClass1 {
companion object Named { }
}
val x = MyClass1
class MyClass2 {
companion object { }
}
val y = MyClass2
伴生對象 還可以實現接口。
9.3 對象表達式和對象聲明之間的語義差異
對象表達式和對象聲明之間有一個重要的語義差別:
- 對象表達式是在使用他們的地方立即執行(及初始化)的;
- 對象聲明是在第一次被訪問到時延遲初始化的;
- 伴生對象的初始化是在相應的類被加載(解析)時,也可以理解爲和類綁定在一起。
10. 類型別名
如果你覺得集合類型比較冗長、如果你覺得函數寫起來不方便、如果你覺得內部類寫起來太複雜。那麼在kotlin
中你可以給這些複雜的結構起個別名。
typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate<T> = (T) -> Boolean
class A {
inner class Inner
}
class B {
inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner
typealias Predicate<T> = (T) -> Boolean
fun foo(p: Predicate<Int>) = p(42)
11. 委託
所謂的委託就是某一個對象委託給另一個類中,那麼該類對象就有了被委託對象的功能。
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()//Derived擁有了被委託對象BaseImpl的接口功能
}
如果委託對象實現了和被委託對象同樣的函數,那麼優先使用委託對象的實現:
interface Base {
fun printMessage()
}
class BaseImpl(val x: Int) : Base {
override fun printMessage() { print(x) }
}
class Derived(b: Base) : Base by b {
override fun printMessage() { print("abc") }
}
fun main() {
val b = BaseImpl(10)
Derived(b).printMessage()//會打印出"abc",也就是說最終調用的是委託對象Derived的實現
}
如果委託對象實現了和被委託對象同樣的成員,那麼被委託對象只會調用自己定義的成員。
12. 委託屬性
委託就相當於代理。不同的代理處理不同的場景,有以下三種場景:
-
延遲屬性(lazy properties): 其值只在首次訪問時計算;
-
可觀察屬性(observable properties): 監聽器會收到有關此屬性變更的通知;
-
把多個屬性儲存在一個映射(map)中,而不是每個存在單獨的字段中。
委託的語法是: val/var <屬性名>: <類型> by <表達式>
。在 by 後面的表達式是該 委託, 因爲屬性對應的 get()
(與 set()
)會被委託給它的 getValue()
與 setValue()
方法。 屬性的委託不必實現任何的接口,但是需要提供一個 getValue()
函數(與 setValue()
——對於 var 屬性)。 例如:
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
val e = Example()
println(e.p)
12.1 延遲屬性Lazy
lazy的話,就是第一次是會執行 lambda 表達式並記錄結果.接下來的話是直接使用結果:
val lazyValue: String by lazy() {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)//執行打印computed
println(lazyValue)//沒有執行打印computed
}
如果初始化委託的同步鎖不是必需的,這樣多個線程可以同時執行,那麼將 LazyThreadSafetyMode.PUBLICATION
作爲參數傳遞給 lazy()
函數。 而如果你確定初始化將總是發生在與屬性使用位於相同的線程, 那麼可以使用 LazyThreadSafetyMode.NONE
模式:它不會有任何線程安全的保證以及相關的開銷。
12.2 可觀察屬性 Observable
這個也就是說屬性的改變會被實時回調執行,這個在監控數據變化的時候極其有用:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "first"//執行<no name> -> first
user.name = "second"//first -> second
}
12.3 把屬性儲存在映射中
先寫下傳統的Map用法:
val map = mapOf(
"name" to "John Doe",
"age" to 25
)
val name = map.get("name")
val age = map.get("age")
這樣子獲取name或者age會不會覺得要定義多次字段很麻煩,kotlin
提供了Map委託:
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
//再也不用定義name和name了,是不是感覺很方便
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
1.1開始可以將局部變量聲明爲委託屬性.
12.4 屬性委託要求
對於一個只讀屬性(即 val 聲明的),委託必須提供一個操作符函數 getValue()
,該函數具有以下參數:
thisRef
—— 必須與 屬性所有者 類型(對於擴展屬性——指被擴展的類型)相同或者是其超類型。property
—— 必須是類型KProperty<*>
或其超類型。
getValue()
必須返回與屬性相同的類型(或其子類型)。
舉個例子:
fun main(args: Array<String>) {
println(Owner().valResource)
}
open class SuperOwner{
}
class Owner():SuperOwner(){
val valResource: Int by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: SuperOwner, property: KProperty<*>): Int {//這邊的SuperOwner換成Owner同樣是可以的.
return 1
}
}
對於一個可變屬性(即 var 聲明的),委託必須額外提供一個操作符函數 setValue()
, 該函數具有以下參數:
-
thisRef
—— 必須與 屬性所有者 類型(對於擴展屬性——指被擴展的類型)相同或者是其超類型。 -
property
—— 必須是類型KProperty<*>
或其超類型。 -
value
— 必須與屬性類型相同(或者是其超類型)。
12.5 提供委託
如果想擴展屬性委託的話是這樣子做的:
// 檢測屬性名稱而不使用“provideDelegate”功能
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}
fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// 創建委託
}
注意到如果想做校驗checkProperty
的話,那麼就必須額外傳入字符串.那麼有沒有什麼方法可以攔截屬性呢?
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// 創建委託
return ResourceDelegate()
}
private fun checkProperty(thisRef: MyUI, name: String) { …… }
}
相對的他對變量屬性 KProperty<*>
進行了自動獲取,而不用手動傳入.方便很多