希望帮你在Kotlin进阶路上少走弯路,在技术上稳步提升。当然,由于个人知识储备有限,笔记中难免存在疏漏或表述不当的地方,也非常欢迎大家提出宝贵意见,一起交流进步。 ------ Android_小雨
整体目录:Kotlin 进阶不迷路:41 个核心知识点,构建完整知识体系
一、前言
1.1 类属性的核心作用
在面向对象编程体系中,类属性 是刻画对象本质的核心要素,其核心作用是存储对象的状态信息并描述对象的固有特征。如果将类比作创建对象的"蓝图",那么属性就是蓝图中定义的"数据插槽",每个对象通过这些插槽存储自身独有的数据。例如"Person"类的"name""age"属性,可精准存储具体人物的姓名与年龄;"Book"类的"title""author""price"属性,能清晰描述一本书的核心特征。缺少属性的类无法承载对象的状态,面向对象的抽象性与封装性也无从谈起。
1.2 Kotlin 属性的特点
相较于 Java 中"字段(Field)+ 访问器(Getter/Setter)"的分离式设计,Kotlin 的属性设计实现了"简洁性"与"灵活性"的统一,核心特点体现在两方面:
- 默认访问器自动生成:定义属性时,Kotlin 会根据属性可变性(var/val)自动生成对应的 Getter(读取方法)和 Setter(修改方法),无需像 Java 那样手动编写冗余的访问器代码。例如定义"var age: Int",Kotlin 会自动生成"getAge()"和"setAge()"方法,大幅简化开发流程;
- 支持灵活自定义访问器:当默认访问器无法满足业务需求(如数据校验、动态计算、格式转换等)时,可直接在属性定义中重写 Getter 或 Setter 逻辑,既保留了语法简洁性,又能适配复杂场景。
1.3 本文核心内容预告
本文将围绕 Kotlin 类的"属性"与"访问器"展开系统讲解,遵循"基础认知→原理剖析→实践应用→总结升华"的逻辑脉络:首先从属性的基本定义语法、可变与只读属性的区别入手,掌握属性的默认行为;接着深入解析访问器(Getter/Setter)的核心原理,理解属性读写的底层逻辑;然后重点讲解自定义访问器的语法及用法,包括自定义 Getter 实现动态计算、自定义 Setter 实现数据校验等关键场景;之后结合实际开发案例,展示不同访问器用法的适用场景;最后总结核心知识点、避坑要点及优化技巧,帮助读者高效掌握 Kotlin 属性的使用精髓。
二、类属性基础:定义与默认行为
Kotlin 的属性是"字段 + 访问器"的封装体,定义时无需显式区分字段和访问器,通过简洁语法即可完成定义,其默认行为已能满足大部分常规开发场景。
2.1 属性的基本语法
Kotlin 类属性的定义语法高度凝练,核心由"可变性关键字 + 属性名 + 数据类型 + 默认值"四部分组成,根据场景不同可衍生出多种简化形式,基本格式如下:
kotlin
// 完整语法:可变性关键字 + 属性名 + 类型 + 默认值
var/val 属性名: 数据类型 = 默认值
// 简化场景1:类型推断(默认值明确时可省略数据类型)
var/val 属性名 = 默认值
// 简化场景2:延迟初始化(适用于无法立即赋值的非空引用类型)
lateinit var 属性名: 数据类型
// 简化场景3:惰性初始化(适用于重量级对象,首次访问时初始化)
val 属性名: 数据类型 by lazy { 初始化逻辑 }
语法说明:
- 可变性关键字:"var"(variable 的缩写)表示可变属性,支持读写操作;"val"(value 的缩写)表示只读属性,仅支持读取操作,初始化后不可修改;
- 数据类型:指定属性存储的数据类型(如 String、Int、Boolean 等),Kotlin 支持强大的类型推断,若提供默认值且类型明确,可省略数据类型;
- 默认值:属性的初始值,若暂时无法赋值(如依赖后续注入),可使用"lateinit"(仅适用于 var 修饰的非空引用类型)或"lazy"(仅适用于 val 修饰的属性)实现延迟初始化;
- lateinit 与 lazy 区别:lateinit 需手动赋值后才能访问,否则抛出未初始化异常;lazy 通过 Lambda 表达式定义初始化逻辑,首次访问时执行且仅执行一次,返回初始化结果。
2.2 可变属性 (var) 与只读属性 (val) 的区别
"var"和"val"是 Kotlin 区分属性可变性的核心关键字,两者的本质区别体现在"可修改性""生成的访问器类型"及"底层实现"上,具体对比如下表:
| 对比维度 | 可变属性(var) | 只读属性(val) |
|---|---|---|
| 可变性 | 支持读取和修改操作 | 仅支持读取,初始化后不可修改 |
| 默认访问器 | 自动生成 Getter 和 Setter 方法 | 仅自动生成 Getter 方法,无 Setter 方法 |
| 底层实现(Java 视角) | 对应"私有字段 + getXxx() + setXxx()" | 对应"私有 final 字段 + getXxx()"(字段不可修改) |
| 适用场景 | 对象状态需动态更新的属性(如用户年龄、商品库存) | 对象状态固定不变的属性(如用户 ID、书籍 ISBN、创建时间) |
示例:可变属性与只读属性的定义及使用
kotlin
// 定义包含可变和只读属性的类
class User(
val userId: String, // 只读属性:用户ID一旦生成不可修改
var userName: String, // 可变属性:用户名可修改
var userAge: Int // 可变属性:年龄可更新
)
// 测试代码
fun main() {
// 创建对象并初始化属性
val user = User("U2024001", "张三", 25)
// 读取属性(隐式调用 Getter 方法)
println("用户ID:${user.userId}") // 输出:用户ID:U2024001
println("用户名:${user.userName}") // 输出:用户名:张三
// 修改可变属性(隐式调用 Setter 方法)
user.userName = "张三三"
user.userAge = 26
println("修改后用户名:${user.userName}") // 输出:修改后用户名:张三三
println("修改后年龄:${user.userAge}") // 输出:修改后年龄:26
// 尝试修改只读属性:编译报错(val 不可重新赋值)
// user.userId = "U2024002" // 错误提示:Val cannot be reassigned
}
2.3 默认访问器
Kotlin 的核心特性之一是"默认访问器自动生成",定义属性时无需像 Java 那样手动编写 Getter 和 Setter 方法,编译器会根据属性可变性自动生成对应的访问器,隐藏底层实现细节,简化开发。
默认访问器的生成规则:
- 可变属性(var):自动生成一对访问器方法------Getter 用于读取属性值,Setter 用于修改属性值。例如属性"var age: Int",会生成"getAge(): Int"和"setAge(value: Int)"方法(Java 代码中可见,Kotlin 中隐式调用);
- 只读属性(val):仅生成 Getter 方法,无 Setter 方法。例如属性"val id: String",仅生成"getId(): String"方法,确保属性无法被修改。
默认访问器的核心逻辑:Getter 方法直接返回属性对应的底层字段值,Setter 方法直接将传入的参数值赋值给底层字段,无额外业务逻辑,适用于"无需数据处理、直接读写"的常规场景。
为更直观理解默认访问器,以下是 Kotlin 代码与对应 Java 代码的对比:
java
// Java 代码:需手动定义字段和访问器(冗余)
public class JavaBook {
// 私有字段
private String title;
private double price;
// 手动编写 Getter 方法
public String getTitle() {
return title;
}
// 手动编写 Setter 方法
public void setTitle(String title) {
this.title = title;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
// 构造函数
public JavaBook(String title, double price) {
this.title = title;
this.price = price;
}
}
kotlin
// Kotlin 代码:自动生成访问器(简洁)
class KotlinBook(
var title: String, // 自动生成 Getter 和 Setter
var price: Double // 自动生成 Getter 和 Setter
)
// 编译后等价于 JavaBook 类,访问器由编译器自动生成
2.4 简单示例
综合属性定义的基础语法、可变性及默认访问器特性,通过"Book"类展示完整使用流程,包含延迟初始化和惰性初始化的应用:
kotlin
import java.util.Date
/**
* 书籍类:演示属性定义的多种场景
* isbn:只读属性(书籍唯一标识,固定不变)
* title:可变属性(书名可修改)
* author:可变属性(作者可修改)
* publishDate:延迟初始化属性(出版日期,后续赋值)
* publisher:惰性初始化属性(出版社信息,首次访问时初始化)
*/
class Book(
val isbn: String,
var title: String,
var author: String
) {
// 延迟初始化属性(非空引用类型,需手动赋值后访问)
lateinit var publishDate: Date
// 惰性初始化属性(重量级对象,首次访问时执行初始化逻辑)
val publisher: String by lazy {
println("初始化出版社信息...") // 仅首次访问时打印
"机械工业出版社"
}
// 类方法:展示书籍信息
fun showInfo() {
println("ISBN:$isbn,书名:$title,作者:$author")
println("出版日期:$publishDate,出版社:$publisher")
}
}
// 测试代码
fun main() {
// 1. 创建对象并初始化必填属性
val book = Book(
isbn = "9787111641247",
title = "Kotlin 编程实战",
author = "张三"
)
// 2. 延迟初始化属性赋值(必须赋值后才能访问)
book.publishDate = Date()
// 3. 读取和修改属性(调用默认访问器)
println("修改前书名:${book.title}") // 输出:修改前书名:Kotlin 编程实战
book.title = "Kotlin 编程实战(第2版)" // 调用 Setter 修改
println("修改后书名:${book.title}") // 输出:修改后书名:Kotlin 编程实战(第2版)
// 4. 访问惰性初始化属性(首次访问)
println("出版社:${book.publisher}") // 输出:初始化出版社信息... 机械工业出版社
// 再次访问惰性初始化属性(不执行初始化逻辑)
println("出版社:${book.publisher}") // 输出:机械工业出版社
// 5. 调用方法展示完整信息
book.showInfo()
/* 输出:
ISBN:9787111641247,书名:Kotlin 编程实战(第2版),作者:张三
出版日期:Wed Oct 11 15:30:00 CST 2024,出版社:机械工业出版社
*/
}
三、访问器原理:Getter 与 Setter 是什么?
虽然 Kotlin 隐藏了访问器的显式定义,但属性的读写操作本质上都是通过访问器完成的。深入理解 Getter 和 Setter 的工作原理,是掌握自定义访问器的基础,也是排查属性相关问题的关键。
3.1 Getter:获取属性值的方法
Getter 是用于读取属性值的特殊方法,当通过"对象.属性名"的语法读取属性时,Kotlin 编译器会自动将其转换为对 Getter 方法的调用,最终返回属性的实际值。
Getter 的核心特点:
- 触发时机:每次通过"对象.属性名"读取属性时,都会触发 Getter 方法的执行;
- 返回值:返回值类型与属性的类型完全一致,确保数据类型匹配;
- 只读属性的核心:val 类型属性仅生成 Getter 方法,无 Setter 方法,因此无法修改属性值,本质是缺少修改入口;
- 默认逻辑:默认生成的 Getter 方法直接返回属性对应的底层字段值,无额外业务逻辑。
为更清晰理解 Getter 原理,可通过反编译 Kotlin 代码查看其底层实现(以"Person"类为例):
kotlin
// Kotlin 代码
class Person(val name: String, var age: Int)
// 反编译后的 Java 代码(简化版)
public class Person {
// 只读属性对应 final 字段(不可修改)
private final String name;
// 可变属性对应普通字段(可修改)
private int age;
// 只读属性的 Getter 方法(无 Setter)
public String getName() {
return this.name;
}
// 可变属性的 Getter 方法
public int getAge() {
return this.age;
}
// 可变属性的 Setter 方法
public void setAge(int age) {
this.age = age;
}
// 构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
解析:Kotlin 中"val name = person.name"的读取操作,本质是调用了 Java 中的"person.getName()"方法,返回私有字段"name"的值,这就是 Getter 方法的底层执行逻辑。
3.2 Setter:设置属性值的方法
Setter 是用于修改属性值的特殊方法,当通过"对象.属性名 = 新值"的语法修改属性时,Kotlin 编译器会自动将其转换为对 Setter 方法的调用,将新值赋值给属性的底层字段。
Setter 的核心特点:
- 触发时机:每次通过"对象.属性名 = 新值"修改属性时,都会触发 Setter 方法的执行;
- 参数:默认接收一个名为"value"的参数,参数类型与属性类型一致,代表要设置的新值;
- 可变属性专属:仅 var 类型的可变属性会生成 Setter 方法,val 类型的只读属性无 Setter 方法,因此无法修改;
- 默认逻辑:默认生成的 Setter 方法直接将参数"value"赋值给底层字段(如"this.age = value"),无额外业务逻辑。
原理演示:Kotlin 中"person.age = 26"的修改操作,本质是调用了 Java 中的"person.setAge(26)"方法,将 26 赋值给私有字段"age",完成属性值的修改。
3.3 默认访问器的执行逻辑
当使用默认访问器时,属性的读写操作遵循固定的执行流程,整个过程由编译器自动完成,开发者无需手动干预。明确执行逻辑有助于理解属性值的变化过程,避免因执行顺序导致的问题。
3.3.1 读取属性的执行逻辑
- 开发者编写"val 变量 = 对象.属性名"的代码(如"val age = person.age");
- 编译器将代码转换为"对象.get+首字母大写属性名()"的方法调用(如"person.getAge()");
- Getter 方法执行,返回属性对应的底层字段值(如"return this.age");
- 将返回值赋值给变量(如"age = 25"),读取操作完成。
3.3.2 修改属性的执行逻辑
- 开发者编写"对象.属性名 = 新值"的代码(如"person.age = 26");
- 编译器将代码转换为"对象.set属性名(新值)"的方法调用(如"person.setAge(26)");
- Setter 方法执行,将传入的新值赋值给底层字段(如"this.age = 26");
- 属性值修改完成,Setter 方法返回,无返回值。
3.3.3 执行逻辑示例验证
kotlin
// 定义包含可变属性的类
class Counter(var count: Int)
// 测试代码:验证访问器执行逻辑
fun main() {
val counter = Counter(0)
// 1. 读取属性:触发 Getter
val oldCount = counter.count
println("修改前计数:$oldCount") // 输出:修改前计数:0
// 2. 修改属性:触发 Setter
counter.count = 10
// 3. 再次读取属性:触发 Getter
val newCount = counter.count
println("修改后计数:$newCount") // 输出:修改后计数:10
}
执行流程解析:
- "val oldCount = counter.count" → 调用"counter.getCount()" → 返回底层字段值 0 → oldCount 赋值为 0;
- "counter.count = 10" → 调用"counter.setCount(10)" → 底层字段"count"赋值为 10;
- "val newCount = counter.count" → 调用"counter.getCount()" → 返回底层字段值 10 → newCount 赋值为 10。
四、自定义访问器:灵活控制属性读写
默认访问器仅能实现"直接读写"的简单逻辑,但实际开发中常需"数据校验、动态计算、格式转换"等复杂需求。Kotlin 支持通过自定义访问器重写 Getter 或 Setter 逻辑,灵活控制属性的读写行为,适配多样化业务场景。
4.1 自定义 Getter
自定义 Getter 用于修改属性的"读取逻辑",例如根据其他属性动态计算值、对返回值进行格式化、隐藏内部实现细节等。自定义 Getter 不改变属性的可变性,仅影响读取时的返回结果。
4.1.1 语法格式
在属性定义后,通过"get()"关键字定义自定义 Getter 逻辑,根据逻辑复杂度可分为"多行逻辑"和"单行简化"两种形式,基本格式如下:
kotlin
// 1. 可变属性自定义 Getter(带默认值)
var 属性名: 数据类型 = 默认值
get() {
// 自定义读取逻辑(多行)
val result = 处理逻辑
return result
}
// 2. 只读属性自定义 Getter(无默认值,动态计算)
val 属性名: 数据类型
get() {
// 自定义读取逻辑(多行)
return 处理逻辑
}
// 3. 单行简化(逻辑简单时省略花括号和 return)
val 属性名: 数据类型
get() = 处理逻辑
语法说明:
- 自定义 Getter 是属性定义的一部分,需缩进(通常 4 个空格),与属性名对齐,保证代码可读性;
- 只读属性若通过自定义 Getter 动态计算值,可无需设置默认值,因为每次读取都会重新执行计算逻辑;
- 单行简化格式适用于逻辑简单的场景,通过"get() = 表达式"直接返回结果,简洁高效。
4.1.2 简单示例
示例 1:动态计算属性值(根据其他属性推导结果)
kotlin
/**
* 矩形类:通过宽和高动态计算面积和周长
* width:宽(可变属性)
* height:高(可变属性)
* area:面积(只读计算属性,宽 * 高)
* perimeter:周长(只读计算属性,2*(宽+高))
*/
class Rectangle(var width: Double, var height: Double) {
// 自定义 Getter:动态计算面积(多行逻辑)
val area: Double
get() {
println("计算面积:width = $width, height = $height")
return width * height
}
// 自定义 Getter:动态计算周长(单行简化)
val perimeter: Double
get() = 2 * (width + height)
}
// 测试代码
fun main() {
val rectangle = Rectangle(5.0, 3.0)
// 首次读取面积:触发自定义 Getter,执行计算逻辑
println("矩形面积:${rectangle.area}") // 输出:计算面积:width = 5.0, height = 3.0 → 矩形面积:15.0
// 修改宽和高后再次读取:重新执行计算逻辑
rectangle.width = 6.0
println("修改后面积:${rectangle.area}") // 输出:计算面积:width = 6.0, height = 3.0 → 修改后面积:18.0
// 读取周长:触发单行简化 Getter
println("矩形周长:${rectangle.perimeter}") // 输出:矩形周长:18.0
}
解析:面积"area"和周长"perimeter"均为只读计算属性,无底层存储字段,每次读取时都会执行自定义 Getter 逻辑重新计算,确保返回结果与最新的宽高保持一致。
示例 2:返回值格式化(隐藏内部存储格式,统一对外输出)
解析:内部通过 Long 类型存储时间戳(节省存储空间且便于计算),对外通过自定义 Getter 提供"yyyy-MM-dd"格式的日期字符串,隐藏了内部存储细节,同时统一了对外的展示格式。需注意:SimpleDateFormat 非线程安全,实际开发推荐使用 java.time 包下的 DateTimeFormatter。
解析:内部通过 Long 类型存储时间戳(节省存储空间且便于计算),对外通过自定义 Getter 提供"yyyy-MM-dd"格式的日期字符串,隐藏了内部存储细节,同时统一了对外的展示格式。
4.2 自定义 Setter
自定义 Setter 用于修改属性的"修改逻辑",例如对传入的新值进行合法性校验(过滤非法值)、格式转换(统一值的格式)、触发关联操作(修改一个属性时同步更新其他属性)等。自定义 Setter 仅适用于可变属性(var),只读属性(val)无 Setter 可自定义。
4.2.1 语法格式
在属性定义后,通过"set(value)"关键字定义自定义 Setter 逻辑,核心是通过"field"关键字引用属性的底层字段,避免递归调用。基本格式如下:
kotlin
var 属性名: 数据类型 = 默认值
set(value) {
// 自定义修改逻辑(对 value 进行处理)
// field 关键字:引用属性的底层字段,避免递归调用
field = 处理后的 value
}
// 单行简化(逻辑简单时)
var 属性名: 数据类型 = 默认值
set(value) = if (条件) field = value else 处理逻辑
语法说明:
- value 参数:默认参数名,代表要设置的新值,类型与属性类型一致,可显式指定其他参数名;
- field 关键字:用于引用属性对应的底层字段,必须显式使用。若直接写"属性名 = value",会触发 Setter 方法的递归调用,导致死循环;
- 自定义 Setter 仅能修改底层字段的值,无法改变属性的可变性和数据类型。
4.2.2 简单示例
示例 1:数据合法性校验(过滤非法值,保证属性值有效)
kotlin
/**
* 学生类:对年龄和成绩进行合法性校验
* age:年龄(1-150 之间,非法值设为默认 18)
* score:成绩(0-100 之间,非法值抛出异常)
*/
class Student(var name: String) {
// 自定义 Setter:年龄校验(1-150 之间)
var age: Int = 18
set(value) {
// 校验逻辑:若输入值合法则赋值,否则使用默认值 18
val validAge = if (value in 1..150) value else 18
field = validAge
println("设置年龄:输入 $value → 实际设置 $validAge")
}
// 自定义 Setter:成绩校验(0-100 之间,非法值抛异常)
var score: Double = 60.0
set(value) {
// 使用 require 函数校验,非法值抛出 IllegalArgumentException
require(value in 0.0..100.0) { "成绩必须在 0-100 之间,当前输入:$value" }
field = value
println("设置成绩:$value 分(合法)")
}
}
// 测试代码
fun main() {
val student = Student("张三")
// 设置合法年龄
student.age = 20
// 输出:设置年龄:输入 20 → 实际设置 20
println("当前年龄:${student.age}") // 输出:当前年龄:20
// 设置非法年龄(超过上限)
student.age = 200
// 输出:设置年龄:输入 200 → 实际设置 18
println("当前年龄:${student.age}") // 输出:当前年龄:18
// 设置合法成绩
student.score = 85.5
// 输出:设置成绩:85.5 分(合法)
println("当前成绩:${student.score}") // 输出:当前成绩:85.5
// 设置非法成绩(超过上限,抛出异常)
try {
student.score = 105.0
} catch (e: IllegalArgumentException) {
println("设置成绩失败:${e.message}")
// 输出:设置成绩失败:成绩必须在 0-100 之间,当前输入:105.0
}
}
解析:通过自定义 Setter 对年龄和成绩进行校验,确保属性值始终合法------年龄非法时使用默认值,成绩非法时抛出异常,避免对象处于无效状态,提升代码健壮性。
示例 2:格式转换(统一属性值格式,避免数据混乱)
kotlin
/**
* 商品类:统一商品名称格式(首字母大写,去除空格)
* name:商品名称(设置时自动格式化)
*/
class Product {
// 自定义 Setter:商品名称格式转换
var name: String = ""
set(value) {
// 处理逻辑:去除前后空格 → 首字母大写 → 其余小写
val formattedName = value.trim()
.takeIf { it.isNotBlank() } // 非空判断
?.replaceFirstChar { it.uppercase() } // 首字母大写
?.let { it.first() + it.substring(1).lowercase() } // 其余小写
?: "" // 空值处理
field = formattedName
println("设置名称:输入 '$value' → 实际设置 '$formattedName'")
}
}
// 测试代码
fun main() {
val product = Product()
// 输入带空格的全小写名称
product.name = " apple phone "
// 输出:设置名称:输入 ' apple phone ' → 实际设置 'Apple phone'
println("商品名称:${product.name}") // 输出:商品名称:Apple phone
// 输入全大写名称
product.name = "COMPUTER"
// 输出:设置名称:输入 'COMPUTER' → 实际设置 'Computer'
println("商品名称:${product.name}") // 输出:商品名称:Computer
// 输入空值或纯空格
product.name = " "
// 输出:设置名称:输入 ' ' → 实际设置 ''
println("商品名称:${product.name}") // 输出:商品名称:
}
解析:无论外部输入何种格式的商品名称,自定义 Setter 都会将其转换为"首字母大写、其余小写、无前后空格"的标准化格式,保证数据格式一致性,便于后续数据处理和展示。
4.3 计算属性
计算属性(Computed Property)是一种特殊的属性,它没有底层存储字段,仅通过自定义 Getter(或 Setter)实现读写逻辑。计算属性的核心特征是"值不存储,动态计算",每次访问时都会重新执行 Getter 逻辑得到结果,适用于"值由其他属性推导"的场景。
4.3.1 核心特征
- 无底层字段:计算属性不占用对象的存储空间,没有对应的"field"字段,因此在自定义访问器中无法使用"field"关键字;
- 动态计算:每次访问计算属性时,都会重新执行 Getter 逻辑,返回最新的计算结果,与依赖的属性实时同步;
- 可变性分类: 只读计算属性(val):仅需自定义 Getter,通过其他属性推导结果,最常用;
- 可变计算属性(var):需同时自定义 Getter 和 Setter,实现"读写联动"(如温度转换场景)。
4.3.2 示例:只读与可变计算属性
kotlin
/**
* 温度转换类:演示计算属性的读写联动
* celsius:摄氏度(存储字段,可变属性)
* fahrenheit:华氏度(可变计算属性,与摄氏度联动)
* isBoiling:是否沸腾(只读计算属性,基于摄氏度判断)
*/
class Temperature(var celsius: Double) {
// 可变计算属性:华氏度(℃ ↔ ℉ 双向转换)
var fahrenheit: Double
get() {
// 摄氏度转华氏度:℃ × 1.8 + 32
val value = celsius * 1.8 + 32
println("计算华氏度:$celsius ℃ → $value ℉")
return value
}
set(value) {
// 华氏度转摄氏度:(℉ - 32) ÷ 1.8,赋值给存储字段 celsius
celsius = (value - 32) / 1.8
println("设置华氏度:$value ℉ → 转换为 $celsius ℃")
}
// 只读计算属性:是否沸腾(标准大气压下,℃ ≥ 100 为沸腾)
val isBoiling: Boolean
get() = celsius >= 100.0
}
// 测试代码
fun main() {
val temperature = Temperature(25.0)
// 访问只读计算属性(是否沸腾)
println("25℃ 是否沸腾:${temperature.isBoiling}") // 输出:false
// 访问可变计算属性(华氏度)
println("华氏度:${temperature.fahrenheit}")
// 输出:计算华氏度:25.0 ℃ → 77.0 ℉ → 华氏度:77.0
// 修改计算属性(华氏度),触发 Setter 转换为摄氏度
temperature.fahrenheit = 98.6
// 输出:设置华氏度:98.6 ℉ → 转换为 37.0 ℃
println("修改后摄氏度:${temperature.celsius}") // 输出:37.0
// 修改存储字段(摄氏度),再次访问计算属性
temperature.celsius = 100.0
println("100℃ 是否沸腾:${temperature.isBoiling}") // 输出:true
println("100℃ 对应的华氏度:${temperature.fahrenheit}")
// 输出:计算华氏度:100.0 ℃ → 212.0 ℉ → 100℃ 对应的华氏度:212.0
}
解析:
- fahrenheit(可变计算属性):无底层字段,Getter 将 celsius 转换为华氏度,Setter 将华氏度转换为 celsius 并赋值给存储字段,实现"修改华氏度同步更新摄氏度"的联动效果;
- isBoiling(只读计算属性):无底层字段,每次访问时都会判断 celsius 是否≥100,实时反映温度状态。
五、实用场景举例
结合实际开发中的典型场景,展示属性及自定义访问器的灵活应用,帮助读者理解"何时需要自定义访问器"及"如何设计合理的属性结构"。
5.1 自定义 Getter 场景
5.1.1 场景 1:动态计算对象状态
当对象的某个状态需要根据多个属性动态推导时,使用自定义 Getter 实现,避免状态不一致。例如"订单类"根据"支付状态"和"发货状态",动态判断"订单总状态"(未支付、已支付待发货、已发货、已完成)。
kotlin
/**
* 订单类:动态计算订单总状态
* orderNo:订单号(只读,固定不变)
* isPaid:是否支付(可变)
* isShipped:是否发货(可变)
* isReceived:是否收货(可变)
* status:订单总状态(只读计算属性,动态推导)
*/
class Order(val orderNo: String) {
var isPaid: Boolean = false // 是否支付
var isShipped: Boolean = false // 是否发货
var isReceived: Boolean = false // 是否收货
// 自定义 Getter:根据三个状态动态推导订单总状态
val status: String
get() = when {
!isPaid -> "未支付"
isPaid && !isShipped -> "已支付待发货"
isPaid && isShipped && !isReceived -> "已发货待收货"
isPaid && isShipped && isReceived -> "已完成"
else -> "未知状态"
}
// 展示订单信息
fun showInfo() {
println("订单号:$orderNo,总状态:$status")
println("支付状态:${if (isPaid) "已支付" else "未支付"}")
println("发货状态:${if (isShipped) "已发货" else "未发货"}")
println("收货状态:${if (isReceived) "已收货" else "未收货"}")
}
}
// 测试代码
fun main() {
val order = Order("20240501001")
// 初始状态:未支付
println("=== 初始状态 ===")
order.showInfo()
/* 输出:
订单号:20240501001,总状态:未支付
支付状态:未支付
发货状态:未发货
收货状态:未收货
*/
// 支付后:已支付待发货
order.isPaid = true
println("\n=== 支付后 ===")
order.showInfo()
// 发货后:已发货待收货
order.isShipped = true
println("\n=== 发货后 ===")
order.showInfo()
// 收货后:已完成
order.isReceived = true
println("\n=== 收货后 ===")
order.showInfo()
}
5.1.2 场景 2:隐藏内部复杂逻辑
当属性的读取需要复杂逻辑(如数据拼接、加密解密、数据库查询等)时,使用自定义 Getter 隐藏内部实现,对外提供简洁的属性访问方式。例如"用户类"通过自定义 Getter 拼接"姓名+手机号后4位"作为显示名称,同时对手机号进行脱敏处理。
kotlin
import java.security.MessageDigest
import java.util.Base64
/**
* 用户类:隐藏复杂逻辑,对外提供简洁访问
* realName:真实姓名(存储字段)
* phone:手机号(存储字段,加密存储)
* displayName:显示名称(计算属性:姓名+手机号后4位)
* hiddenPhone:脱敏手机号(计算属性:中间4位用*代替)
*/
class User(val realName: String, private val phone: String) {
// 自定义 Getter:拼接显示名称(隐藏手机号截取逻辑)
val displayName: String
get() {
// 解密手机号(实际开发中需结合加密算法)
val decryptedPhone = decryptPhone(phone)
// 截取后4位
val last4Digits = decryptedPhone.takeLast(4)
return "$realName($last4Digits)"
}
// 自定义 Getter:手机号脱敏(隐藏脱敏逻辑)
val hiddenPhone: String
get() {
val decryptedPhone = decryptPhone(phone)
if (decryptedPhone.length != 11) return "无效手机号"
// 脱敏:中间4位用*代替
return decryptedPhone.substring(0, 3) + "****" + decryptedPhone.substring(7)
}
// 私有方法:模拟手机号解密(实际开发中使用真实加密算法)
private fun decryptPhone(encryptedPhone: String): String {
// 此处仅为演示,实际需使用 AES、RSA 等加密算法
return String(Base64.getDecoder().decode(encryptedPhone))
}
// 静态方法:模拟手机号加密(供外部创建对象时使用)
companion object {
fun encryptPhone(phone: String): String {
return Base64.getEncoder().encodeToString(phone.toByteArray())
}
}
}
// 测试代码
fun main() {
// 加密手机号(模拟外部传入加密后的数据)
val encryptedPhone = User.encryptPhone("13800138000")
// 创建用户对象
val user = User("张三", encryptedPhone)
// 访问显示名称:无需关心解密和拼接逻辑
println("显示名称:${user.displayName}") // 输出:显示名称:张三(8000)
// 访问脱敏手机号:无需关心解密和脱敏逻辑
println("脱敏手机号:${user.hiddenPhone}") // 输出:脱敏手机号:138****8000
}
5.2 自定义 Setter 场景
5.2.1 场景 1:数据合法性校验(最常用)
在接收用户输入、接口数据等外部数据时,通过自定义 Setter 对属性值进行校验,确保数据合法,避免无效数据进入系统。例如"员工类"对"工资""入职年份"进行校验,防止输入不合理值。
kotlin
import java.util.Calendar
/**
* 员工类:对关键属性进行合法性校验
* name:姓名(只读)
* salary:工资(必须 > 0)
* hireYear:入职年份(必须 ≥ 公司成立年份 2000,且 ≤ 当前年份)
*/
class Employee(val name: String) {
// 公司成立年份(常量)
private val companyFoundedYear = 2000
// 当前年份(动态获取)
private val currentYear = Calendar.getInstance().get(Calendar.YEAR)
// 自定义 Setter:工资校验(必须 > 0)
var salary: Double = 0.0
set(value) {
require(value > 0) { "工资必须大于 0,当前输入:$value" }
field = value
}
// 自定义 Setter:入职年份校验
var hireYear: Int = currentYear
set(value) {
require(value >= companyFoundedYear) {
"入职年份不能早于公司成立年份 $companyFoundedYear,当前输入:$value"
}
require(value <= currentYear) {
"入职年份不能晚于当前年份 $currentYear,当前输入:$value"
}
field = value
}
// 展示员工信息
fun showInfo() {
println("姓名:$name,工资:${String.format("%.2f", salary)} 元,入职年份:$hireYear")
}
}
// 测试代码
fun main() {
val employee = Employee("李四")
// 设置合法数据
employee.salary = 8000.0
employee.hireYear = 2020
println("=== 合法数据 ===")
employee.showInfo() // 输出:姓名:李四,工资:8000.00 元,入职年份:2020
// 设置非法工资(抛出异常)
try {
employee.salary = -5000.0
} catch (e: IllegalArgumentException) {
println("\n设置工资失败:${e.message}") // 输出:设置工资失败:工资必须大于 0,当前输入:-5000.0
}
// 设置非法入职年份(早于公司成立年份)
try {
employee.hireYear = 1990
} catch (e: IllegalArgumentException) {
println("设置入职年份失败:${e.message}")
// 输出:设置入职年份失败:入职年份不能早于公司成立年份 2000,当前输入:1990
}
}
5.2.2 场景 2:值格式统一转换
当属性值有固定格式要求(如日期格式、字符串大小写、集合去重等)时,通过自定义 Setter 对输入值进行格式转换,确保存储格式统一。例如"文章类"对"标题"进行大小写规范、对"发布时间"进行统一格式转换,对"标签列表"进行去重和标准化处理。
kotlin
import java.text.SimpleDateFormat
import java.util.*
import java.util.stream.Collectors
/**
* 文章类:统一属性格式,保证数据规范性
* title:文章标题(首字母大写,去除多余空格)
* publishTimeStr:发布时间字符串(转换为统一 Date 类型存储)
* tags:标签列表(去重、统一小写、去除空格)
* publishTime:标准化发布时间(Date 类型,供内部使用)
*/
class Article {
// 统一日期格式
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
// 文章标题:格式统一
var title: String = ""
set(value) {
field = value.trim()
.takeIf { it.isNotBlank() }
?.replaceFirstChar { if (it.isLowerCase()) it.uppercase() else it }
?: "未命名文章"
}
// 发布时间字符串:转换为 Date 类型存储
var publishTimeStr: String = dateFormat.format(Date())
set(value) {
val formattedDate = try {
// 尝试按标准格式解析
dateFormat.parse(value)
} catch (e: Exception) {
// 解析失败则使用当前时间
Date()
}
publishTime = formattedDate
field = dateFormat.format(formattedDate)
}
// 标签列表:去重、标准化
var tags: List<String> = emptyList()
set(value) {
field = value.stream()
.map { it.trim().lowercase() } // 统一小写、去空格
.filter { it.isNotBlank() } // 过滤空标签
.distinct() // 去重
.collect(Collectors.toList())
}
// 内部存储的标准化发布时间
var publishTime: Date = Date()
// 展示文章信息
fun showInfo() {
println("标题:$title")
println("发布时间:$publishTimeStr")
println("标签:${if (tags.isNotEmpty()) tags.joinToString("、") else "无标签"}")
}
}
// 测试代码
fun main() {
val article = Article()
// 设置不规范的属性值
article.title = " kotlin 自定义访问器 实战 "
article.publishTimeStr = "2024-10-15 10:30" // 非标准格式(缺少秒)
article.tags = listOf("Kotlin", " getter", "setter", "kotlin", " GETTER ")
println("=== 标准化后的文章信息 ===")
article.showInfo()
/* 输出:
标题:Kotlin 自定义访问器 实战
发布时间:2024-10-15 10:30:00
标签:kotlin、getter、setter
*/
}
5.2.3 场景 3:触发关联操作
当修改一个属性时,需要同步更新其他属性或执行特定业务逻辑(如日志记录、缓存更新、通知发送等),可通过自定义 Setter 实现关联操作。例如"购物车类"中,修改商品数量时同步计算"小计金额"和"总金额",并记录修改日志。
kotlin
/**
* 购物车项类:修改数量时触发关联计算
* productName:商品名称
* price:单价(只读)
* quantity:购买数量(修改时同步计算小计)
* subtotal:小计金额(数量 × 单价)
*/
class CartItem(val productName: String, val price: Double) {
// 购买数量
var quantity: Int = 1
set(value) {
// 校验数量合法性
val validQty = if (value < 1) 1 else if (value > 100) 100 else value
val oldQty = field
field = validQty
// 触发关联操作:更新小计、记录日志
updateSubtotal()
logQuantityChange(oldQty, validQty)
}
// 小计金额
var subtotal: Double = price
private set // 私有 Setter,仅内部可修改
// 同步更新小计金额
private fun updateSubtotal() {
subtotal = price * quantity
}
// 记录数量修改日志
private fun logQuantityChange(oldQty: Int, newQty: Int) {
println("[日志] 商品 '$productName' 数量变更:$oldQty → $newQty")
}
}
/**
* 购物车类:管理多个购物车项,计算总金额
* items:购物车项列表
* totalAmount:总金额(随购物车项变化而更新)
*/
class ShoppingCart {
val items: MutableList<CartItem> = mutableListOf()
// 总金额:动态计算所有项小计之和
val totalAmount: Double
get() = items.sumOf { it.subtotal }
// 添加商品到购物车
fun addItem(item: CartItem) {
items.add(item)
println("[日志] 商品 '${item.productName}' 已加入购物车")
}
// 展示购物车信息
fun showCart() {
println("\n=== 购物车详情 ===")
items.forEachIndexed { index, item ->
println("${index + 1}. 商品:${item.productName},单价:${String.format("%.2f", item.price)} 元,数量:${item.quantity},小计:${String.format("%.2f", item.subtotal)} 元")
}
println("==================")
println("购物车总金额:${String.format("%.2f", totalAmount)} 元")
}
}
// 测试代码
fun main() {
val cart = ShoppingCart()
val item1 = CartItem("Kotlin 编程书籍", 59.9)
val item2 = CartItem("机械键盘", 299.0)
// 添加商品到购物车
cart.addItem(item1)
cart.addItem(item2)
// 修改商品数量(触发关联计算)
item1.quantity = 3
item2.quantity = 150 // 超过上限,自动调整为 100
// 展示购物车详情
cart.showCart()
/* 输出:
[日志] 商品 'Kotlin 编程书籍' 已加入购物车
[日志] 商品 '机械键盘' 已加入购物车
[日志] 商品 'Kotlin 编程书籍' 数量变更:1 → 3
[日志] 商品 '机械键盘' 数量变更:1 → 100
=== 购物车详情 ===
1. 商品:Kotlin 编程书籍,单价:59.90 元,数量:3,小计:179.70 元
2. 商品:机械键盘,单价:299.00 元,数量:100,小计:29900.00 元
==================
购物车总金额:30079.70 元
*/
}
5.3 计算属性场景
5.3.1 场景 1:单位转换
当需要在不同单位之间灵活转换时,使用计算属性实现"读写联动",修改一个单位的值时自动同步另一个单位的值。例如"长度类"支持"米"和"厘米"之间的双向转换,修改任意一个单位都会实时更新另一个。
kotlin
/**
* 长度类:支持米和厘米双向转换
* meter:米(基础单位,存储字段)
* centimeter:厘米(计算属性,与米联动)
*/
class Length(var meter: Double) {
// 厘米:计算属性,1米 = 100厘米
var centimeter: Double
get() = meter * 100
set(value) {
meter = value / 100
}
// 展示长度信息
fun showLength() {
println("长度:${String.format("%.2f", meter)} 米 = ${String.format("%.2f", centimeter)} 厘米")
}
}
// 测试代码
fun main() {
val length = Length(2.5)
// 初始状态
length.showLength() // 输出:长度:2.50 米 = 250.00 厘米
// 修改米数,同步更新厘米
length.meter = 3.8
length.showLength() // 输出:长度:3.80 米 = 380.00 厘米
// 修改厘米数,同步更新米
length.centimeter = 520.5
length.showLength() // 输出:长度:5.21 米 = 520.50 厘米
}
5.3.2 场景 2:动态统计信息
当需要实时统计对象的某些状态(如集合大小、平均值、最大值等)时,使用计算属性避免手动更新统计结果,确保数据实时准确。例如"学生成绩类"中,通过计算属性实时统计"总分""平均分""最高分"。
kotlin
/**
* 学生成绩类:实时统计成绩信息
* name:学生姓名
* scores:各科成绩列表(支持动态添加/修改)
* totalScore:总分(所有成绩之和)
* averageScore:平均分(总分/科目数,保留两位小数)
* maxScore:最高分(成绩列表中的最大值)
*/
class StudentScore(val name: String) {
// 各科成绩列表(可变)
val scores: MutableList<Double> = mutableListOf()
// 总分:计算属性
val totalScore: Double
get() = scores.sum()
// 平均分:计算属性
val averageScore: Double
get() = if (scores.isEmpty()) 0.0 else String.format("%.2f", totalScore / scores.size).toDouble()
// 最高分:计算属性
val maxScore: Double
get() = scores.maxOrNull() ?: 0.0
// 添加成绩
fun addScore(score: Double) {
if (score in 0.0..100.0) {
scores.add(score)
println("已添加成绩:$score 分")
} else {
println("成绩 $score 无效,需在 0-100 之间")
}
}
// 展示成绩统计
fun showStatistics() {
println("\n=== ${name} 的成绩统计 ===")
println("各科成绩:${scores.joinToString("、")}")
println("总分:$totalScore 分")
println("平均分:$averageScore 分")
println("最高分:$maxScore 分")
}
}
// 测试代码
fun main() {
val studentScore = StudentScore("王五")
// 添加成绩
studentScore.addScore(85.5)
studentScore.addScore(92.0)
studentScore.addScore(78.5)
studentScore.addScore(105.0) // 无效成绩
// 展示统计信息
studentScore.showStatistics()
/* 输出:
已添加成绩:85.5 分
已添加成绩:92.0 分
已添加成绩:78.5 分
成绩 105.0 无效,需在 0-100 之间
=== 王五 的成绩统计 ===
各科成绩:85.5、92.0、78.5
总分:256.0 分
平均分:85.33 分
最高分:92.0 分
*/
// 再添加一门成绩,统计信息实时更新
studentScore.addScore(90.0)
studentScore.showStatistics()
}
六、避坑要点与优化技巧
6.1 常见避坑要点
6.1.1 避免 Getter/Setter 递归调用
在自定义访问器中,若直接使用"属性名"进行读写,会触发自身的访问器调用,导致无限递归。核心解决方法是使用 field 关键字引用底层字段,仅在计算属性(无底层字段)中可省略 field。
kotlin
// 错误示例:Setter 中直接使用属性名导致递归
class Person {
var age: Int = 0
set(value) {
// 错误:age = value 会再次触发 Setter,陷入死循环
age = if (value > 0) value else 18
}
}
// 正确示例:使用 field 引用底层字段
class Person {
var age: Int = 0
set(value) {
// 正确:field 直接操作底层字段,不触发访问器
field = if (value > 0) value else 18
}
}
6.1.2 区分计算属性与普通属性
计算属性无底层存储,每次访问都会重新计算,若计算逻辑复杂(如循环、网络请求),会导致性能问题;普通属性(含自定义访问器)有底层字段,仅在访问器逻辑中执行计算。避免将复杂逻辑写入计算属性的 Getter 中。
kotlin
// 不推荐:计算属性 Getter 包含复杂逻辑
class DataAnalyzer(val data: List<Int>) {
// 每次访问都会执行排序和去重,性能开销大
val sortedUniqueData: List<Int>
get() = data.sorted().distinct()
}
// 推荐:普通属性 + 初始化时计算(仅执行一次)
class DataAnalyzer(val data: List<Int>) {
// 初始化时计算并存储结果,后续访问直接返回
val sortedUniqueData: List<Int> = data.sorted().distinct()
}
6.1.3 谨慎使用 lateinit 与 lazy
lateinit 和 lazy 虽能解决延迟初始化问题,但使用不当会引发异常,需注意适用场景和约束条件:
- lateinit 约束:仅适用于 var 修饰的非空引用类型(如 String、自定义类),基本数据类型需通过`Delegates.notNull
- lazy 约束 :仅适用于 val 修饰的属性;初始化逻辑默认线程安全(可通过
LazyThreadSafetyMode调整);初始化后不可修改,若需动态更新,应使用普通属性。
kotlin
// lateinit 正确使用示例
class UserService {
lateinit var userRepository: UserRepository
// 初始化方法(如依赖注入后调用)
fun init(repo: UserRepository) {
userRepository = repo
}
fun getUser(id: String): User? {
// 检查初始化状态,避免异常
return if (::userRepository.isInitialized) {
userRepository.findById(id)
} else {
null
}
}
}
// lazy 线程安全调整示例
class Config {
// 单线程场景下,使用 NONE 模式提升性能
val appConfig: Map<String, String> by lazy(LazyThreadSafetyMode.NONE) {
loadConfigFromFile() // 从文件加载配置
}
// 补充配置加载方法实现
private fun loadConfigFromFile(): Map<String, String> {
// 实际开发中实现文件读取逻辑
return mapOf("timeout" to "3000", "maxRetry" to "3")
}
}
// 补充必要的类型定义
class UserRepository {
fun findById(id: String): User? = User(id, "默认用户")
}
data class User(val id: String, val name: String)
6.1.4 避免在访问器中执行耗时操作
访问器(尤其是 Getter)的设计初衷是"快速读写属性",若在其中执行耗时操作(如数据库查询、网络请求、大文件读写),会导致属性访问时阻塞,影响用户体验和系统性能。耗时操作应封装为独立方法,显式调用。
kotlin
// 不推荐:Getter 中执行耗时操作
class User(val userId: String) {
// 访问时执行数据库查询,可能阻塞线程
val userDetails: UserDetails
get() = userDao.queryDetails(userId)
}
// 推荐:封装为独立方法,显式调用
class User(val userId: String) {
// 显式方法,表明存在耗时操作
suspend fun getUserDetails(): UserDetails {
return userDao.queryDetails(userId)
}
}
// 补充必要的类型定义(实际开发中需根据业务实现)
class UserDetails
object UserDao {
suspend fun queryDetails(userId: String): UserDetails = UserDetails()
}
6.2 优化技巧
6.2.1 合理使用私有访问器
对于无需外部修改但需内部更新的属性,可将 Setter 设为私有(private set),既能保证外部只读性,又能避免属性被意外修改,提升封装性。
kotlin
class Order(val orderNo: String) {
// 订单状态:外部只读,内部可修改
var status: String = "未支付"
private set
// 内部方法修改状态
fun pay() {
if (status == "未支付") {
status = "已支付"
}
}
}
// 外部使用
fun main() {
val order = Order("2024001")
println(order.status) // 可读:输出 未支付
order.pay() // 通过方法修改
// order.status = "已取消" // 编译报错:外部无法修改私有 Setter 的属性
}
6.2.2 缓存计算属性结果(针对复杂逻辑)
若计算属性的逻辑复杂但依赖的数据源不频繁变化,可通过"普通属性 + 监听依赖变化"的方式缓存计算结果,避免每次访问都重新计算,提升性能。
kotlin
class ProductAnalyzer(var products: List<Product>) {
// 缓存计算结果的普通属性
private var _averagePrice: Double? = null
// 计算属性:优先使用缓存,缓存失效时重新计算
val averagePrice: Double
get() {
if (_averagePrice == null) {
_averagePrice = products.map { it.price }.average()
}
return _averagePrice!!
}
// 当依赖数据源变化时,清空缓存
fun updateProducts(newProducts: List<Product>) {
products = newProducts
_averagePrice = null // 缓存失效
}
}
// 补充必要的类型定义
data class Product(val price: Double)
6.2.3 利用 const 优化编译期常量
对于编译期即可确定值的常量(如固定的字符串、数值),使用 const val 替代普通 val,可定义在对象、伴生对象或顶层,编译时会将其替换为字面量,避免运行时属性访问的开销,且支持在注解中使用。
kotlin
// 普通常量:运行时访问属性
class AppConstants {
val API_BASE_URL = "https://api.example.com"
}
// 编译期常量:编译时替换为字面量
object AppConstants {
const val API_BASE_URL = "https://api.example.com"
}
// 支持在注解中使用(Retrofit 实际注解示例)
import retrofit2.http.GET
@retrofit2.http.BaseUrl(AppConstants.API_BASE_URL)
interface ApiService {
@GET("/data")
suspend fun fetchData(): Response<Data>
}
// 补充必要的类型定义
class Response<T>
class Data
6.2.4 简化访问器逻辑(提取公共方法)
若多个属性的访问器逻辑存在重复(如相同的格式校验、转换规则),应将重复逻辑提取为私有方法,在访问器中调用,提升代码复用性和可维护性。
kotlin
class UserData {
// 提取公共的字符串格式化方法
private fun formatString(value: String): String {
return value.trim()
.takeIf { it.isNotBlank() }
?: "未知"
}
// 多个属性复用格式化逻辑
var username: String = ""
set(value) { field = formatString(value) }
var nickname: String = ""
set(value) { field = formatString(value) }
var address: String = ""
set(value) { field = formatString(value) }
}
七、总结
7.1 核心知识点梳理
Kotlin 类的属性与自定义访问器是面向对象编程的核心,其核心知识点可归纳为"一个本质、两类属性、两种访问器、三大场景":
- 一个本质:Kotlin 属性是"底层字段 + 访问器(Getter/Setter)"的封装体,访问属性本质是调用访问器方法。
- 两类属性:可变属性(var)支持读写,自动生成 Getter 和 Setter;只读属性(val)仅支持读,仅生成 Getter。
- 两种访问器:默认访问器实现简单读写,无需手动编写;自定义访问器可重写逻辑,实现数据校验、动态计算等复杂需求。
- 三大场景:自定义 Getter 用于动态计算、结果格式化;自定义 Setter 用于数据校验、格式转换、关联操作;计算属性用于无存储的动态推导场景。
7.2 最佳实践建议
- 优先使用默认访问器:常规读写场景下,默认访问器已满足需求,避免过度自定义导致代码冗余。
- 明确属性可变性:优先使用 val 定义只读属性,仅在确需修改时使用 var,减少对象状态变化带来的复杂度。
- 合理选择初始化方式:即时初始化适用于值明确的场景;lateinit 适用于非空引用类型的延迟赋值;lazy 适用于 val 修饰的重量级对象延迟初始化。
- 保持访问器轻量化:访问器中仅执行简单逻辑,耗时操作或复杂业务逻辑封装为独立方法。
- 强化封装性:通过私有字段、私有访问器隐藏内部实现,对外提供简洁的访问接口。
7.3 学习展望
掌握属性与自定义访问器后,可进一步学习 Kotlin 中与属性相关的高级特性,深化对面向对象编程的理解:
- 委托属性 :通过
by关键字将属性的访问逻辑委托给其他对象,实现属性的复用(如 lazy 本质是委托属性)。 - 数据类与密封类的属性特性:数据类自动生成基于属性的 equals、hashCode 等方法;密封类的属性可用于约束子类状态。
- 属性注解与反射 :通过注解标记属性(如
@SerializedName),结合反射实现属性的序列化/反序列化。
Kotlin 的属性设计兼顾了简洁性与灵活性,合理运用属性与访问器,能大幅提升代码的可读性、健壮性和可维护性,是 Kotlin 开发的核心技能之一。