kotlin面向对象二

扩展

扩展方法

//定义一个函数,函数名为"被扩展类.方法"
fun Float.info() {
    Log.d("Float", "info: ===扩展的 info 方法===")
}

//调用
var f = 0.4f
f.info()

程序为 Float 类扩展了 info() 方法之后,就像为 Float 类增加了 info() 方法一样,所有的 Float 对象都可调用 info() 方法,如果被扩展的类有子类,子类的实例对象也可以调用该方法

对集合元素进行随机排列的扩展方法:

//List<T>的随机排序方法
//生成 List 集合所有索引的随机排列、根据随机排列的索引去 List 集合中取元素
fun <T> List<T>.shuffle(): List<T> {
    //返回的List
    var resultList = mutableListOf<T>()
    //用于存放随机生成的索引
    var indexArray = Array(size) { 0 }

    var random = Random()
    var i = 0
    outer@ while (i < size) {
        //生成随机索引
        var randomInt = random.nextInt(size)
        //如果随机生成的这个索引之前已经生成过了,继续随机生成
        for (j in 0 until i) {
            if (randomInt == indexArray[j]) {
                continue@outer
            }
        }
        //保存生成的随机索引,以便下次生成随机数时判断是否生成重复的随机数了
        indexArray[i] = randomInt
        resultList.add(get(randomInt))
        i++
    }
    return resultList
}

//调用
var list = listOf("linux", "java", "kotlin", "web")
list = list.shuffle()
Log.d(TAG, "onCreate: list=$list")

扩展的实现机制

Kotlin 的扩展并没有真正地修改所扩展的类,被扩展的类还是原来的类,没有任何改变。 Kotlin 扩展的本质就是定义了一个函数,当程序用对象调用扩展方法时, Kotlin 在编译时会执行静态解析一一就是根据调用对象、方法名找到扩展函数,转换为函数调用

list.shuffle() 编译时步骤:

  1. 检查 list 的类型,发现其类型是 List<String>
  2. 检查 List<String> 类本身是否定义了 shuffle() 方法,如果该类本身包含该方法,则 Kotlin无须进行处理,直接编译即可
  3. 如果 List<String> 类本身不包含 shuffle() 方法,则 Kotlin 会查找程序是否为 List<String> 扩展了 shuffle() 方法,如果找到该函数,则 Kotlin 编译器会执行静态解析,它会将上面代码替换成执行 List<String>. shuffle() 函数
  4. 如果 List<String>不包含 shuffle() 方法,也找不到名为 List<String>.shuffle()的函数(或泛型函数)定义,编译器将报错

这意味着调用扩展方法是由其所在表达式的编译时类型决定的,而不是由它所在表达式的运行时类型决定的,成员方法的优先级高于扩展方法,如果一个类包含了具有相同签名的成员方法和扩展方法,总是会执行成员方法,而不会执行扩展方法

val baseClass: BaseClass = SubClass()
baseClass.test()

如上面的代码:

如果 test() 是成员方法,那么 test() 方法表现出运行时类型的方法行为

如果 test() 是扩展方法,那么 test() 方法总是表现出编译时类型的方法行为

在java里,kotlin的扩展方法会被编译成静态方法,调用时以静态方法的方式调用:

public class Test {
    public void test() {
        List<String> list = new ArrayList();
        list.add("linux");
        list.add("java");
        list.add("kotlin");
        list.add("web");
        // 文件名.扩展方法名(调用对象)
        // 扩展方法写在MainActivity.kt文件里,因此文件名应该是:MainActivityKt
        // kotlin中调用的对象作为参数传入
        List<String> shuffle = MainActivityKt.shuffle(list);
    }
}

为可空类型扩展方法

Kotlin 还允许为可空类型(带“?”后缀的类型)扩展方法,由于会导致 null 值调用该扩展方法,因此程序需要在扩展方法中处理 null 值的情形

//为可空类型扩展 equals 方法
fun Any?.equals(other: Any?): Boolean {
    if (this == null) {
        return other == null
    }
    return this == other
}

扩展属性

Kotlin 也允许扩展属性,但由于 Kotlin 扩展并不能真正修改目标类,因此 Kotlin 扩展的属性其实是通过添加 getter 、 setter 方法实现的,没有幕后字段。简单来说,扩展的属性只能是计算属性

  1. 扩展属性不能有初始值(没有存储属性值的幕后字段)
  2. 不能用 field 关键字显式访问幕后字段
  3. 扩展只读属性必须提供 getter 方法,扩展读写属性必须提供 getter 、setter 方法
class User(var first: String, var last: String) {}

var User.fullName: String
    get() = "$first.$last"
    set(value) {
        //value 字符串中不包含.或包含几个.都不行
        if ("." !in value || value.indexOf(".") != value.lastIndexOf(".")) {
            Log.d("TAG", ": 您输入的 fullName 不合法")
        } else {
            var tokens = value.split(".")
            first = tokens[0]
            last = tokens[1]
        }
    }

//使用
var user = User("悟空", "孙")
Log.d(TAG, "onCreate: ${user.fullName}")
user.fullName = "八戒.猪"

也可用泛型函数的形式来定义扩展属性,Kotlin 为 List 扩展的 lastlndex 属性的代码如下:

public val <T> List<T>.lastIndex: Int
    get() = this.size - 1

以成员方式定义扩展

前面的扩展,都是以顶层函数的形式(放在包空间下)进行定义的,因此这些扩展都可直接使用(如果扩展位于不同的包中,当然也需要导包)。 Kotlin 还支持以类成员的方式定义扩展一一就像为类定义方法、属性那样定义扩展

对于以类成员方式定义的扩展,在扩展方法(属性)中可直接调用被扩展类的成员,在扩展方法(属性)中也可直接调用它所在类的成员

class A {
    fun testA() {
        Log.d("TAG", "testA: A 的 testA 方法")
    }
}

class B {
    fun testB() {
        Log.d("TAG", "testB: B 的 testB 方法")
    }

    //以成员方式为 A 扩展 foo() 方法
    fun A.foo() {
        //在该方法内既可调用类 A 的成员,也可调用类 B 的成员
        testA() // 对象为隐式调用者
        testB() // 对象为隐式调用者
    }

    fun test(target: A) {
        //调用 A 对象的成员方法
        target.testA()
        //调用 A 对象的扩展方法
        target.foo()
    }
}

如果被扩展类和扩展定义所在的类包含了同名的方法,优先调用被扩展类的方法,为了让系统调用扩展定义所在类的方法,必须使用带标签的 this 进行限定

class A {
    fun test() {
        Log.d("TAG", "testA: A 的 test 方法")
    }
}

class B {
    fun test() {
        Log.d("TAG", "testB: B 的 test 方法")
    }

    //以成员方式为 A 扩展 foo() 方法
    fun A.foo() {
        //在该方法内既可调用类 A 的成员,也可调用类 B 的成员
        test() // 对象为隐式调用者
        this@B.test() // 使用带标签的 this 指定调用 B 的 test() 方法
    }
}

带接收者的匿名函数

Kotlin 还支持为类扩展匿名函数,去掉被扩展类的类名和点(.)之后的函数名即可

在这种情况下,该扩展函数所属的类也是该函数的接收者。因此,这种匿名函数也被称为“带接收者的匿名函数”

//定义一个带接收者的匿名函数
val factorial = fun Int.(): Int {
    //该匿名函数的接收者是 Int 对象
    //因此在该匿名函数中, this 代表调用该匿名函数的 Int 对象
    if (this < 0) {
        return -1
    } else if (this == 1) {
        return 1
    } else {
        var result = 1
        for (i in 1..this) {
            result *= 1
        }
        return result
    }
}

//调用
Log.d(TAG, "onCreate: ${8.factorial()}")

由于上面程序最后将带接收者的匿名函数赋值给了 factorial 变量,因此可通过 Int 对象来调用 factoria() 函数,带接收者的匿名函数也有自身的类型,就是在普通函数类型的前面添加了一个接收者类型进行限定。factorial 的类型为:

Int.()->Int

如果接收者类型可通过上下文推断出来,那么 Kotlin 允许使用 Lambda 表达式作为带接收者的匿名函数

class HTML {
    fun body() {
        println("<body> </body>")
    }

    fun head() {
        println("<head> </head>")
    }
}

//定义一个类型为 HTML.() -> Unit 的形参(带接收者的匿名函数)
//这样在函数中 HTML 对象就增加了 init 方法
fun html(init: HTML.() -> Unit) {
    println("<html>")
    val html = HTML() // 创建接收者对象
    html.init() //使用接收者调用 init 引用匿名函数(即传入的参数)
    println("</html>")
}

//调用
//调用 html 函数,需要传入 HTML.() -> Unit 类型的参数
//此时系统可推断出接收者的类型,故可用 Lambda 表达式代替匿名函数
html { // Lambda 表达式中的 this 就是该方法的调用者
    head()
    body()
}

扩展的作用主要有如下两个方面:

  1. 动态地为已有的类添加方法或属性
  2. 以更好的形式组织一些工具方法

final 和 open 修饰符

final 关键字可用于修饰类、属性和方法,表示它修饰的类、属性和方法不可改变,open 修饰符与 final 修饰符是反义词

final 和 open 不能修饰局部变量

可执行“宏替换”的常量

Java使用 final 修饰“宏变量”,该“宏变量”在编译阶段就会被替换掉,Kotlin 提供了 const 用来修饰可执行“宏替换”的常量,这种常量也被称为“编译时”常量,因为它在编译阶段就会被替换掉

“宏替换”的常量除使用 const 修饰之外,还必须满足如下条件:

  1. 位于顶层或者是对象表达式的成员
  2. 初始值为基本类型值或字符串字面值
  3. 没有自定义的 getter 方法
//定义支持“宏替换”的常量
const val MAX_AGE = 100

如果被赋值的表达式只是基本的算术表达式或进行字符串连接运算,没有访问普通变量、常量,调用方法,那么 Kotlin 编译器同样会将这种 const 常量当成“宏变量”处理

//下面定义了 4 个“宏变量”
const val a = 5 + 2
const val b: Double = 1.2 / 3
const val str: String = "kotlin" + "java"
const val book: String = "kotlin" + 99.0

final 属性

如果一个属性、方法、类没有使用 open 修饰,Kotlin 会自动为其添加 final 修饰,添加后:

  1. 子类就不可以重写该属性
  2. 方法不可被重写,但可以被重载
  3. 不可以有子类

不可变类

不可变( immutable )类的意思是创建该类的实例后,该实例的属性值是不可改变的

如果需要创建自定义的不可变类,可遵守如下规则

  1. 提供带参数的构造器,用于根据传入的参数来初始化类中的属性
  2. 定义使用 final 修饰的只读属性,避免程序通过 setter 方法改变该属性值
  3. 重写 hashCode() 和 equals() 方法。equals() 方法将关键属性作为两个对象是否相等的标准,除此之外,还应该保证两个用 equals() 方法判断为相等的对象的 hashCode() 也相等
//定义可初始化两个属性的构造器
class Address(val detail: String, val postCode: String) {
    //重写 equals() 方法,判断两个对象是否相等
    override operator fun equals(other: Any?): Boolean {
        if (this == other) {
            return true
        }
        if (other == null) {
            return false
        }
        if (other.javaClass == Address::class) {
            var ad = other as Address
            //当 detail 和 postCode 相等时,可认为两个 Address 对象相等
            return this.detail.equals(ad.detail)
                    && this.postCode.equals(ad.postCode)
        }
        return false
    }

    override fun hashCode(): Int {
        return detail.hashCode() + postCode.hashCode() * 31
    }
}

与不可变类对应的是可变类,只要我们定义了任何读写属性,该类就是可变类

当创建不可变类时,如果它包含的成员属性的类型是可变的,那么其对象的属性值依然是可改变的,这个不可变类其实是失败

class Name(var firstName: String = "", var lastName: String = "") {}
class Person(val name: Name) {}

//使用
val n = Name("悟空", "孙")
var p = Person(n)
// Person 对象的 name 的 firstName 值为 悟空
Log.d(TAG, "onCreate: ${p.name.firstName}")
//改变 Person 对象的 name 的 firstName
n.firstName = "八戒"

为了保持 Person 对象的不可变性,要程序无法访问到 Person 对象的 name 属性的幕后变量

class Person {
    val name: Name
        get() {
            //返回一个新的对象,该对象的 firstName 和 lastName
            //与该 Person 对象里的幕后字段的 firstName 和 lastName 相同
            return Name(field.firstName,field.lastName)
        }

    constructor(name: Name){
        //设置 name 属性值为新创建的 Name 对象,该对象的 firstName 和 lastName
        //与传入的 name 参数的 firstName 和 lastName相同
        this.name = Name(name.firstName, name . lastName)
    }
}

当程序向 Person 构造器里传入一个Name 对象时,该构造器创建 Person对象时并不是直接利用己有的 Name 对象(利用己有的 Name 对象有风险,因为这个己有的Name 对象是可变的,如果程序改变了这个 Name 对象,将会导致 Person 对象也发生变化),而是重新创建了一个 Name 对象来赋给 Person 对象的 name 属性

当Person 对象返回 name性时,它并没有直接返回 name 属性的幕后字段,因为直接返回 name 属性的幕后字也可能导致它所引用的 Name 对象被修改

抽象类

有 abstract 修饰的成员,无须使用 open 修饰,当使用 abstract 修饰类时,表明这个类需要被继承,当使用 abstract 修饰方法、属性时,表明这个方法、属性必须由子类提供实现(即重写), final 和 abstract 永远不能同时使用

抽象成员和抽象类

包含抽象成员的类只能被定义成抽象类,抽象类中可以没有抽象成员

规则:

  1. 抽象类和抽象成员必须使用 abstract 修饰,抽象方法不能有方法体
  2. 抽象类不能被实例化,无法调用抽象类的构造器创建抽象类的实例
  3. 抽象类可以包含属性、方法(普通方法和抽象方法都可以)、构造器、初始化块、嵌套类(接口、枚举) 5种成员,抽象类的构造器不能用于创建实例,主要用于被其子类调用
  4. 含有抽象成员的类(包括直接定义了一个抽象成员;或继承了一个抽象父类,但没有完全实现父类包含的抽象成员;或实现了一个接口,但没有完全实现接口包含的抽象成员)只能被定义成抽象类

定义抽象方法,只需在普通方法上增加 abstract 修饰符,并把普通方法的方法体(也就是方法后用花括号括起来的部分)全部去掉即可

定义抽象类,只需在普通类上增加 abstract 修饰符即可

abstract class Shape {
    init {
        Log.d("Shape", ": 执行 Shape 的初始化块")
    }

    var color = ""

    //定义一个计算周长的抽象方法
    abstract fun calPerimeter(): Double

    //定义一个代表形状的抽象的只读属性,抽象属性不需要初始值
    abstract val type: String

    //定义 Shape 的构造器,该构造器并不是用于创建 Shape 对象的,而是用于被子类调用
    constructor() {}
    constructor(color: String) {
        Log.d("Shape", ": 执行 Shape 的构造器")
        this.color = color
    }
}

class Triangle(color: String, var a: Double, var b: Double, var c: Double) : Shape(color) {
    //重写 Shape 类的代表形状的抽象属性
    override val type: String = "三角形"

    //重写 Shape 类的计算周长的抽象方法
    override fun calPerimeter(): Double {
        return a + b + c
    }
}

abstract 不能用于修饰局部交量,也不能用于修饰构造器,private 和 abstract 能同时修饰方法

与 Java 类似的是, Kotlin 也允许使用抽象成员重写非抽象成员

open class Base {
    open fun foo() {}
}

abstract class Sub : Base() {
    abstract override fun foo()
}

抽象类的作用

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式

再介绍一个模板模式的范例,在这个范例的抽象父类中,父类的普通方法依赖于一个抽象方法,而抽象方法则推迟到子类中提供实现

//定义车速表,带转速属性的主构造器
abstract class SpeedMeter(var turnRate: Double) {
    //把返回车轮周长的方法定义成抽象方法
    abstract fun calGirth(): Double

    //定义计算速度的通用算法
    fun getSpeed(): Double {
        //速度等于车轮周长 * 转速
        return calGirth() * turnRate
    }
}

class CarSpeedMeter(var radius: Double, turnRate: Double) : SpeedMeter(turnRate) {
    override fun calGirth(): Double {
        return radius * 2 * Math.PI
    }
}

密封类

密封类是一种特殊的抽象类,使用sealed关键字的抽象类,专门用于派生子类

密封类的子类是固定的。密封类的直接子类必须与密封类本身在同一个文件中,在其他文件中则不能为密封类派生直接子类,但密封类的间接子类(子类的子类)则无须在同一个文件中

密封类的所有构造器都必须是 private ,不管开发者是否使用 private 修饰,系统都会为之自动添加 private 修饰

//定义一个密封类,其实就是抽象类
sealed class Apple {
    abstract fun taste()
}

open class RedFuji : Apple() {
    override fun taste() {
        Log.d("RedFuji", "taste: 红富士苹果香甜可口")
    }
}

data class Gala(var weight: Double) : Apple() {
    override fun taste() {
        Log.d("RedFuji", "taste: 嘎拉果更消脆,重量为:${weight}")
    }
}

使用密封类的好处是:它的子类是固定的,因此使用 when 表达式判定密封类时,编译器可以清楚地知道是否覆盖了所有情况,从而判断是否需要添加 else 子句

fun judge(ap: Apple) {
    when (ap) {
        is RedFuji -> Log.d("TAG", "judge: 红富士苹果")
        is Gala -> Log.d("TAG", "judge: 嘎拉果")
    }
}

接口

接口的定义

用 interface 关键字,接口定义的基本语法如下:

[修饰符] interface 接口名: 父接口1, 父接口2... {
    零个到多个属性定义...
    零个到多个方法定义...
    零个到多个嵌套类、嵌套接口 嵌套枚举定义...
}
  1. 修饰符可以是 public | internal | private 中的任意一个,或完全省略修饰符,默认采用 public
  2. 接口名应采用大驼峰表示法
  3. 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类

Kotlin 的接口既可包含抽象方法,也可包含非抽象方法,但接口中的属性没有幕后字段,因此无法保存状态,所以接口中的属性要么声明为抽象属性,要么为之提供 setter、 getter 方法

由于接口定义的是一种规范,因此接口中不能包含构造器和初始化块定义

  1. 如果一个方法没有方法体, Kotlin会自动为该方法添加 abstract 修饰符
  2. 如果一个只读属性没有定义 getter 方法, Kotlin会自动为该属性添加 abstract 修饰符
  3. 如果一个读写属性没有定义 getter 和 setter 方法, Kotlin会自动为该属性添加 abstract 修饰符

Java 接口中的所有成员都会自动且只能使用 public 修饰,Kotlin 接口中的成员可支持 private 和 public 两种:

  1. 对于需要被实现类重写的成员,只能使用 public 修饰
  2. 对于不需要被实现类重写的成员,如非抽象方法、非抽象属性、嵌套类(包括嵌套抽象类)、嵌套接口、嵌套枚举,都可使用 private 或 public (默认)修饰
interface Outputable {
    //只读属性定义了 getter 方法,非抽象属性
    val name: String
        get() = "输出设备"

    //只读属性没有定义 getter 方法,抽象属性
    val brand: String

    //读写属性没有定义 getter 和 setter 方法,抽象属性
    var category: String

    //接口中定义的抽象方法
    fun out()
    fun getData(msg: String)

    //在接口中定义的非抽象方法,可使用 private 修饰
    fun print(vararg msgs: String) {
        for (msg in msgs) {
            Log.d("TAG", "print: $msg")
        }
    }
}

接口的继承

接口支持多继承,多个父接口排在英文冒号(:)之后,它们之间以英文逗号(,)隔开

interface InterfaceA {
    val propA: Int
        get() = 5

    fun testA()
}

interface InterfaceB {
    val propB: Int
        get() = 6

    fun testB()
}

interface InterfaceC : InterfaceA, InterfaceB {
    val propC: Int
        get() = 7

    fun testC()
}

使用接口

主要用途:

  1. 定义变量,也可用于进行强制类型转换
  2. 被其他类实现

一个类可以实现一个或多个接口,直接将被实现的多个接口、父类放在英文冒号之后,且父类、接口之间没有顺序要求,只要将它们用英文逗号隔开即可

[修饰符] class 类名: 父类, 接口1, 接口2...{
    类体部分
}

一个类实现了一个或多个接口之后,这个类必须完全实现这些接口中所定义的全部抽象成员(也就是重写这些抽象方法和抽象属性);否则,该类将保留从父接口那里继承到的抽象成员,该类也必须定义成抽象类

接口不能显式继承任何类,但所有接口类型的变量都可以直接赋给 Any 类型的变量

open class BaseClass {

}

class SubClass : BaseClass(), InterfaceA, InterfaceB {
    override fun testA() {
        Log.d("TAG", "testA: ")
    }

    override fun testB() {
        Log.d("TAG", "testA: ")
    }
}

接口和抽象类

接口和抽象类有一些相似之处:

  1. 不能被实例化
  2. 都可以包含抽象成员,实现接口或继承抽象类的普通子类都必须实现这些抽象成员

接口和抽象类在用法上的区别:

  1. 接口中不包含构造器;但抽象类中可以包含构造器
  2. 接口中不能包含初始化块;但抽象类中可以包含初始化块
  3. 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口

嵌套类和内部类

把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为嵌套类,包含嵌套类的类被称为外部类

Java 的内部类可分为两种:静态内部类 (有 static 修饰)和非静态内部类(无 static 修饰)

而对于Kotlin:

  1. 嵌套类(相当于静态内部类):只要将一个类放在另一个类中定义,这个类就变成了嵌套类
  2. 内部类(非静态内部类):使用 inner 修饰的嵌套类叫内部类,相当于 Java 中无 static 修饰的非静态内部类

作用:

  1. 嵌套类提供了更好的封装,可以把嵌套类隐藏在外部类之内,不允许同一个包中的其他类访问该类
  2. 内部类成员可以直接访问外部类的私有数据,但外部类不能访问内部类的实现细节,例如内部类的属性

定义嵌套类(内部类)与定义外部类的语法大致相同,只是嵌套类(内部类)可以使用任意访问控制符,包括 protected 修饰,用于表示该嵌套类(内部类)可在其外部类的子类中被访问

外部类的上一级程序单元是包,它不在任何类的里面,因此使用 protected 修饰符没有任何意义,使用 private 修饰符则意味着外部类仅能在定义该类的文件中才能访问

定义嵌套类(内部类)非常简单,只要把一个类放在另一个类的类内部嵌套定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义嵌套类(方法中定义的嵌套类被称为局部嵌套类)

内部类

//通过主构造器为外部类定义属性
class Cow(var weight: Double = 0.0) {
    //定义一个内部类(用 inner 修饰,相当于 Java 的非静态内部类)
    //通过主构造器为内部类定义属性
    private inner class CowLeg(var length: Double = 0.0, var color: String = "") {
        //内部类的方法
        fun info() {
            Log.d("TAG", "info: 当前牛腿颜色是:${color},高:${length}")
            //直接访问外部类的 private 修饰的 foo() 方法
            foo()
        }
    }
    
    fun test() {
        val cowLeg = CowLeg(1.12, "黑白相间")
        cowLeg.info()
    }

    private fun foo() {
        Log.d("TAG", "foo: Cow foo 方法")
    }
}

编译后悔生成 Cow.class 和 CowCowLeg.class 两个文件,成员嵌套类和成员内部类的 class 文件总是这种形式: OuterClassInnerClass.class

在内部类中可以直接访问外部类的 private 成员,这是因为在内部类对象中,保存了一个它所寄生的外部类对象的引用

如果外部类属性、内部类属性与内部类中方法的局部变量同名,则可通过使用 this、带标签的 this 进行限定来区分

//通过主构造器为外部类定义属性
class Cow(var weight: Double = 0.0) {
    private val prop ="外部类的属性"
    //定义一个内部类(用 inner 修饰,相当于 Java 的非静态内部类)
    //通过主构造器为内部类定义属性
    private inner class CowLeg(var length: Double = 0.0, var color: String = "") {

        private val prop ="内部类的属性"
        //内部类的方法
        fun info() {
            val prop ="局部变量"
            //通过 this@外部类类名.varName 访问外部类的属性
            Log.d("TAG", "info: 外部类的属性值:${this@Cow.prop}")
            //通过 this.varName 访问内部类的属性
            Log.d("TAG", "info: 内部类的属性值:${this.prop}")
            //直接访问局部变量
            Log.d("TAG", "info: 局部变量的值:${prop}")
        }
    }
}

Kotlin 关于 this 的处理规则如下:

  1. 类的方法或属性中, this 表调用该方法或属性的对象
  2. 类的构造器中, this 代表该构造器即将返回的对象
  3. 扩展函数或带接收者的函数字面值中, this 表示点(.)左边的“接收者”
  4. 如果 this 没有限定符,那么它优先代表包含该 this 的最内层的接收者,并且会自动向外搜索。如果要让 this 明确引用特定的接收者,则可使用标签限定符
class A { //隐式标签 @A
    inner class B { //隐式标签 @B
        //为 Int 扩展 foo() 方法
        fun Int.foo() { //隐式标签 @foo
            val a = this@A //A 的 this
            val b = this@B // B 的 this
            val c = this //不带标签的 this ,默认代表该方法所属对象: Int 对象
            val c1 = this@foo//显式指定@foo标签,与c代表的对象相同
            Log.d("TAG", "foo: $a")
            Log.d("TAG", "foo: $b")
            Log.d("TAG", "foo: $c")
            Log.d("TAG", "foo: $c1")
            //为 String 扩展 funLit() 方法
            val funLit = lambda@ fun String.() {
                val d = this //不带标签的 this ,默认代表该方法所属对象:String 对象
                val d1 = this@lambda //显式指定 @lambda 标签,与 d 代表的对象相同
                Log.d("TAG", "foo: $d")
                Log.d("TAG", "foo: $d1")
            }
            "book".funLit()
            //直接定义一个 Lambda 表达式,没有接收者
            val funLit2 = {
                //该 this 所在的 Lambda 表达式没有接收者,因此当前范围没有 this
                //系统会继续向该 Lambda 表达式所在范围搜索 this
                //故此处 this 将代表 foo() 方法的接收者: Int 对象
                val e = this
                val e1 = this@foo//显式指定@foo标签,与 e 代表的对象相同
                Log.d("TAG", "foo: $e")
                Log.d("TAG", "foo: $e1")
            }
            funLit2()
        }

        fun testB() {
            //调用 2(Int 值)的 foo() 方法
            2.foo()
        }
    }

    fun testA() {
        var bObj = B()
        Log.d("TAG", "testA: $bObj")
        bObj.testB()
    }
}

//调用
var aObj = A()
Log.d("TAG", "testA: $aObj")
aObj.testA()

嵌套类

嵌套类相当于 Java 的静态内部类,因此嵌套类直接属于外部类的类本身,而不是与外部类实例相关

  1. 嵌套类不可访问外部类的其他任何成员(只能访问其他嵌套类)

  2. 外部类依然不能直接访问嵌套类的成员,但可以使用嵌套类的对象作为调用者来访问嵌套

    类的成员

Kotlin 还允许在接口中定义嵌套类,但不允许在接口中定义内部类(即不允许定义使用 inner 修饰的内部类),如果为接口中的嵌套类指定访问控制符,则只能指定 public 或 private ,如果省略,默认是 public 访问权限

在外部类以外使用内部类

如果希望在外部类以外的地方使用内部类或嵌套类,一定要注意访问权限的限制,比如 private 修饰的嵌套类或内部类只能在外部类之内使用

在外部类以外的地方定义内部类变量的语法格式:

var | val varName: OuterClass.InnerClass

内部类完整的类名应该是OuterClass.InnerClass 。如果外部类有包名,则还应该增加包名前缀

在外部类以外的地方创建内部类实例的语法格式:

OuterInstance.InnerConstructor()

在外部类以外的地方创建内部类实例时,必须使用外部类实例来调用内部类的构造器

class Out {
    //定义一个内部类,不使用访问控制符,默认是 public
    inner class In(msg: String) {
        init {
            Log.d("TAG", ": $msg")
        }
    }
}

//调用
var oi: Out.In = Out().In("测试信息")
//上面代码可改为如下三行代码
//使用 OutterClass.InnerClass 的形式定义内部类变量
var outIn: Out.In
//创建外部类实例,内部类实例将寄生在该实例中
val out = Out()
//通过外部类实例来调用内部类的构造器创建内部类实例
outIn = out.In("测试信息")

在外部类以外使用嵌套类

因为嵌套类是属于外部类的类本身的,因此创建嵌套类对象时无须创建外部类对象,语法格式如下:

OuterClass.NestedConstructor()
class Out {
    //定义一个内部类,不使用访问控制符,默认是 public
    class In(msg: String) {
        init {
            Log.d("TAG", ": $msg")
        }
    }
}

//调用
var oi: Out.In = Out.In("测试信息")
//上面代码可改为如下两行代码
//使用 OuterClass.NestedClass 的形式定义嵌套类变量
var outIn: Out.In
//通过调用嵌套类的构造器创建嵌套类实例
outIn = Out.In("测试信息")

从上面代码可以看出,不管是内部类还是嵌套类,其声明变量的语法完全一样。区别只是在创建对象时,嵌套类只需使用外部类即可调用构造器,而内部类必须使用外部类对象来调用构造器

创建嵌套类的子类语法:

class NestedSubClass: NestedOut.Nested(){}

局部嵌套类

如果把一个嵌套类放在方法或函数中定义,则这个嵌套类就是一个局部嵌套类,由于局部嵌套类是局部成员,因此不能使用访问控制符修饰

class LocalNestedClass {
    fun info() {
        //定义局部嵌套类
        open class NestedBase(var a: Int = 0) {}

        //定义用部嵌套类的子类
        class NestedSub(var b: Int = 0) : NestedBase() {}

        //创建局部嵌套类的对象
        val ns = NestedSub()
        ns.a = 5
        ns.b = 8
        Log.d("TAG", "info: NestedSub 对象的 a 和 b 属性是:${ns.a},${ns.b}")
    }
}

编译,可以看到生成了三个 class 文件: LocalNestedClass.class、LocalNestedClass1NestedBase 和 LocalNestedClass1NestedSub.class ,这表明局部嵌套类的 class 文件总是遵循如下命名格式:OuterClass$NNestedClass.class

匿名内部类

Kotlin 彻底抛弃了Java 的匿名内部类,使用对象表达式

对象表达式其实就是增强版的匿名内部类,此外,如果对象是函数式接口(只包含一个抽象方法的接口)的实例,则可使用带接口类型前缀的 Lambda 表达式创建它

//使用 Lambda 表达式创建 Runnable 实例
var t = Runnable {
    for (i in 0..100) {
        Log.d("TAG", "onCreate: ${Thread.currentThread().name} i=$i")
    }
}
//启动新线程
Thread(t).start()
//主线程的循环
for (i in 0..100) {
    Log.d("TAG", "onCreate: ${Thread.currentThread().name} i=$i")
}

对象表达式和对象声明

Kotlin 提供了比匿名内部类更加强大的语法:对象表达式。它们的主要区别在于:匿名内部类只能指定一个父类型(接口或父类),但对象表达式可指定多个父类型(接口或父类)

对象表达式

对象表达式的本质就是增强版的匿名内部类,因此编译器处理对象表达式时也会像匿名内部类一样生成对应的 class 文件

语法格式:

object[: 0~N个父类型] {
    //对象表达式的类体部分
}

规则:

  1. 对象表达式不能是抽象类,因为系统在创建对象表达式时会立即创建对象
  2. 对象表达式不能定义构造器。但对象表达式可以定义初始化块,可以通过初始化块来完成构造器需要完成的事情
  3. 对象表达式可以包含内部类(有 inner 修饰的内部类),不能包含嵌套类
interface Outputable {
    fun output(msg: String)
}

abstract class Product(var price: Double) {
    abstract val name: String
    abstract fun printInfo()
}

//使用
//指定一个父类型(接口)的对象表达式
var ob1 = object : Outputable {
    override fun output(msg: String) {
        Log.d(TAG, "output: $msg")
    }
}
ob1.output("kotlin")

//指定0个父类型的对象表达式
var ob2 = object {
    //初始化块
    init {
        Log.d(TAG, "onCreate: 初始化块")
    }

    //属性
    var name = "kotlin"

    //方法
    fun test() {
        Log.d(TAG, "onCreate: test方法")
    }

    //只能包含内部类,不能包含嵌套类
    inner class Foo
}
Log.d(TAG, "onCreate: ${ob2.name}")
ob2.test()

//指定两个父类型的对象表达式
//由于 Product 只有一个带参数的构造器,因此要传入构造器参数
var ob3 = object : Outputable, Product(28.8) {
    override fun output(msg: String) {
        Log.d(TAG, "output: 输出信息$msg")
    }

    override val name: String
    get() = "激光打印机"

    override fun printInfo() {
        Log.d(TAG, "printInfo: 高速激光打印机,支持自动双面打印!")
    }
}
Log.d(TAG, "onCreate: ${ob3.name}")
Log.d(TAG, "onCreate: Kotlin 真不错!")
ob3.printInfo()

对象表达式在方法(或函数)的局部范围内,或使用 private 修饰的对象表达式, Kotlin编译器可识别该对象表达式的真实类型,就像上面的代码所示:程序为 ob2 增加了方法和属性,在 mainO 函数的局部范围内, Kotlin编译器完全可以识别 ob2 的真实类型,因此 ob2 可调用对象表达式增加的属性和方法

非 private 修饰的对象表达式与 Java 的匿名内部类相似,编译器只会把对象表达式当成它所继承的父类或所实现的接口处理。如果它没有父类型,系统当它是 Any 类型

class ObjectExprType {
    private val ob1 = object {
        val name: String = "kotlin"
    }
    
    internal val ob2 = object {
        val name: String = "java"
    }

    private fun privateBar() = object {
        val name: String = "privateBar"
    }

    fun publicBar() = object {
        val name: String = "publicBar"
    }

    fun test() {
        // ob1 是 private 对象表达式,编译器可识别它的真实类型
        //下面代码正确
        Log.d("TAG", "test: ${ob1.name}")
        // ob2 是非 private 对象表达式,编译器当它是 Any 类型
        //下面代码错误
        Log.d("TAG", "test: ${ob2.name}")
        // privateBar() 是 private 函数,编译器可识别它返回的对象表达式的真实类型
        //下面代码正确
        Log.d("TAG", "test: ${privateBar().name}")
        // publicBar() 是非 private 函数,编译器将它返回的对象表达式当成 Any 类型
        //下面代码错误
        Log.d("TAG", "test: ${publicBar().name}")
    }
}

Kotlin 的对象表达式可访问或修改其作用域内的局部变量

var a = 20
var obj = object {
    fun change() {
        Log.d(TAG, "change: 修改变量 a 的值")
        a++
    }
}
obj.change()
Log.d(TAG, ": a=$a") //输出 21

Kotlin 对象表达式比 Java 匿名内部类增强了三个方面:

  1. 可指定多个父类型
  2. Kotlin 编译器能更准确地识别局部范围内 private 对象表达式的类型
  3. 可访问或修改其所在范围内的局部变量

对象声明和单例模式

对象声明专门用于实现单例模式,对象声明所定义的对象也就是该类的唯一实例,程序可通过对象声明的名称直接访问该类的唯一实例。语法格式:

object ObjectName[: 0~N个父类型]{
    //对象表达式的类体部分
}

对象声明与对象表达式的语法格式的区别是:对象表达式在 object 关键字后没有名字,而对象声明需要在 object 关键字后指定名字

对象声明和对象表达式还存在如下区别:

  1. 对象表达式是一个表达式,它可以被赋值给变量,而对象声明不是表达式,因此它不能用于赋值
  2. 对象声明可包含嵌套类,不能包含内部类,而对象表达式可包含内部类,不能包含嵌套类
  3. 对象声明不能定义在函数和方法内,但对象表达式可嵌套在其他对象声明或非内部类中
interface Outputable {
    fun output(msg: String)
}

abstract class Product(var price: Double) {
    abstract val name: String
    abstract fun printInfo()
}

//指定一个父类型(接口)的对象表达式
object MyObject1 : Outputable {
    //重写父接口中的抽象方法
    override fun output(msg: String) {
        Log.d("TAG", "output: $msg")
    }
}

//指定0个父类型的对象表达式
object MyObject2{
    //初始化块
    init {
        Log.d("TAG", ": 初始化块")
    }

    //属性
    var name = "kotlin"

    //方法
    fun test() {
        Log.d("TAG", ": test方法")
    }

    //只能包含嵌套类,不能包含内部类
    class Foo
}

//指定两个父类型的对象友达式
//由于 Product 只有一个带参数的构造器,因此要传入构造器参数
object MyObject3: Outputable, Product(28.8) {
    override fun output(msg: String) {
        Log.d("TAG", "output: 输出信息$msg")
    }

    override val name: String
        get() = "激光打印机"

    override fun printInfo() {
        Log.d("TAG", "printInfo: 高速激光打印机,支持自动双面打印!")
    }
}

//使用
MyObject1.output("kotlin")
Log.d(TAG, "onCreate: ${MyObject2.name}")
MyObject2.test()
Log.d(TAG, "onCreate: ${MyObject3.name}")
MyObject3.output("kotlin")
MyObject3.printInfo()

伴生对象和静态成员

在类中定义的对象声明,可使用 companion 修饰,这样该对象就变成了伴生对象

每个类最多只能定义一个伴生对象,伴生对象相当于外部类的对象,程序可通过外部类直接调用伴生对象的成员

interface Outputable {
    fun output(msg: String)
}

class MyClass {
    //使用 companion 修饰的伴生对象
    companion object MyObject1 : Outputable {
        val name = "name属性值"

        //重写父接口中的抽象方法
        override fun output(msg: String) {
            Log.d("TAG", "output: msg=$msg")
        }
    }
}

//使用
//使用伴生对象所在的类调用伴生对象的方法
MyClass.output("ktolin")
Log.d(TAG, "onCreate: ${MyClass.name}")

伴生对象可以省略名称。省略名称之后,如果程序真的要访问伴生对象,则可通过 Companion 名称进行访问

interface Outputable {
    fun output(msg: String)
}

class MyClass {
    //使用 companion 修饰的伴生对象
    companion object : Outputable {
        val name = "name属性值"

        //重写父接口中的抽象方法
        override fun output(msg: String) {
            Log.d("TAG", "output: msg=$msg")
        }
    }
}

//使用
//使用伴生对象所在的类调用伴生对象的方法
MyClass.output("ktolin")
//使用 Companion 名称访问伴生对象
Log.d(TAG, "onCreate: ${MyClass.Companion}")

伴生对象的主要作用就是为它所在的类模拟静态成员,但只是模拟,伴生对象的成员依然是伴生对象本身的实例成员,并不属于伴生对象所在的外部类,可通过@JvmStatic 注解让系统根据伴生对象的成员为其所在的外部类生成真正的静态成员

伴生对象的扩展

伴生对象也可以被扩展,就相当于为伴生对象所在的外部类扩展了静态成员,可通过外部类的类名访问这些扩展成员

//为伴生对象扩展方法
fun MyClass.Companion.test() {
    Log.d("TAG", "test: 为伴生对象扩展的方法")
}

val MyClass.Companion.foo
    get() = "为伴生对象扩展的属性"

//使用
//通过伴生对象所在的类调用为伴生对象扩展的成员
MyClass.test()
println(MyClass.foo)

枚举类

枚举类入门

Kotlin 使用 enum class 关键字组合定义枚举类。枚举类是一种特殊的类,它一样可以有自己的属性、方法,可以实现一个或多个接口,也可以定义自己的构造器

枚举类与普通类有如下简单区别:

  1. 使用 enum 定义的枚举类默认继承 kotlin.Enum 类,而不是默认继承 Any 类,因此枚举类不能显式继承其他父类,其中 Enum 类实现了kotlin.Comparable 接口
  2. 使用 enum 定义的非抽象的枚举类不能使用 open 修饰,因此枚举类不能派生子类
  3. 枚举类的构造器只能使用 private(默认) 访问控制符
  4. 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例

枚举类默认提供了如下两个方法

方法或属性 说明
EnumClass.valueOf(value: String): EnumClass 类似于 Java 枚举类的 valueOf() 方法,用于根据枚举的字符串名获取实际的枚举值
EnumClass.values(): Array<EnumClass> 类似于 Java 枚举类的 values() 方法,用于获取该枚举的所有枚举值组成的数组
name 属性 返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一,优先考虑更加友好的 toString() 方法
ordinal 属性 返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为0
int compareTo(E o) 该方法用于与指定的枚举对象比较顺序,如果该枚举对象位于指定的枚举对象之后,则返回正整数;如果该枚举对象位于指定的枚举对象之前,则返回负整数;否则返回 0
String toString() 返回枚举常量的名称
enum class Season {
    //在第一行列出4个枚举实例
    SPRING, SUMMER, FALL, WINTER
}

如果需要使用该枚举类的某个实例,则可使用 EnumClass. variable 的形式,如:Season.SPRING

//枚举类默认有一个 values() 方法,返回该枚举类的所有实例
for (s in Season.values()) {
    Log.d(TAG, "onCreate: $s")
}
val seasonName = "SUMMER"
val s: Season = Season.valueOf(seasonName)
Log.d(TAG, "onCreate: $s")
//直接访问枚举值
Log.d(TAG, "onCreate: ${Season.WINTER}")

枚举可使用 enumValues<T>() 和 enumValueOf<T>() 函数以泛型的方式访问枚举类中的常量,因此可为所有枚举定义如下方法来遍历所有的枚举实例

inline fun <reified T : Enum<T>> printAllValues() {
    var joinToString = enumValues<T>().joinToString { it.name }
    Log.d("TAG", "printAllValues: $joinToString")
}

按如下方式调用该函数输出所有枚举值:

printAllValues<Season>()

枚举类的属性、方法和构造器

由于枚举类应该设计成不可变类,枚举的属性都是只读属性,枚举必须在构造器中为这些属性指定初始值(或在初始化块中指定初始值),一般不会在定义时指定初始值,因为这样会导致所有枚举值的该属性值总是相同的),因此应该为枚举类显式定义带参数的构造器

//使用主构造器声明 cnName 只读属性
enum class Gender(val cnName: String) {
    MALE("男"), FEMALE("女");

    //定义方法
    fun info() {
        when (this) {
            MALE -> Log.d("TAG", "info: 帅")
            FEMALE -> Log.d("TAG", "info: 美")
        }
    }
}

在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须显式调用构造器

由于上面的枚举类需要在列出枚举值之后定义额外的枚举成员(如枚举方法),因此上面程序需要用分号表示枚举值列表结束

实现接口的枚举类

枚举类也可以实现一个或多个接口

interface GenderDesc {
    fun info()
}

//使用主构造器声明 cnName 只读属性
enum class Gender(val cnName: String):GenderDesc {
    MALE("男"), FEMALE("女");

    //定义方法
    override fun info() {
        when (this) {
            MALE -> Log.d("TAG", "info: 帅")
            FEMALE -> Log.d("TAG", "info: 美")
        }
    }
}

包含抽象方法的抽象枚举类

需要让枚举的各个枚举值对某个方法各有不同的实现,就可以定义一个抽象方法,然后让各个枚举值分别提供不同的实现

enum class Operation {
    PLUS {
        override fun eval(x: Double, y: Double) = x + y
    },
    MINUS {
        override fun eval(x: Double, y: Double) = x - y
    },
    TIMES {
        override fun eval(x: Double, y: Double) = x * y
    },
    DIVIDE {
        override fun eval(x: Double, y: Double) = x / y
    };

    //为枚举类定义一个抽象方法
    //这个抽象方法由不同的枚举值提供不同的实现
    abstract fun eval(x: Double, y: Double): Double
}

//使用
Operation.PLUS.eval(3.0, 4.0)

编译上面程序,会生成 5 个 class 文件,其中 Operation 对应一个 class 文件,它的 4 个匿名内部子类各对应 class 文件

在定义每个枚举值时必须为抽象成员提供实现,否则将出现编译错误

类委托和属性委托

类委托

类委托的本质就是:将本类需要实现的部分方法委托给其他对象一一相当于借用其他对象的方法作为自己的实现

interface Outputable {
    fun output(msg: String)
    var type: String
}

//定义一个 DefaultOutput 类实现 Outputable 接口
class DefaultOutput : Outputable {
    override fun output(msg: String) {
        Log.d("TAG", "output: msg=$msg")
    }

    override var type: String = "输出设备"
}

//定义 Printer 指定构造参数 b 作为委托对象
class Printer(b: DefaultOutput) : Outputable by b

//定义 Projector 类,指定新建的对象作为委托对象
class Projector() : Outputable by DefaultOutput() {
    //该类重写 output() 方法
    override fun output(msg: String) {
        Log.d("TAG", "output: Projector msg=$msg")
    }
}

//使用
val output = DefaultOutput()
// Printer 对象的委托对象是 output
var printer = Printer(output)
//其实就是调用委托对象的 output() 方法
printer.output("kotlin")
//其实就是调用委托对象的 type 属性方法
Log.d(TAG, "onCreate: ${printer.type}")
// Projector 对象的委托对象也是 output
var projector = Projector()
// Projector 本身重写了 output() 方法,所以此处是调用本类重写的方法
projector.output("kotlin")
//其实就是调用委托对象的 type 属性方法
Log.d(TAG, "onCreate: ${projector.type}")

属性委托

Kotlin 也支持属性委托,属性委托可以将多个类的类似属性统一交给委托对象集中实现,这样就可避免每个类都需要单独实现这些属性

不能为委托属性提供 getter 和 setter 方法, Kotlin 也不会为委托属性提供 getter 和 setter 方法的默认实现,属性的委托对象一定要提供一个使用 operator 修饰 getValue() 方法和 setValue() 方法(val 属性无须提供 setValue() 方法)

getValue() 方法的参数和返回值要求:

  1. thisRef:该参数代表属性所属的对象,因此该参数的类型必须是属性所属对象的类型(对于扩展属性,指被扩展的类型)或其超类型
  2. property:该参数代表目标属性,该参数的类型必须是 KProperty<*>或其超类型
  3. 返回值:该方法必须返回与目标属性相同的类型(或其子类型)

getValue() 方法的参数和返回值要求:

  1. thisRef:与 getValue() 方法的 thisRef 参数的作用相同
  2. property:与 getValue() 方法的 thisRef 参数的作用相同
  3. newValue:该参数代表目标属性新设置的属性值,该参数的类型必须具有和目标属性相同的类型或其超类型
class PropertyDelegation {
    //该属性的委托对象是 MyDelegation
    var name: String by MyDelegation()
}

class MyDelegation {
    private var _backValue = "默认值"
    operator fun getValue(
        thisRef: PropertyDelegation,
        property: KProperty<*>
    ): String {
        Log.d("TAG", "getValue: ${thisRef} 的${property.name}属性执行getter方法")
        return _backValue
    }

    operator fun setValue(
        thisRef: PropertyDelegation,
        property: KProperty<*>, newValue: String
    ) {
        Log.d("TAG", "getValue: ${thisRef} 的${property.name}属性执行setter方法,传入参数值为:${newValue}")
        _backValue = newValue
    }
}

//使用
val pd = PropertyDelegation()
//读取属性,实际上是调用属性的委托对象的 getter 方法
Log.d(TAG, "onCreate: ${pd.name}")
println(pd.name)
//写入属性,实际上是调用属性的委托对象的 setter 方法
pd.name = "kotlin"
Log.d(TAG, "onCreate: ${pd.name}")

ReadOnlyProperty 接口定义了一个符合只读属性委托标准的 getValue() 抽象方法,因此该接口的实现类可作为读属性的委托对象;而ReadWriteProperty 接口定义了符合读写属性委托标准的getValue() 方法和 setValue() 抽象方法,因此该接口的实现类可作为读写属性的委托对象

Kotlin 会为委托属性生成辅助属性,该辅助属性引用了属性的委托对象,例如上面的属性委托的类会编译为:

//这段代码是由编译器生成的
class PropertyDelegation {
    //生成隐藏属性,隐藏属性引用代理对象
    var name$delegate = MyDelegation()

    var name: String
        // getter 方法,实际上就是调用代理对象的 getValue() 方法
        get() = name$delegate.getValue(this, this::name$delegate)
        // setter 方法,实际上就是调用代理对象的 setValue() 方法
        set(value: String) = name$delegate.setValue(this, this::name$delegate, value)
}

延迟属性

Kotlin 提供了 lazy() 函数,该函数接受 Lambda 表达式作为参数,并返回一个Lazy<T>对象,Lazy<T>对象包含了一个符合只读属性委托要求的 getValue() 方法,因此该 Lazy<T>对象可作为只读属性的委托对象

val lazyProp: String by lazy {
    Log.d("TAG", ": 第一次访问时执行代码块")
    "kotlin"
}

//使用
Log.d(TAG, "onCreate: $lazyProp")

Lazy<T>的 getValue() 方法的处理逻辑是:第一次调用该方法时,程序会计算 Lambda 表达式,并得到其返回值,以后程序再次调用该方法时,不再计算 Lambda 表达式,而是直接使用第一次计算得到的返回值

lazy方法 说明
fun <T> lazy(initializer: () -> T): Lazy<T> 会自动添加保证线程安全的同步锁,相当第二个 lazy() 函数的第一个参数指定为LazyThreadSafetyMode.SYNCHRONIZED
fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> LazyThreadSafetyMode.PUBLICATION: lazy() 函数不会使用排他同步锁,多个线程可同步执行;LazyThreadSafetyMode.NONE:lazy() 函数不会有任何线程安全相关的操作和开销

属性监昕

Kotlin 的 kotlin.properties 包下提供了 Delegates 对象声明 (单例对象),该对象中包含如下两个常用方法:

方法 说明
fun <T> observable(initialValue: T, onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit): ReadWriteProperty<Any?> 返回ReadWriteProperty对象,因此该方法的返回值可作为属性的委托对象。第一个参数用于为接受委托的属性指定初始值,第二个参数可接受 Lambda 表达式, 当接受委托的属性被设置值时,Lambda 表达式就会被触发
fun <T> vetoable(initialValue: T, onChange: (property:KPrope y<*>, oldValue: T, newValue: T) -> Boolean): ReadWritePrope ty<Any?> 与前一个方法基本相同,区别只是负责执行监听的 Lambda 表达式需要返回值,当该返回值为 true 时,接受委托的属性的新值设置成功,否则设置失败
var observableProp: String by Delegates.observable("默认值") {
        // Lambda 表达式的第一个参数代表被监听的属性
        //第二个参数代表修改之前的值
        //第三个参数代表被设置的新值
        prop, old, new ->
    Log.d("TAG", ": ${prop} 的${old} 被改为 ${new}")
}

//使用
//访问 observableProp 属性,不会触发监听器的 Lambda 表达式
Log.d(TAG, "onCreate: $observableProp")
//设置属性值,触发监听器的 Lambda 表达式
observableProp = "kotlin"
Log.d(TAG, "onCreate: $observableProp")

        //上面输出结果为:
//        默认值
//        property observableProp (Kotlin reflection is not available) 的默认值 被改为 kotlin
//        kotlin

使用 Map 存储属性值

Kotlin 的 Map 提供了如下方法:

public inline operator fun <V, V1 : V> Map<in String, @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1

该方法符合只读属性的委托对象的要求,这意味着 Map 对象可作为只读对象的委托

MutableMap 提供如下两个方法:

public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V)

符合读写属性的委托对象的要求,这意味着 MutableMap 对象可作为读写对象的委托

因此程序可将类只读属性委托给 Map 象管理,在这种情况下,对象本身并不负责存储对象状态,而是将对象状态保存在 Map 集合中。这么做的好处是,当程序需要与外部接口(如JSON )通信时,程序并不需要将该对象直接暴露出来,只要将该对象属性所委托的 Map 暴露出来即可,而 Map 完整地保存了整个对象状态

class Item(val map: Map<String, Any?>) {
    val bareCode: String by map
    val name: String by map
    val price: Double by map
}

//使用
val item = Item(
    mapOf(
        "barCode" to "34285",
        "name" to "kotlin",
        "price" to 56
    )
)
Log.d(TAG, "onCreate: bareCode=${item.bareCode} name=${item.name} price=${item.price}")
//将对象持有的 map 暴露出来,其他程序可通过标准 Map 读取数据
val map = item.map
Log.d(
    TAG,
    "onCreate: bareCode=${map["bareCode"]} name=${map["name"]} price=${map["price"]}"
)

局部属性委托

Kotlin 支持为局部变量指定委托对象。这种指定了委托对象的局部变量被称为“局部委托属性”一一其实还是局部变量,只是对该变量的读取、赋值操作将会交给委托对象去实现

对于只读属性而言,同样需要实现 operator 修饰的 getValue(thisRef: Nothing?, property: KProperty<*>)方法,注意该方法的第一个参数是 Nothing? 类型, 是一个特殊的类,专门用于代表永不存在的对象

对于读写属性而言, 需要实现 getValue() 和 setValue() 两个方法,其中 setValue() 方法的第一个参数的类型也应该声明为 Nohting?

class MyDelegation {

    private var _backValue = "默认值"
    operator fun getValue(nothing: Nothing?, property: KProperty<*>): String {
        Log.d("TAG", "getValue: ${nothing} 的${property.name}属性执行getter方法")
        return _backValue
    }

    operator fun setValue(nothing: Nothing?, property: KProperty<*>, s: String) {

        Log.d("TAG", "getValue: ${nothing} 的${property.name}属性执行setter方法,传入参数值为:${s}")
        _backValue = s
    }
}

//使用
var name: String by MyDelegation()
//访问局部变量,实际上是调用 MyDelegation 对象的 getValue() 方法
Log.d(TAG, "onCreate: $name")
//对局部变量赋值,实际上是调用 MyDelegation 对象的 setValue() 方法
name = "新的值"

与普通属性类似的是,程序也可以利用 Kotlin 所提供的标准委托,比如可以利用 lazy() 函数对局部变量延迟初始化

委托工厂

除提供 getValue() 和 setValue() 方法的对象可作为属性的委托对象之外, Kotlin 1.1 开始,一种类似于“委托工厂”的对象也可作为委托对象。委托工厂需要提供如下方法:

operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): 该方法的两个参数与委托的 getValue() 方法的两个参数的意义相同

如果上面方法返回 ReadOnlyProperty 对象,那么该对象可作为只读属性的委托对象,如果返回 ReadWriteProperty 对象,则该对象就可作为读写属性的委托对象

使用 provideDelegate() 方法来生成委托的好处是, Kotlin 会保证对象的属性被初始化时调用该方法来生成委托,这样我们即可在该方法中加入任何自定义代码

class MyTarget {
    //该属性的委托对象是 provideDelegate() 方法返回的 MyDelegation 对象
    var name: String by PropertyChecker()
}

class PropertyChecker() {
    operator fun provideDelegate(
        thisRef: MyTarget,
        prop: KProperty<*>
    ): ReadWriteProperty<MyTarget, String> {
        //插入口定义代码,可执行任意业务操作
        checkProperty(thisRef, prop.name)
        //返回实际的委托对象
        return MyDelegation()
    }

    private fun checkProperty(thisRef: MyTarget, name: String) {
        Log.d("TAG", "checkProperty: 检查属性")
    }
}

class MyDelegation : ReadWriteProperty<MyTarget, String> {

    private var _backValue = "默认值"

    override fun getValue(thisRef: MyTarget, property: KProperty<*>): String {
        Log.d("TAG", "getValue: ${thisRef} 的${property.name}属性执行getter方法")
        return _backValue
    }

    override fun setValue(thisRef: MyTarget, property: KProperty<*>, value: String) {
        Log.d("TAG", "getValue: ${thisRef} 的${property.name}属性执行setter方法,传入参数值为:${value}")
        _backValue = value
    }
}

//使用
//创建对象(初始化属性),调用委托工厂的 provideDelegate() 方法
val pd = MyTarget()
//读取属性,实际上是调用属性的委托对象的 getter 方法
Log.d(TAG, "onCreate: ${pd.name}")
//写入属性,实际上是调用属性的委托对象的 setter 方法
pd.name = "kotlin"
Log.d(TAG, "onCreate: ${pd.name}")
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容