Kotlin 类与对象

类是面向对象编程(OOP)的核心——把数据(属性)和行为(方法)封装成一个整体。Kotlin 的类语法在 Java 基础上做了大幅简化:主构造函数、属性语法、data class、object 单例、companion object,让常见的样板代码消失。本篇逐个讲。

1. 类基础

class 关键字声明,类体用花括号包裹:

// 类声明:class + 名字 + 类体
class Person {
    // 成员属性(必须初始化,或用 lateinit / 委托)
    val name: String = "小明"
    var age: Int = 25

    // 成员函数(方法)
    fun greet() {
        println("你好,我是 $name")
    }
}

// 使用:val 实例 = 类名()
val p = Person()       // 注意:Kotlin 没有 new 关键字
p.greet()              // 你好,我是 小明
println(p.age)         // 25
p.age = 26             // 修改属性

关键差异:Kotlin 创建实例没有 new 关键字——直接 Person() 就行,像调用函数一样。这是 Kotlin 简洁的体现之一。

2. 主构造函数 + 次构造函数

Kotlin 把构造函数分两种:主构造(primary,只有一个,紧跟类名)和次构造(secondary,可以有多个):

// 主构造函数:紧跟类名后的 ()
class Person(val name: String, var age: Int)
// val/var 自动生成同名属性

// 使用
val p = Person("小明", 25)
println(p.name)
println(p.age)

// 带可见性的主构造函数
class User private constructor(val id: Int)  // private,不能直接 new

// 次构造函数(constructor 关键字)
class Person(val name: String) {
    var age: Int = 0

    // 次构造函数:必须委托给主构造函数(直接或间接)
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

val p1 = Person("小明")
val p2 = Person("小红", 22)

主构造函数的参数加 valvar自动生成同名属性,省掉了 Java 的"字段 + 构造参数 + this.字段 = 参数"三件套。注意次构造函数必须委托给主构造函数(this(...)),保证主构造一定被调用。

3. init 块(初始化逻辑)

主构造函数不能有代码体,初始化逻辑放 init 块:

// init 块:构造时执行,可以有多个,按声明顺序执行
class User(name: String) {
    val upperName: String
    val length: Int

    init {
        println("初始化 User: $name")
        upperName = name.uppercase()
    }

    init {
        length = upperName.length
        println("名字长度: $length")
    }
}

val u = User("xiaoming")
// 输出:
// 初始化 User: xiaoming
// 名字长度: 8

// init 块主要用于:参数校验、计算派生属性
class Email(address: String) {
    init {
        require(address.contains("@")) {
            "邮箱地址必须包含 @"
        }
    }
}

init 块可以有多个,按声明顺序执行。属性初始化和 init 块也是按声明顺序执行的——所以后面的 init 可以用到前面已初始化的属性。

常见用途:参数校验(require / check)、计算派生属性、注册到全局表、打日志。

4. 属性(Property)

Kotlin 没有 Java 那种"字段 + 显式 getter + 显式 setter"的啰嗦写法,而是用属性语法统一:

// 属性:Kotlin 的"字段+getter+setter" 一体化
class Person(name: String) {
    // val:只读属性(只有 getter)
    val name: String = name

    // var:可变属性(有 getter 和 setter)
    var age: Int = 0

    // 自定义 getter / setter
    var email: String = ""
        get() = field.lowercase()           // field 是幕后字段
        set(value) {
            require(value.contains("@")) { "邮箱无效" }
            field = value
        }

    // 计算属性(无幕后字段)
    val isAdult: Boolean
        get() = age >= 18
}

val p = Person("Xiao")
p.age = 25
p.email = "XIAO@Email.com"
println(p.email)               // xiao@email.com(自动转小写)
println(p.isAdult)             // true

关键概念:

对比 Java:val name: String 一行就代替了 Java 的 private final String name; public String getName() { return name; } 三行。Kotlin 自动生成等价的 getter(setter for var),你可以随时加自定义 getter/setter 而不破坏调用方(p.name 不变)。

5. 可见性修饰符

// 可见性修饰符
// public   :默认,所有地方可见
// private  :只在本类(或本文件)可见
// protected:本类 + 子类可见(不能用于顶层声明)
// internal :同模块可见(Kotlin 特有)

class BankAccount(initial: Double) {
    public val owner: String = "默认"      // 显式 public
    private var balance: Double = initial   // 私有,外部不可见
    internal val currency: String = "CNY"   // 同模块可见

    fun deposit(amount: Double) {
        if (amount > 0) balance += amount
    }

    fun getBalance(): Double = balance      // 通过公共方法暴露

    private fun log(msg: String) {
        println("[LOG] $msg")
    }
}

// 构造函数可见性
class Secret private constructor(val id: Int)
// 通过 companion object 工厂方法创建

关键:Kotlin 默认是 public(Java 默认 package-private,容易出错)。多了个 internal,只在同一个模块内可见——非常适合库开发,你想暴露给本模块但不暴露给使用者时用。

注意 Kotlin 的 protected 比 Java 更严格:只在本类和子类可见,同包不可见(Java 的 protected 同包可见是历史包袱)。

6. 数据类(data class,招牌特性)

这是 Kotlin 最受欢迎的特性之一。Java 写一个 POJO(只有字段的类)要写几十行:getter、setter、equals、hashCode、toString、构造函数。Kotlin 一行搞定:

// 数据类(data class):专门装数据,自动生成 equals/hashCode/toString/copy
data class User(
    val name: String,
    val age: Int,
    val email: String? = null
)

val u1 = User("小明", 20)
val u2 = User("小明", 20)

println(u1 == u2)             // true(自动 equals 比较内容)
println(u1.toString())        // User(name=小明, age=20, email=null)

// copy:基于现有对象,修改部分字段
val u3 = u1.copy(age = 21)
println(u3)                   // User(name=小明, age=21, email=null)

// 解构声明:把字段拆出来
val (name, age) = u1
println("$name $age")          // 小明 20

// 数据类要求:
// - 主构造函数至少有 1 个参数
// - 参数必须用 val/var
// - 不能是 abstract / open / sealed / inner

data class 自动生成:

实际项目里,data class 用于DTO、API 响应、数据库行、领域模型等"装数据"的场景。业务逻辑多的类用普通 class,装数据的类用 data class。

7. object 单例(替代 Java 的 static 单例)

Kotlin 用 object 关键字直接声明单例,替代 Java 的"private 构造 + static getInstance"模式:

// object 单例:Kotlin 用 object 关键字声明单例
object Database {
    val url = "jdbc:postgresql://localhost/db"

    fun connect() {
        println("连接到 $url")
    }

    fun query(sql: String): List<String> {
        return listOf("结果1", "结果2")
    }
}

// 使用:直接通过 object 名调用,无需创建实例
Database.connect()
val results = Database.query("SELECT * FROM users")

// object 替代了 Java 的"private 构造 + static getInstance" 模式
// 线程安全(由 JVM 类加载机制保证)

object 声明的类只有一个实例,通过类名直接访问成员。线程安全(由 JVM 类加载机制保证)。

还有一种用法——对象表达式(object :),类似 Java 的匿名内部类,常用于回调:

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        println("clicked at ${e.x}, ${e.y}")
    }
})

8. companion object(伴生对象,类级静态)

Kotlin 没有 Java 的 static 关键字——类内的"静态"成员通过 companion object(伴生对象)实现:

// companion object:类内部的"静态"成员
class User(val name: String) {

    // 伴生对象,里面的成员类似 Java 的 static
    companion object {
        const val DEFAULT_AGE = 18

        // 工厂方法
        fun create(name: String): User = User(name)

        // 也可以有自己的属性和方法
        private var counter = 0
        fun totalCreated() = counter
    }
}

// 使用:通过类名访问伴生对象成员
val u = User.create("小明")
println(User.DEFAULT_AGE)        // 18
println(User.totalCreated())     // 0(这里没增加)

// companion object 也能实现接口、有名字
class Config {
    companion object Factory {
        fun load() = Config()
    }
}
Config.load()
Config.Factory.load()            // 也可以

伴生对象本身是一个真实的对象(在类加载时初始化),里面的成员通过类名直接访问。常见用途:

9. 枚举类(enum class)

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF);

    // 枚举也可以有方法和属性
    fun isPrimary() = this == RED || this == GREEN || this == BLUE
}

val c = Color.RED
println(c)                  // RED
println(c.rgb)              // 16711680
println(c.isPrimary())      // true
println(c.name)             // "RED"
println(c.ordinal)          // 0

// 遍历所有值
Color.values().forEach { println(it) }
// 配合 when 做穷尽匹配
val desc = when (c) {
    Color.RED -> "红"
    Color.GREEN -> "绿"
    Color.BLUE -> "蓝"
}

10. 密封类(sealed class,重点)

密封类限定子类数量(只能在同一个文件或同一包内声明),配合 when 做穷尽匹配——编译器强制你处理所有分支:

sealed class Result

data class Success(val data: String) : Result()
data class Failure(val msg: String) : Result()
object Loading : Result()

fun handle(r: Result): String = when (r) {
    is Success -> "成功: ${r.data}"        // 智能转换
    is Failure -> "失败: ${r.msg}"
    Loading -> "加载中..."
    // 不需要 else!编译器知道所有可能
}

println(handle(Success("hello")))    // 成功: hello
println(handle(Loading))              // 加载中...

密封类的优势:新加子类时,所有 when 表达式会编译报错,提醒你补上新分支。这避免了 Java/TS 里"忘记处理新 case"的 bug。在表达状态机、API 结果、UI 状态时极为好用。

11. 实战:一个简单的银行账户类

class BankAccount(
    val owner: String,
    initialBalance: Double = 0.0
) {
    private var balance: Double = initialBalance
        private set                                  // setter 私有,外部不能直接改

    val isOverdrawn: Boolean
        get() = balance < 0

    fun deposit(amount: Double) {
        require(amount > 0) { "金额必须为正" }
        balance += amount
    }

    fun withdraw(amount: Double): Boolean {
        require(amount > 0) { "金额必须为正" }
        return if (balance >= amount) {
            balance -= amount
            true
        } else {
            false
        }
    }

    override fun toString() = "BankAccount(owner=$owner, balance=$balance)"
}

fun main() {
    val acc = BankAccount("小明", 100.0)
    acc.deposit(50.0)
    println(acc)                  // BankAccount(owner=小明, balance=150.0)
    val ok = acc.withdraw(200.0)
    println("取款成功: $ok")      // 取款成功: false
    println(acc.isOverdrawn)      // false
}

这个例子综合了主构造、属性、私有 setter、计算属性、require 校验、toString 重写。注意 balance 的 setter 私有,只能通过 deposit/withdraw 修改——封装的精髓。

小结

这一章你掌握了 Kotlin 面向对象的核心:类声明、主/次构造、init 块、属性(含自定义 getter/setter)、可见性、data class、object 单例、companion object、enum、sealed class。下一篇看 Kotlin 的继承——类的多态与抽象。

← 上一篇 Kotlin 集合

下一篇 Kotlin 继承

✈️💬