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阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容