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)主构造函数的参数加 val 或 var 会自动生成同名属性,省掉了 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关键概念:
- val:只读属性(只有 getter)。
- var:可变属性(有 getter 和 setter)。
- field:幕后字段(backing field),getter/setter 内部用,避免无限递归。
- 计算属性:没有幕后字段,每次访问重新计算,getter 必须返回值。
对比 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 / innerdata class 自动生成:
- equals / hashCode:基于所有构造参数比较。
- toString:格式
User(name=小明, age=20),调试友好。 - copy:基于现有对象,只修改指定字段。
- componentN:用于解构声明
val (name, age) = user。
实际项目里,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() // 也可以伴生对象本身是一个真实的对象(在类加载时初始化),里面的成员通过类名直接访问。常见用途:
- 常量:
const val DEFAULT_AGE = 18。 - 工厂方法:
User.create(...),可以替代主构造函数的可见性限制。 - 缓存或共享状态:整个类共享。
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 继承 →