本系列内容均来自《Kotlin从小白到大牛》一书,感谢作者关东升老师。
属性是为了方便访问封装后的字段而设计的, 属性本身并不存储数据, 数据是存储在支持字段( backing field)
中的。
Kotlin中属性可以在类中声明, 称为成员属性。 属性也可以在类之外, 类似于 顶层函数, 称为顶层属性, 事实上顶层属性就是全局变量。 本章介绍的属性主要是类 的成员属性。
1 回顾JavaBean
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<String>) {
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关联起来。 这种关联关系体现为: 一个员工必然隶属于一个部门, 一个员工实例对应于一个部门实例。
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<String>) {
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<String>) {
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<String>) {
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<String>) {
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<String>) {
val dept = Department()
dept.no = 20
dept.name = "技术部" //输出<无> -> 技术部 ②
dept.name = "市场部" //输出技术部 -> 市场部 ③
}
声明name委托属性, by关键字后面Delegates.observable()函数有两个参数
: 第一个参数是委托属性的初始化值
, 第二个参数是属性变化事件的响应器
,响应器是函数类型, 具体调用时可使用Lambda表达式作为实际参数。 在用Lambda表达式中有三个参数, 其中p是属性, oldValue是属性的旧值, newValue是属性的新值。
网友评论