第9章:类与对象——类声明、构造函数、init块、属性与字段

类与对象,这是Kotlin面向对象编程的基石。说实话,我见过不少从Java转过来的朋友,一开始写Kotlin类时还是带着Java的思维习惯。嗯,这很正常,但Kotlin其实给了我们更简洁、更安全的写法。今天我们就来彻底搞懂它。

9.1 类声明:最基础的写法

Kotlin里声明一个类,用class关键字就行。比Java简单不少。

class Person

这就完了?对,这就完了。没有public class,没有大括号。Kotlin默认就是public的。如果你需要加修饰符,可以这样:

// 私有类,只在当前文件可见
private class SecretData

// 开放继承的类
open class BaseEntity

我个人习惯是,能不加修饰符就不加。Kotlin的设计哲学就是「少即是多」。你想想看,Java里那些public写了几十年,其实大部分时候都是默认的,何必呢?

9.2 构造函数:主构造 vs 次构造

这是Kotlin和Java最大的区别之一。Kotlin把构造函数分成了两种:主构造函数次构造函数

9.2.1 主构造函数

主构造函数直接写在类名后面,这是最常用的方式。

class Person(firstName: String, lastName: String)

如果你需要给参数加默认值,也很自然:

class Person(
    val firstName: String = "张",
    val lastName: String = "三"
)

我在项目中遇到过一种情况:有人把主构造函数的参数写得特别长,七八个参数挤在一行。说实话,可读性很差。我建议每个参数单独一行,就像上面那样。

小技巧:主构造函数参数前面加valvar,会自动生成对应的属性。不加的话,它只是一个构造参数,外部无法访问。

9.2.2 次构造函数

次构造函数用constructor关键字声明。它必须直接或间接地委托给主构造函数。

class Person(val name: String) {
    // 次构造函数
    constructor(name: String, age: Int) : this(name) {
        println("年龄: $age")
    }
}

为什么要这样设计?说白了,Kotlin希望所有初始化逻辑都走主构造函数那条路,这样更统一、更安全。我曾经见过一个项目,次构造函数里写了大量逻辑,结果主构造函数和次构造函数的初始化顺序乱成一团。嗯,后来重构时全改成了主构造函数+init块的方式。

注意:如果类有主构造函数,次构造函数必须委托给它。如果类没有主构造函数(类名后没跟括号),次构造函数就不需要委托。

9.3 init初始化块

init块是Kotlin特有的。它会在对象创建时执行,而且是在主构造函数之后、次构造函数之前执行。

class Person(val name: String) {
    init {
        require(name.isNotBlank()) { "名字不能为空" }
        println("Person对象创建,名字: $name")
    }
}

你可以在类里写多个init块,它们会按从上到下的顺序执行。

class Person(val name: String) {
    init {
        println("第一个init块")
    }
    
    init {
        println("第二个init块")
    }
}

为什么会需要多个init块?我个人习惯是把校验逻辑放在第一个init块,把日志或统计逻辑放在后面。这样职责清晰,也方便维护。

9.4 属性与字段

Kotlin的属性声明非常简洁。用val声明只读属性,用var声明可变属性。

class Person {
    val id: String = "未知"  // 只读
    var age: Int = 0         // 可变
}

但这里有个细节:Kotlin的属性背后其实有一个幕后字段(backing field),用field关键字访问。你平时写代码时基本感觉不到它的存在,但一旦你自定义getter/setter,它就会冒出来。

class Person {
    var age: Int = 0
        set(value) {
            if (value >= 0) {
                field = value  // 这里必须用field,不能用age
            }
        }
}

为什么不能用age = value?因为那样会递归调用setter,造成无限循环。我在项目中就见过这样的bug,排查了半天才发现是setter里直接赋值了自己。

9.5 Getter/Setter:自定义访问器

Kotlin的getter和setter是隐式的。你声明一个属性,它就自动有了getter和(如果是var)setter。但你也可以自定义它们。

9.5.1 自定义Getter

class Rectangle(val width: Int, val height: Int) {
    val isSquare: Boolean
        get() = width == height
}

这个isSquare没有幕后字段,它每次访问时都计算一次。说白了,它是一个「计算属性」。

9.5.2 自定义Setter

class Person {
    var name: String = ""
        set(value) {
            field = value.trim()  // 自动去除前后空格
        }
}

我在项目中经常用自定义setter来做数据清洗。比如用户输入的手机号、邮箱,在setter里统一格式化,这样业务层就不用每次都处理了。

核心要点:
  • getter/setter中访问属性本身必须用field,否则会递归
  • val属性只有getter,没有setter
  • 自定义getter/setter不会改变属性的使用方式,外部调用者无感知

9.6 知识体系图

下面这张图总结了类与对象的核心结构,你可以把它当作一个快速参考。

Kotlin 类与对象核心结构 类声明 构造函数 init 初始化块 属性与字段 Getter / Setter 主构造函数 次构造函数 幕后字段 (field) 计算属性 (getter) 初始化顺序:主构造函数 → init块 → 次构造函数 属性访问:getter/setter 内部必须使用 field 避免递归

9.7 实战:一个完整的例子

把上面这些知识点串起来,写一个实际项目中常见的用户类:

class User(
    val id: String,
    var name: String,
    var email: String
) {
    // 校验逻辑
    init {
        require(id.isNotBlank()) { "用户ID不能为空" }
        require(name.isNotBlank()) { "用户名不能为空" }
    }
    
    // 计算属性:邮箱的脱敏显示
    val maskedEmail: String
        get() {
            val atIndex = email.indexOf('@')
            if (atIndex <= 1) return email
            return "${email[0]}***${email.substring(atIndex)}"
        }
    
    // 自定义setter:自动格式化邮箱为小写
    var formattedEmail: String = email.lowercase()
        set(value) {
            field = value.trim().lowercase()
        }
    
    // 次构造函数:支持从数据库记录创建
    constructor(record: Map<String, String>) : this(
        id = record["id"] ?: "",
        name = record["name"] ?: "",
        email = record["email"] ?: ""
    ) {
        println("从数据库记录创建用户: $name")
    }
}

这个例子涵盖了主构造函数、init块、计算属性、自定义setter和次构造函数。我在项目中写用户类时,基本就是这个模板。你想想看,如果每个属性都手动写getter/setter,那得多累?Kotlin帮你省掉了这些模板代码。

我的建议:能用主构造函数解决的问题,就不要用次构造函数。能用计算属性解决的问题,就不要用幕后字段。Kotlin的设计就是让你少写代码,多关注业务。

好了,关于类与对象的核心内容就这些。记住一句话:Kotlin的类设计,核心是「简洁」和「安全」。主构造函数+init块+属性的组合,已经能覆盖90%的场景。剩下的10%,用次构造函数和自定义getter/setter来补充。

公众号:蓝海资料掘金营,微信deep3321