第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 = "三"
)
我在项目中遇到过一种情况:有人把主构造函数的参数写得特别长,七八个参数挤在一行。说实话,可读性很差。我建议每个参数单独一行,就像上面那样。
val或var,会自动生成对应的属性。不加的话,它只是一个构造参数,外部无法访问。
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 知识体系图
下面这张图总结了类与对象的核心结构,你可以把它当作一个快速参考。
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的类设计,核心是「简洁」和「安全」。主构造函数+init块+属性的组合,已经能覆盖90%的场景。剩下的10%,用次构造函数和自定义getter/setter来补充。
公众号:蓝海资料掘金营,微信deep3321