9.Kotlin 类:类的核心:属性 (Property) 与自定义访问器 (Getter/Setter)

希望帮你在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 方法,编译器会根据属性可变性自动生成对应的访问器,隐藏底层实现细节,简化开发。

默认访问器的生成规则:

  1. 可变属性(var):自动生成一对访问器方法------Getter 用于读取属性值,Setter 用于修改属性值。例如属性"var age: Int",会生成"getAge(): Int"和"setAge(value: Int)"方法(Java 代码中可见,Kotlin 中隐式调用);
  2. 只读属性(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 读取属性的执行逻辑

  1. 开发者编写"val 变量 = 对象.属性名"的代码(如"val age = person.age");
  2. 编译器将代码转换为"对象.get+首字母大写属性名()"的方法调用(如"person.getAge()");
  3. Getter 方法执行,返回属性对应的底层字段值(如"return this.age");
  4. 将返回值赋值给变量(如"age = 25"),读取操作完成。

3.3.2 修改属性的执行逻辑

  1. 开发者编写"对象.属性名 = 新值"的代码(如"person.age = 26");
  2. 编译器将代码转换为"对象.set属性名(新值)"的方法调用(如"person.setAge(26)");
  3. Setter 方法执行,将传入的新值赋值给底层字段(如"this.age = 26");
  4. 属性值修改完成,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 最佳实践建议

  1. 优先使用默认访问器:常规读写场景下,默认访问器已满足需求,避免过度自定义导致代码冗余。
  2. 明确属性可变性:优先使用 val 定义只读属性,仅在确需修改时使用 var,减少对象状态变化带来的复杂度。
  3. 合理选择初始化方式:即时初始化适用于值明确的场景;lateinit 适用于非空引用类型的延迟赋值;lazy 适用于 val 修饰的重量级对象延迟初始化。
  4. 保持访问器轻量化:访问器中仅执行简单逻辑,耗时操作或复杂业务逻辑封装为独立方法。
  5. 强化封装性:通过私有字段、私有访问器隐藏内部实现,对外提供简洁的访问接口。

7.3 学习展望

掌握属性与自定义访问器后,可进一步学习 Kotlin 中与属性相关的高级特性,深化对面向对象编程的理解:

  • 委托属性 :通过 by 关键字将属性的访问逻辑委托给其他对象,实现属性的复用(如 lazy 本质是委托属性)。
  • 数据类与密封类的属性特性:数据类自动生成基于属性的 equals、hashCode 等方法;密封类的属性可用于约束子类状态。
  • 属性注解与反射 :通过注解标记属性(如 @SerializedName),结合反射实现属性的序列化/反序列化。

Kotlin 的属性设计兼顾了简洁性与灵活性,合理运用属性与访问器,能大幅提升代码的可读性、健壮性和可维护性,是 Kotlin 开发的核心技能之一。

相关推荐
kerli1 小时前
Android:使用 Tint 为图标 Icon 动态着色
android
回家路上绕了弯1 小时前
接口 QPS 从 100 飙到 1000?从应急到根治的全流程优化方案
分布式·后端
hqk1 小时前
鸿蒙零基础语法入门:开启你的开发之旅
android·前端·harmonyos
The_cute_cat2 小时前
通过内网穿透为课设临时添加域名访问【springboot+Vue】
vue.js·spring boot·后端
howcode2 小时前
女友去玩,竟带回一道 “虐哭程序员” 的难题
后端·设计模式·程序员
z***94842 小时前
SpringBoot教程(三十二) SpringBoot集成Skywalking链路跟踪
spring boot·后端·skywalking
程序员西西2 小时前
SpringBoot 数据存储实战拆解!
后端
QuantumLeap丶2 小时前
《Flutter全栈开发实战指南:从零到高级》- 17 -核心动画
android·flutter·ios
辜月十2 小时前
NSSM服务
后端