Kotlin極簡教程:第7章 面向對象編程(下)

原文鏈接:https://github.com/EasyKotlin

7.9 單例模式(Singleton)與伴生對象(companion object)

7.9.1 單例模式(Singleton)

單例模式很常用。它是一種常用的軟件設計模式。例如,Spring中的Bean默認就是單例。通過單例模式可以保證系統中一個類只有一個實例。即一個類只有一個對象實例。

我們用Java實現一個簡單的單例類的代碼如下:

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

測試代碼:

Singleton singleton1 = Singleton.getInstance();

可以看出,我們先在單例類中聲明了一個私有靜態的Singleton instance變量,然后聲明一個私有構造函數private Singleton() {}, 這個私有構造函數使得外部無法直接通過new的方式來構建對象:

Singleton singleton2 = new Singleton(); //error, cannot private access

最后提供一個public的獲取當前類的唯一實例的靜態方法getInstance()。我們這里給出的是一個簡單的單例類,是線程不安全的。

7.9.2 object對象

Kotlin中沒有 靜態屬性和方法,但是也提供了實現類似于單例的功能,我們可以使用關鍵字 object 聲明一個object對象:

object AdminUser {
    val username: String = "admin"
    val password: String = "admin"
    fun getTimestamp() = SimpleDateFormat("yyyyMMddHHmmss").format(Date())
    fun md5Password() = EncoderByMd5(password + getTimestamp())
}

測試代碼:

val adminUser = AdminUser.username
val adminPassword = AdminUser.md5Password()
println(adminUser)  // admin
println(adminPassword)  // g+0yLfaPVYxUf6TMIdXFXw==,這個值具體運行時會變化

為了方便在REPL中演示說明,我們再寫一個示例代碼:

>>> object User {
...     val username: String = "admin"
...     val password: String = "admin"
... }

object對象只能通過對象名字來訪問:

>>> User.username
admin
>>> User.password
admin

不能像下面這樣使用構造函數:

>>> val u = User()
error: expression 'User' of type 'Line130.User' cannot be invoked as a function. The function 'invoke()' is not found
val u = User()
        ^

為了更加直觀的了解object對象的概念,我們把上面的object User的代碼反編譯成Java代碼:

public final class User {
   @NotNull
   private static final String username = "admin";
   @NotNull
   private static final String password = "admin";
   public static final User INSTANCE;

   @NotNull
   public final String getUsername() {
      return username;
   }

   @NotNull
   public final String getPassword() {
      return password;
   }

   private User() {
      INSTANCE = (User)this;
      username = "admin";
      password = "admin";
   }

   static {
      new User();
   }
}

從上面的反編譯代碼,我們可以直觀了解Kotlin的object背后的一些原理。

7.9.3 嵌套(Nested)object對象

這個object對象還可以放到一個類里面:

class DataProcessor {
    fun process() {
        println("Process Data")
    }

    object FileUtils {
        val userHome = "/Users/jack/"

        fun getFileContent(file: String): String {
            var content = ""
            val f = File(file)
            f.forEachLine { content = content + it + "\n" }
            return content
        }
    }
}

測試代碼:

DataProcessor.FileUtils.userHome // /Users/jack/
DataProcessor.FileUtils.getFileContent("test.data") // 輸出文件的內容

同樣的,我們只能通過類的名稱來直接訪問object,不能使用對象實例引用。下面的寫法是錯誤的:

val dp = DataProcessor()
dp.FileUtils.userHome // error, Nested object FileUtils cannot access object via reference

我們在Java中通常會寫一些Utils類,這樣的類我們在Kotlin中就可以直接使用object對象:

object HttpUtils {
    val client = OkHttpClient()

    @Throws(Exception::class)
    fun getSync(url: String): String? {
        val request = Request.Builder()
                .url(url)
                .build()

        val response = client.newCall(request).execute()
        if (!response.isSuccessful()) throw IOException("Unexpected code " + response)

        val responseHeaders = response.headers()
        for (i in 0..responseHeaders.size() - 1) {
            println(responseHeaders.name(i) + ": " + responseHeaders.value(i))
        }
        return response.body()?.string()
    }

    @Throws(Exception::class)
    fun getAsync(url: String) {
        var result: String? = ""

        val request = Request.Builder()
                .url(url)
                .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException?) {
                e?.printStackTrace()
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                if (!response.isSuccessful()) throw IOException("Unexpected code " + response)

                val responseHeaders = response.headers()
                for (i in 0..responseHeaders.size() - 1) {
                    println(responseHeaders.name(i) + ": " + responseHeaders.value(i))
                }
                result = response.body()?.string()
                println(result)
            }
        })
    }
}

測試代碼:

val url = "http://www.baidu.com"
val html1 = HttpUtils.getSync(url) // 同步get
println("html1=${html1}") 
HttpUtils.getAsync(url) // 異步get

7.9.4 匿名object

還有,在代碼行內,有時候我們需要的僅僅是一個簡單的對象,我們這個時候就可以使用下面的匿名object的方式:

fun distance(x: Double, y: Double): Double {
    val porigin = object {
        var x = 0.0
        var y = 0.0
    }
    return Math.sqrt((x - porigin.x) * (x - porigin.x) + (y - porigin.y) * (y - porigin.y))
}

測試代碼:

distance(3.0, 4.0)

需要注意的是,匿名對象只可以用在本地和私有作用域中聲明的類型。代碼示例:

class AnonymousObjectType {
    // 私有函數,返回的是匿名object類型
    private fun privateFoo() = object {
        val x: String = "x"
    }

    // 公有函數,返回的類型是 Any
    fun publicFoo() = object {
        val x: String = "x" // 無法訪問到
    }

    fun test() {
        val x1 = privateFoo().x   // Works
        //val x2 = publicFoo().x  // ERROR: Unresolved reference 'x'
    }
}

fun main(args: Array<String>) {
    AnonymousObjectType().publicFoo().x // Unresolved reference 'x'
}

跟 Java 匿名內部類類似,object對象表達式中的代碼可以訪問來自包含它的作用域的變量(與 Java 不同的是,這不限于 final 變量):

fun countCompare() {
    var list = mutableListOf(1, 4, 3, 7, 11, 9, 10, 20)
    var countCompare = 0
    Collections.sort(list, object : Comparator<Int> {
        override fun compare(o1: Int, o2: Int): Int {
            countCompare++
            println("countCompare=$countCompare")
            println(list)
            return o1.compareTo(o2)
        }
    })
}

測試代碼:

countCompare()

countCompare=1
[1, 4, 3, 7, 11, 9, 10, 20]
...
countCompare=17
[1, 3, 4, 7, 9, 10, 11, 20]

7.9.5 伴生對象(companion object)

Kotlin中還提供了 伴生對象 ,用companion object關鍵字聲明:

class DataProcessor {
    fun process() {
        println("Process Data")
    }

    object FileUtils {
        val userHome = "/Users/jack/"

        fun getFileContent(file: String): String {
            var content = ""
            val f = File(file)
            f.forEachLine { content = content + it + "\n" }
            return content
        }
    }

    companion object StringUtils {
        fun isEmpty(s: String): Boolean {
            return s.isEmpty()
        }
    }
}

一個類只能有1個伴生對象。也就是是下面的寫法是錯誤的:

class ClassA {
    companion object Factory {
        fun create(): ClassA = ClassA()
    }

    companion object Factory2 { // error, only 1 companion object is allowed per class
        fun create(): MyClass = MyClass()
    }
}

一個類的伴生對象默認引用名是Companion:

class ClassB {
    companion object {
        fun create(): ClassB = ClassB()
        fun get() = "Hi, I am CompanyB"
    }
}

我們可以直接像在Java靜態類中使用靜態方法一樣使用一個類的伴生對象的函數,屬性(但是在運行時,它們依舊是實體的實例成員):

ClassB.Companion.index
ClassB.Companion.create()
ClassB.Companion.get()

其中, Companion可以省略不寫:

ClassB.index
ClassB.create()
ClassB.get()

當然,我們也可以指定伴生對象的名稱:

class ClassC {
    var index = 0
    fun get(index: Int): Int {
        return 0
    }

    companion object CompanyC {
        fun create(): ClassC = ClassC()
        fun get() = "Hi, I am CompanyC"
    }
}

測試代碼:

ClassC.index
ClassC.create()// com.easy.kotli.ClassC@7440e464,具體運行值會變化
ClassC.get() // Hi, I am CompanyC
ClassC.CompanyC.index
ClassC.CompanyC.create()
ClassC.CompanyC.get()

伴生對象的初始化是在相應的類被加載解析時,與 Java 靜態初始化器的語義相匹配。

即使伴生對象的成員看起來像其他語言的靜態成員,在運行時他們仍然是真實對象的實例成員。而且,還可以實現接口:

interface BeanFactory<T> {
    fun create(): T
}

class MyClass {
    companion object : BeanFactory<MyClass> {
        override fun create(): MyClass {
            println("MyClass Created!")
            return MyClass()
        }
    }
}

測試代碼:

MyClass.create()  // "MyClass Created!"
MyClass.Companion.create() // "MyClass Created!"

另外,如果想使用Java中的靜態成員和靜態方法的話,我們可以用:

@JvmField注解:生成與該屬性相同的靜態字段
@JvmStatic注解:在單例對象和伴生對象中生成對應的靜態方法

7.10 sealed 密封類

7.10.1 為什么使用密封類

就像我們為什么要用enum類型一樣,比如你有一個enum類型 MoneyUnit,定義了元、角、分這些單位。枚舉就是為了控制住你所有要的情況是正確的,而不是用硬編碼方式寫成字符串“元”,“角”,“分”。

同樣,sealed的目的類似,一個類之所以設計成sealed,就是為了限制類的繼承結構,將一個值限制在有限集中的類型中,而不能有任何其他的類型。

在某種意義上,sealed類是枚舉類的擴展:枚舉類型的值集合也是受限的,但每個枚舉常量只存在一個實例,而密封類的一個子類可以有可包含狀態的多個實例。

7.10.1 聲明密封類

要聲明一個密封類,需要在類名前面添加 sealed 修飾符。密封類的所有子類都必須與密封類在同一個文件中聲明(在 Kotlin 1.1 之前, 該規則更加嚴格:子類必須嵌套在密封類聲明的內部):

sealed class Expression

class Unit : Expression()
data class Const(val number: Double) : Expression()
data class Sum(val e1: Expression, val e2: Expression) : Expression()
data class Multiply(val e1: Expression, val e2: Expression) : Expression()
object NaN : Expression()

使用密封類的主要場景是在使用 when 表達式的時候,能夠驗證語句覆蓋了所有情況,而無需再添加一個 else 子句:

fun eval(expr: Expression): Double = when (expr) {
    is Unit -> 1.0
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    is Multiply -> eval(expr.e1) * eval(expr.e2)
    NaN -> Double.NaN
    // 不再需要 `else` 子句,因為我們已經覆蓋了所有的情況
}

測試代碼:

fun main(args: Array<String>) {
    val u = eval(Unit())
    val a = eval(Const(1.1))
    val b = eval(Sum(Const(1.0), Const(9.0)))
    val c = eval(Multiply(Const(10.0), Const(10.0)))
    println(u)
    println(a)
    println(b)
    println(c)
}

輸出:

1.0
1.1
10.0
100.0

7.11 data 數據類

7.11.1 構造函數中的 val/var

在開始講數據類之前,我們先來看一下幾種類聲明的寫法。

寫法一:

class Aook(name: String)

這樣寫,這個name變量是無法被外部訪問到的。它對應的反編譯之后的Java代碼如下:

public final class Aook {
   public Aook(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
   }
}

寫法二:
要想這個name變量被訪問到,我們可以在類體中再聲明一個變量,然后把這個構造函數中的參數賦值給它:

class Cook(name: String) {
    val name = name
}

測試代碼:

val cook = Cook("Cook")
cook.name

對應的Java實現代碼是:

public final class Cook {
   @NotNull
   private final String name;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public Cook(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
}

寫法三:

class Dook(val name: String)
class Eook(var name: String)

構造函數中帶var、val修飾的變量,Kotlin編譯器會自動為它們生成getter、setter函數。

上面的寫法對應的Java代碼就是:

public final class Dook {
   @NotNull
   private final String name;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public Dook(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
}

public final class Eook {
   @NotNull
   private String name;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public Eook(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
}

測試代碼:

val dook = Dook("Dook")
dook.name
val eook = Eook("Eook")
eook.name

下面我們來學習一下Kotlin中的數據類: data class

7.11.2 領域實體類

我們寫Java代碼的時候,會經常創建一些只保存數據的類。比如說:

  • POJO類:POJO全稱是Plain Ordinary Java Object / Pure Old Java Object,中文可以翻譯成:普通Java類,具有一部分getter/setter方法的那種類就可以稱作POJO。

  • DTO類:Data Transfer Object,數據傳輸對象類,泛指用于展示層與服務層之間的數據傳輸對象。

  • VO類:VO有兩種說法,一個是ViewObject,一個是ValueObject。

  • PO類:Persisent Object,持久對象。它們是由一組屬性和屬性的get和set方法組成。PO是在持久層所使用,用來封裝原始數據。

  • BO類:Business Object,業務對象層,表示應用程序領域內“事物”的所有實體類。

  • DO類:Domain Object,領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。

等等。

這些我們統稱為領域模型中的實體類。最簡單的實體類是POJO類,含有屬性及屬性對應的set和get方法,實體類常見的方法還有用于輸出自身數據的toString方法。

7.11.3 數據類data class的概念

在 Kotlin 中,也有對應這樣的領域實體類的概念,并在語言層面上做了支持,叫做數據類 :

data class Book(val name: String)
data class Fook(var name: String)
data class User(val name: String, val gender: String, val age: Int) {
    fun validate(): Boolean {
        return true
    }
}

這里的var/val是必須要帶上的。因為編譯器要把主構造函數中聲明的所有屬性,自動生成以下函數:

equals()/hashCode() 
toString() : 格式是 User(name=Jacky, gender=Male, age=10)
componentN() 函數 : 按聲明順序對應于所有屬性component1()、component2() ...
copy() 函數

如果我們自定義了這些函數,或者繼承父類重寫了這些函數,編譯器就不會再去生成。

測試代碼:

val book = Book("Book")
book.name
book.copy("Book2")

val jack = User("Jack", "Male", 1)
jack.name
jack.gender
jack.age
jack.toString()
jack.validate()

val olderJack = jack.copy(age = 2)
val anotherJack = jack.copy(name = "Jacky", age = 10)

在一些場景下,我們需要復制一個對象來改變它的部分屬性,而其余部分保持不變。 copy() 函數就是為此而生成。例如上面的的 User 類的copy函數的使用:

val olderJack = jack.copy(age = 2)
val anotherJack = jack.copy(name = "Jacky", age = 10)

7.11.4 數據類的限制

數據類有以下的限制要求:

1.主構造函數需要至少有一個參數。下面的寫法是錯誤的:

data class Gook // error, data class must have at least one primary constructor parameter

2.主構造函數的所有參數需要標記為 val 或 var;

data class Hook(name: String)// error, data class must have only var/val property

跟普通類一樣,數據類也可以有次級構造函數:

data class LoginUser(val name: String = "", val password: String = "") : DBase(), IBaseA, IBaseB {

    var isActive = true

    constructor(name: String, password: String, isActive: Boolean) : this(name, password) {
        this.isActive = isActive
    }
    ...
}

3.數據類不能是抽象、開放、密封或者內部的。也就是說,下面的寫法都是錯誤的:

abstract data class Iook(val name: String) // modifier abstract is incompatible with data
open data class Jook(val name: String) // modifier abstract is incompatible with data
sealed data class Kook(val name: String)// modifier sealed is incompatible with data
inner data class Look(val name: String)// modifier inner is incompatible with data

數據類只能是final的:

final data class Mook(val name: String) // modifier abstract is incompatible with data

4.在1.1之前數據類只能實現接口。自 1.1 起,數據類可以擴展其他類。代碼示例:

open class DBase
interface IBaseA
interface IBaseB

data class LoginUser(val name: String, val password: String) : DBase(), IBaseA, IBaseB {

    override fun equals(other: Any?): Boolean {
        return super.equals(other)
    }

    override fun hashCode(): Int {
        return super.hashCode()
    }

    override fun toString(): String {
        return super.toString()
    }

    fun validate(): Boolean {
        return true
    }
}

測試代碼:

val loginUser1 = LoginUser("Admin", "admin")
println(loginUser1.component1())
println(loginUser1.component2())
println(loginUser1.name)
println(loginUser1.password)
println(loginUser1.toString())

輸出:

Admin
admin
Admin
admin
com.easy.kotlin.LoginUser@7440e464

可以看出,由于我們重寫了override fun toString(): String, 對應的輸出使我們熟悉的類的輸出格式。

如果我們不重寫這個toString函數,則會默認輸出:

LoginUser(name=Admin, password=admin)

上面的類聲明的構造函數,要求我們每次必須初始化name、password的值,如果我們想擁有一個無參的構造函數,我們只要對所有的屬性指定默認值即可:

data class LoginUser(val name: String = "", val password: String = "") : DBase(), IBaseA, IBaseB {
...
}

這樣我們在創建對象的時候,就可以直接使用:

val loginUser3 = LoginUser()
loginUser3.name
loginUser3.password

7.11.5 數據類的解構

解構相當于 Component 函數的逆向映射:

val helen = User("Helen", "Female", 15)
val (name, gender, age) = helen
println("$name, $gender, $age years of age")

輸出:Helen, Female, 15 years of age

7.11.6 標準數據類PairTriple

標準庫中的二元組 Pair類就是一個數據類:

public data class Pair<out A, out B>(
        public val first: A,
        public val second: B) : Serializable {
    public override fun toString(): String = "($first, $second)"
}

Kotlin標準庫中,對Pair類還增加了轉換成List的擴展函數:

public fun <T> Pair<T, T>.toList(): List<T> = listOf(first, second)

還有三元組Triple類:

public data class Triple<out A, out B, out C>(
        public val first: A,
        public val second: B,
        public val third: C) : Serializable {
    public override fun toString(): String = "($first, $second, $third)"
}
 fun <T> Triple<T, T, T>.toList(): List<T> = listOf(first, second, third)

7.12 嵌套類(Nested Class)

7.12.1 嵌套類:類中的類

類可以嵌套在其他類中,可以嵌套多層:

class NestedClassesDemo {
    class Outer {
        private val zero: Int = 0
        val one: Int = 1

        class Nested {
            fun getTwo() = 2
            class Nested1 {
                val three = 3
                fun getFour() = 4
            }
        }
    }
}

測試代碼:

val one = NestedClassesDemo.Outer().one
val two = NestedClassesDemo.Outer.Nested().getTwo()
val three = NestedClassesDemo.Outer.Nested.Nested1().three
val four = NestedClassesDemo.Outer.Nested.Nested1().getFour()
println(one)
println(two)
println(three)
println(four)

我們可以看出,訪問嵌套類的方式是直接使用 類名., 有多少層嵌套,就用多少層類名來訪問。

普通的嵌套類,沒有持有外部類的引用,所以是無法訪問外部類的變量的:

class NestedClassesDemo {
class Outer {
        private val zero: Int = 0
        val one: Int = 1

        class Nested {
            fun getTwo() = 2

            fun accessOuter() = {
                println(zero) // error, cannot access outer class
                println(one)  // error, cannot access outer class
            }
        }
    }
}

我們在Nested類中,訪問不到Outer類中的變量zero,one。
如果想要訪問到,我們只需要在Nested類前面加上inner關鍵字修飾,表明這是一個嵌套的內部類。

7.12.2 內部類(Inner Class)

類可以標記為 inner 以便能夠訪問外部類的成員。內部類會帶有一個對外部類的對象的引用:

class NestedClassesDemo {
class Outer {
        private val zero: Int = 0
        val one: Int = 1

        inner class Inner {
            fun accessOuter() = {
                println(zero) // works
                println(one) // works
            }
        }
}

測試代碼:

val innerClass = NestedClassesDemo.Outer().Inner().accessOuter()

我們可以看到,當訪問inner class Inner的時候,我們使用的是Outer().Inner(), 這是持有了Outer的對象引用。跟普通嵌套類直接使用類名訪問的方式區分。

7.12.3 匿名內部類(Annonymous Inner Class)

匿名內部類,就是沒有名字的內部類。既然是內部類,那么它自然也是可以訪問外部類的變量的。

我們使用對象表達式創建一個匿名內部類實例:

class NestedClassesDemo {
class AnonymousInnerClassDemo {
            var isRunning = false
            fun doRun() {
                Thread(object : Runnable {
                    override fun run() {
                        isRunning = true
                        println("doRun : i am running, isRunning = $isRunning")
                    }
                }).start()
            }
    }
}

如果對象是函數式 Java 接口,即具有單個抽象方法的 Java 接口的實例,例如上面的例子中的Runnable接口:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

我們可以使用lambda表達式創建它,下面的幾種寫法都是可以的:

fun doStop() {
    var isRunning = true
    Thread({
        isRunning = false
        println("doStop: i am not running, isRunning = $isRunning")
    }).start()
}

fun doWait() {
    var isRunning = true

    val wait = Runnable {
        isRunning = false
        println("doWait: i am waiting, isRunning = $isRunning")
    }

    Thread(wait).start()
}

fun doNotify() {
    var isRunning = true

    val wait = {
        isRunning = false
        println("doNotify: i notify, isRunning = $isRunning")
    }

    Thread(wait).start()
}

測試代碼:

NestedClassesDemo.Outer.AnonymousInnerClassDemo().doRun()
NestedClassesDemo.Outer.AnonymousInnerClassDemo().doStop()
NestedClassesDemo.Outer.AnonymousInnerClassDemo().doWait()
NestedClassesDemo.Outer.AnonymousInnerClassDemo().doNotify()

輸出:

doRun : i am running, isRunning = true
doStop: i am not running, isRunning = false
doWait: i am waiting, isRunning = false
doNotify: i notify, isRunning = false

關于lambda表達式以及函數式編程,我們將在下一章中學習。

7.13 委托(Delegation)

7.13.1 代理模式(Proxy Pattern)

代理模式,也稱委托模式。

在代理模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委托給另一個對象來處理。代理模式是一項基本技巧,許多其他的模式,如狀態模式、策略模式、訪問者模式本質上是在特殊的場合采用了代理模式。

代理模式使得我們可以用聚合來替代繼承,它還使我們可以模擬mixin(混合類型)。委托模式的作用是將委托者與實際實現代碼分離出來,以達成解耦的目的。

一個代理模式的Java代碼示例:

package com.easy.kotlin;

interface JSubject {
    public void request();
}

class JRealSubject implements JSubject {
    @Override
    public void request() {
        System.out.println("JRealSubject Requesting");
    }
}

class JProxy implements JSubject {
    private JSubject subject = null;

    //通過構造函數傳遞代理者
    public JProxy(JSubject sub) {
        this.subject = sub;
    }

    @Override
    public void request() { //實現接口中定義的方法
        this.before();
        this.subject.request();
        this.after();
    }

    private void before() {
        System.out.println("JProxy Before Requesting ");
    }

    private void after() {
        System.out.println("JProxy After Requesting ");
    }
}

public class DelegateDemo {
    public static void main(String[] args) {
        JRealSubject jRealSubject = new JRealSubject();
        JProxy jProxy = new JProxy(jRealSubject);
        jProxy.request();
    }
}

輸出:

JProxy Before Requesting 
JRealSubject Requesting
JProxy After Requesting 

7.13.2 類的委托(Class Delegation)

就像支持單例模式的object對象一樣,Kotlin 在語言層面原生支持委托模式。

代碼示例:

package com.easy.kotlin

import java.util.*

interface Subject {
    fun hello()
}

class RealSubject(val name: String) : Subject {
    override fun hello() {
        val now = Date()
        println("Hello, REAL $name! Now is $now")
    }
}

class ProxySubject(val sb: Subject) : Subject by sb {
    override fun hello() {
        println("Before ! Now is ${Date()}")
        sb.hello()
        println("After ! Now is ${Date()}")
    }
}

fun main(args: Array<String>) {
    val subject = RealSubject("World")
    subject.hello()
    println("-------------------------")
    val proxySubject = ProxySubject(subject)
    proxySubject.hello()
}

在這個例子中,委托代理類 ProxySubject 繼承接口 Subject,并將其所有共有的方法委托給一個指定的對象sb :

class ProxySubject(val sb: Subject) : Subject by sb 

ProxySubject 的超類型Subject中的 by sb 表示 sb 將會在 ProxySubject 中內部存儲。

另外,我們在覆蓋重寫了函數override fun hello()

測試代碼:

fun main(args: Array<String>) {
    val subject = RealSubject("World")
    subject.hello()
    println("-------------------------")
    val proxySubject = ProxySubject(subject)
    proxySubject.hello()
}

輸出:

Hello, REAL World! Now is Wed Jul 05 02:45:42 CST 2017
-------------------------
Before ! Now is Wed Jul 05 02:45:42 CST 2017
Hello, REAL World! Now is Wed Jul 05 02:45:42 CST 2017
After ! Now is Wed Jul 05 02:45:42 CST 2017

7.13.3 委托屬性 (Delegated Properties)

通常對于屬性類型,我們是在每次需要的時候手動聲明它們:

class NormalPropertiesDemo {
    var content: String = "NormalProperties init content"
}

那么這個content屬性將會很“呆板”。屬性委托賦予了屬性富有變化的活力。

例如:

  • 延遲屬性(lazy properties): 其值只在首次訪問時計算
  • 可觀察屬性(observable properties): 監聽器會收到有關此屬性變更的通知
  • 把多個屬性儲存在一個映射(map)中,而不是每個存在單獨的字段中。

委托屬性

Kotlin 支持 委托屬性:

class DelegatePropertiesDemo {
    var content: String by Content()

    override fun toString(): String {
        return "DelegatePropertiesDemo Class"
    }
}

class Content {
    operator fun getValue(delegatePropertiesDemo: DelegatePropertiesDemo, property: KProperty<*>): String {
        return "${delegatePropertiesDemo} property '${property.name}' = 'Balalala ... ' "
    }

    operator fun setValue(delegatePropertiesDemo: DelegatePropertiesDemo, property: KProperty<*>, value: String) {
        println("${delegatePropertiesDemo} property '${property.name}' is setting value: '$value'")
    }
}

var content: String by Content()中, by 后面的表達式的Content()就是該屬性委托的對象。content屬性對應的 get()(和 set())會被委托給Content()operator fun getValue()operator fun setValue() 函數,這兩個函數是必須的,而且得是操作符函數。

測試代碼:

val n = NormalPropertiesDemo()
println(n.content)
n.content = "Lao tze"
println(n.content)

val e = DelegatePropertiesDemo()
println(e.content) // call Content.getValue
e.content = "Confucius" // call Content.setValue
println(e.content) // call Content.getValue

輸出:

NormalProperties init content
Lao tze
DelegatePropertiesDemo Class property 'content' = 'Balalala ... ' 
DelegatePropertiesDemo Class property 'content' is setting value: 'Confucius'
DelegatePropertiesDemo Class property 'content' = 'Balalala ... 

懶加載屬性委托 lazy

lazy() 函數定義如下:

@kotlin.jvm.JvmVersion
public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

它接受一個 lambda 并返回一個 Lazy <T> 實例的函數,返回的實例可以作為實現懶加載屬性的委托:

第一次調用 get() 會執行已傳遞給 lazy() 的 lamda 表達式并記錄下結果, 后續調用 get() 只是返回之前記錄的結果。

代碼示例:

val synchronizedLazyImpl = lazy({
    println("lazyValueSynchronized1  3!")
    println("lazyValueSynchronized1  2!")
    println("lazyValueSynchronized1  1!")
    "Hello, lazyValueSynchronized1 ! "
})

val lazyValueSynchronized1: String by synchronizedLazyImpl
println(lazyValueSynchronized1)
println(lazyValueSynchronized1)

val lazyValueSynchronized2: String by lazy {
    println("lazyValueSynchronized2  3!")
    println("lazyValueSynchronized2  2!")
    println("lazyValueSynchronized2  1!")
    "Hello, lazyValueSynchronized2 ! "
}

println(lazyValueSynchronized2)
println(lazyValueSynchronized2)

輸出:

lazyValueSynchronized1  3!
lazyValueSynchronized1  2!
lazyValueSynchronized1  1!
Hello, lazyValueSynchronized1 ! 
Hello, lazyValueSynchronized1 ! 

lazyValueSynchronized2  3!
lazyValueSynchronized2  2!
lazyValueSynchronized2  1!
Hello, lazyValueSynchronized2 ! 
Hello, lazyValueSynchronized2 ! 

默認情況下,對于 lazy 屬性的求值是同步的(synchronized), 下面兩種寫法是等價的:

val synchronizedLazyImpl = lazy({
    println("lazyValueSynchronized1  3!")
    println("lazyValueSynchronized1  2!")
    println("lazyValueSynchronized1  1!")
    "Hello, lazyValueSynchronized1 ! "
})

val synchronizedLazyImpl2 = lazy(LazyThreadSafetyMode.SYNCHRONIZED, {
    println("lazyValueSynchronized1  3!")
    println("lazyValueSynchronized1  2!")
    println("lazyValueSynchronized1  1!")
    "Hello, lazyValueSynchronized1 ! "
})

該值是線程安全的。所有線程會看到相同的值。

如果初始化委托多個線程可以同時執行,不需要同步鎖,使用LazyThreadSafetyMode.PUBLICATION

val lazyValuePublication: String by lazy(LazyThreadSafetyMode.PUBLICATION, {
    println("lazyValuePublication 3!")
    println("lazyValuePublication 2!")
    println("lazyValuePublication 1!")
    "Hello, lazyValuePublication ! "
})

而如果屬性的初始化是單線程的,那么我們使用 LazyThreadSafetyMode.NONE 模式(性能最高):

val lazyValueNone: String by lazy(LazyThreadSafetyMode.NONE, {
    println("lazyValueNone 3!")
    println("lazyValueNone 2!")
    println("lazyValueNone 1!")
    "Hello, lazyValueNone ! "
})

Delegates.observable 可觀察屬性委托

我們把屬性委托給Delegates.observable函數,當屬性值被重新賦值的時候, 觸發其中的回調函數 onChange。

該函數定義如下:

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
        ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
            override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
        }

代碼示例:

class PostHierarchy {
    var level: String by Delegates.observable("P0",
            { property: KProperty<*>,
              oldValue: String,
              newValue: String ->
                println("$oldValue -> $newValue")
            })
}

測試代碼:

val ph = PostHierarchy()
ph.level = "P1"
ph.level = "P2"
ph.level = "P3"
println(ph.level) // P3

輸出:

P0 -> P1
P1 -> P2
P2 -> P3
P3

我們可以看出,屬性level每次賦值,都回調了Delegates.observable中的lambda表達式所寫的onChange函數。

Delegates.vetoable 可否決屬性委托

這個函數定義如下:

public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
        ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
            override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
        }

當我們把屬性委托給這個函數時,我們可以通過onChange函數的返回值是否為true, 來選擇屬性的值是否需要改變。

代碼示例:

class PostHierarchy {
    var grade: String by Delegates.vetoable("T0", {
        property, oldValue, newValue ->
        true
    })

    var notChangeGrade: String by Delegates.vetoable("T0", {
        property, oldValue, newValue ->
        false
    })
}

測試代碼:

ph.grade = "T1"
ph.grade = "T2"
ph.grade = "T3"
println(ph.grade) // T3

ph.notChangeGrade = "T1"
ph.notChangeGrade = "T2"
ph.notChangeGrade = "T3"
println(ph.notChangeGrade) // T0

我們可以看出,當onChange函數返回值是false的時候,對屬性notChangeGrade的賦值都沒有生效,依然是原來的默認值T0 。

Delegates.notNull 非空屬性委托

我們也可以使用委托來實現屬性的非空限制:

var name: String by Delegates.notNull()

這樣name屬性就被限制為不能為null,如果被賦值null,編譯器直接報錯:

ph.name = null // error 
Null can not be a value of a non-null type String

屬性委托給Map映射

我們也可以把屬性委托給Map:

class Account(val map: Map<String, Any?>) {
    val name: String by map
    val password: String by map
}

測試代碼:

val account = Account(mapOf(
            "name" to "admin",
            "password" to "admin"
    ))

println("Account(name=${account.name}, password = ${account.password})")

輸出:

Account(name=admin, password = admin)

如果是可變屬性,這里也可以把只讀的 Map 換成 MutableMap :

class MutableAccount(val map: MutableMap<String, Any?>) {
    var name: String by map
    var password: String by map
}

測試代碼:

val maccount = MutableAccount(mutableMapOf(
            "name" to "admin",
            "password" to "admin"
))

maccount.password = "root"
println("MutableAccount(name=${maccount.name}, password = ${maccount.password})")

輸出:

MutableAccount(name=admin, password = root)

本章小結

本章我們介紹了Kotlin面向對象編程的特性: 類與構造函數、抽象類與接口、繼承以及多重繼承等基礎知識,同時介紹了Kotlin中的注解類、枚舉類、數據類、密封類、嵌套類、內部類、匿名內部類等特性類。最后我們學習了Kotlin中對單例模式、委托模式的語言層面上的內置支持:object對象、委托。

總的來說,Kotlin相比于Java的面向對象編程,增加不少有趣的功能與特性支持,這使得我們代碼寫起來更加方便快捷了。

我們知道,在Java 8 中,引進了對函數式編程的支持:Lambda表達式、Function接口、stream API等,而在Kotlin中,對函數式編程的支持更加全面豐富,代碼寫起來也更加簡潔優雅。下一章中,我們來一起學習Kotlin的函數式編程。

本章示例代碼工程:https://github.com/EasyKotlin/chatper7_oop

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容