Kotlin学习系列(五)Kotlin属性(声明属性、延迟初始化属性、属性委托、惰性加载属性,可观察属性)

本系列内容均来自《Kotlin从小白到大牛》一书,感谢作者关东升老师。
属性是为了方便访问封装后的字段而设计的, 属性本身并不存储数据, 数据是存储在支持字段( backing field)中的。
Kotlin中属性可以在类中声明, 称为成员属性。 属性也可以在类之外, 类似于 顶层函数, 称为顶层属性, 事实上顶层属性就是全局变量。 本章介绍的属性主要是类 的成员属性。
1 回顾JavaBean
【Kotlin学习系列(五)Kotlin属性(声明属性、延迟初始化属性、属性委托、惰性加载属性,可观察属性)】JavaBean 是一种Java语言的可重用组件技术, 它能够与JSP( Java Server Page)标签绑定, 很多Java框架也使用JavaBean。 JavaBean的字段( 成员变量) 往往被封装称为私有的, 为了能够在类的外部访问这些字段, 则需要通过getter和setter访问器访问。 动物( Animal) 类Java代码如下:

public class Animal { // 动物年龄 private int age = 1; // 动物性别 private boolean sex = false; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public boolean isSex() { return sex; } public void setSex(boolean sex) { this.sex = sex; } }

如果使用Kotlin语言同样的类, 代码如下:
class Animal { // 动物年龄 var age = 1 // 动物性别 var sex = false }

可见Kotlin代码非常的简洁, 注意上述Animal类中的age和sex不是字段而属性, 一个属性对应一个字段, 以及 setter和getter访问器, 如果是只读属性则没有setter访问器。
2 声明属性
Kotlin中声明属性的语法格式如下:
var|val 属性名 [ : 数据类型] [= 属性初始化 ] [getter访问器] [setter访问器]

提示 属性本身并不真正的保存数据, 数据被保存到支持字段( backing field) 中, 支持字段一般是不可见的, 支持字段只能应用在属性访问器中, 通过系统定义好的field变量访问。
示例代码如下:
class Employee { var no: Int = 0 // 员工编号属性 var job: String? = null // 工作属性 //① var firstName: String = "Tony" //② var lastName: String = "Guan" //③ var fullName: String //全名 ④ get() { //⑤ return firstName + "." + lastName } set (value) { //⑥ val name = value.split(".")// ⑦ firstName = name[0] lastName = name[1] } var salary: Double = 0.0 // 薪资属性 ⑧ set(value) { if (value >= 0.0) field = value// ⑨ } }fun main(args: Array) { val emp = Employee() println(emp.fullName)//Tony.Guan emp.fullName = "Tom.Guan" println(emp.fullName)//Tom.Guan emp.salary = -10.0 //不接收负值 println(emp.salary)//0.0 emp.salary = 10.0 println(emp.salary)//10.0 }

注意 并不是所有的属性都有支持字段( backing field) 的, 例如上述代码中的fullName属性是通过另外属性计算而来, 它没有支持字段, 声明时不需要初始值。
3 延迟初始化属性
假设公司管理系统中两个类Employee( 员工) 和Department( 部门) , 它们的类图如下图所示, 它们有关联关系, Employee所在部门的属性dept与Department关联起来。 这种关联关系体现为: 一个员工必然隶属于一个部门, 一个员工实例对应于一个部门实例。

Kotlin学习系列(五)Kotlin属性(声明属性、延迟初始化属性、属性委托、惰性加载属性,可观察属性)
文章图片
image.png
看下列代码:
// 员工类 class Employee { ... var dept = Department() // 所在部门属性 ① } // 部门类 class Department { var no: Int = 0 // 部门编号属性 var name: String = "" // 部门名称属性 } //代码文件: chapter11/src/com/a51work6/section4/s3/ch11.4.3.kt fun main(args: Array) { val emp = Employee() ... println(emp.dept) }

在创建Employee对象时, 需要同时需要实例化Employee的所有属性, 也包括实例化dept( 部门) 属性, 代码第①行声明dept属性的同时进行了初始化, 创建Department对象。 如果是一个新入职的员工, 有时不关心员工在哪个部门, 只关心他的no( 编号) 和name( 姓名) 。 但上述代码虽然不使用dept对象, 但是仍然会实例化它, 这样会占用内存
Kotlin可以对属性设置为延迟初始化的, 修改代码如下:
// 员工类 class Employee { ... lateinit var dept: Department // 所在部门属性 ① } // 部门类 class Department { var no: Int = 0 // 部门编号属性 var name: String = "" // 部门名称属性 } fun main(args: Array) { val emp = Employee() ... emp.dept = Department() println(emp.dept) }

在声明dept属性前面添加了关键字lateinit, 这样dept属性就是延时初始化。 顾名思义, 延时初始化属性就是不必在类实例化时初始化它, 可以根据需要在程序运行期初始化。 而没有lateinit声明的非可空类型属性必须在类实例化时初始化。
提示 延时初始化属性要求: 不能是可空类型; 只能使用为var声明; lateinit关键字应该放在var之前。
4 委托属性
Kotlin提供一种委托属性, 使用by关键字声明, 示例代码如下:
class User { var name: String by Delegate() }class Delegate { operator fun getValue(thisRef: Any, property: KProperty<*>): String { return property.name }operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println(value) }}fun main(args: Array) { val user = User() user.name = "Tom" println(user.name) }

上述代码声明委托属性,by是委托运算符, 它后面的Delegate()就是属性name的委托对象, 通过by运算符属性name的setter访问器被委托给Delegate对象的setValue函数, 属性name的getter访问器被委托给Delegate对象的getValue函数。
Delegate对象不必实现任何接口, 只需要实现getValue和setValue函数即可, 注意这两个函数前面都有operator关键字修饰operator所修饰的函数是运算符重载函数, 本例中说明了getValue和setValue函数重载by运算符。给name属性赋值, 这会调用委托对象的setValue函数, 读取name数组值, 这会调用委托对象的getValue函数。
5 惰性加载属性
惰性加载属性与延迟初始化属性类似, 只有第一次访问该属性时才进行初始化。 不同的是惰性加载属性使用的lazy函数声明委托属性, 而延迟初始化属性lateinit关键字修饰属性还有惰性加载属性必须是val的, 而延迟初始化属性必须是var的
示例代码如下:
open class Employee { var no: Int = 0 // 员工编号属性 var firstName: String = "Tony" var lastName: String = "Guan" val fullName: String by lazy { firstName + "." + lastName } lateinit var dept: Department } // 部门类 class Department { var no: Int = 0 // 部门编号属性 var name: String = "" // 部门名称属性 } fun main(args: Array) { val emp = Employee() println(emp.fullName)//Tony.Guan val dept = Department() dept.no = 20 emp.dept = dept println(emp.dept) }

注意lazy不是关键字, 而是函数。 lazy函数后面跟着的是尾随Lambda表达式。 惰性加载属性使用val声明。
6 可观察属性
另一个使用委托属性示例是可观察属性, 委托对象监听属性的变化, 当属性变化时委托对象会被触发。
实例代码如下:
// 部门类 class Department { var no: Int = 0 // 部门编号属性 var name: String by Delegates.observable("<无>") { p, oldValue, newValue -> println("$oldValue -> $newValue") } } fun main(args: Array) { val dept = Department() dept.no = 20 dept.name = "技术部" //输出<无> -> 技术部 ② dept.name = "市场部" //输出技术部 -> 市场部 ③ }

声明name委托属性, by关键字后面Delegates.observable()函数有两个参数第一个参数是委托属性的初始化值第二个参数是属性变化事件的响应器,响应器是函数类型, 具体调用时可使用Lambda表达式作为实际参数。 在用Lambda表达式中有三个参数, 其中p是属性, oldValue是属性的旧值, newValue是属性的新值。

    推荐阅读