Kotlin 类与对象:open 不是白写的,构造函数也没你想的那么简单
面向对象是 Kotlin 最重要的特性之一,但它的规则和 Java 有很大区别。这篇文章帮你理清类、构造函数、继承、接口的核心知识点。
1. 类定义:不用 new
class Person {
var name = "张三"
fun sayHello() = println("你好,我是 $name")
}
// 创建对象:不用 new
val person = Person()
person.name = "李四"
person.sayHello()
2. 构造函数:Kotlin 的精髓
主构造函数(最常用)
// 写在类名后面
class Person(val name: String, var age: Int) {
// 类体
}
// 使用
val person = Person("张三", 25)
println(person.name) // 张三
println(person.age) // 25
关键点:主构造函数的参数加 val/var,自动成为类的属性。
init 代码块
主构造函数不能写逻辑,用 init:
class Person(name: String, age: Int) {
val name = name
var age = age
init {
println("初始化:name=$name, age=$age")
}
}
次构造函数
class Person(val name: String) {
var age = 0
// 次构造函数:必须调用主构造
constructor(name: String, age: Int) : this(name) {
this.age = age
println("次构造执行")
}
}
// 使用
val p1 = Person("张三") // 调用主构造
val p2 = Person("李四", 25) // 调用次构造
实战建议:能用主构造函数 + 默认参数解决的,绝不用次构造函数。
// 最佳实践:用默认参数替代次构造函数
class Person(
val name: String,
var age: Int = 0 // 默认参数
)
// 调用灵活
Person("张三") // age 默认 0
Person("李四", 25) // 指定 age
3. getter 和 setter:属性访问器
Kotlin 的属性自动生成 getter/setter,但你可以自定义:
class Person {
var name = "张三"
// 自定义 getter
get() = field.uppercase()
// 自定义 setter
set(value) {
field = value.trim()
}
// 只读属性:只有 getter
val birthYear: Int
get() = 2024 - age
var age = 0
set(value) {
if (value < 0) field = 0
else field = value
}
}
field 是什么:幕后字段,代表属性真正存储的值。在 getter/setter 中必须用 field,不能用属性名(会死循环)。
4. open 关键字:继承的核心
这是 Kotlin 和 Java 最大的区别:
// Java:类默认可以继承
// Kotlin:类默认是 final,禁止继承!
// 想被继承?必须加 open!
open class Person(val name: String) {
open fun sayHello() = println("你好")
}
// 继承:子类加括号调用父构造
class Student(name: String, val grade: Int) : Person(name) {
// 重写方法:必须加 override
override fun sayHello() {
super.sayHello()
println("我是学生,班级:$grade")
}
}
规则总结:
表格
| 场景 | Java | Kotlin |
|---|---|---|
| 类是否可继承 | 默认可以 | 默认 final,需加 open |
| 方法是否可重写 | 默认可以 | 默认 final,需加 open |
| 抽象方法 | 需 abstract | 默认 open,无需重复加 |
5. 继承与接口:统一用冒号
// 继承类:加括号(调用父构造)
class Student : Person()
// 实现接口:不加括号(接口没构造)
class Runner : Runnable
// 同时继承 + 实现
class StudentAthlete : Person(), Runnable, Serializable
口诀:继承类必须加括号,实现接口不加括号。
6. 接口:规则更灵活
interface Person {
// 抽象属性
val name: String
// 抽象方法
fun work()
// 默认实现:可选重写
fun rest() = println("休息中...")
}
// 实现接口
class Teacher(override val name: String) : Person {
override fun work() = println("教书")
}
接口 vs 抽象类:
表格
| 特性 | 接口 | 抽象类 |
|---|---|---|
| 构造 | 无 | 有 |
| 属性 | 默认抽象,不能初始化 | 可以有具体属性 |
| 多实现 | ✅ 可以 | ❌ 不行 |
| 继承 | 接口可以继承接口 | 只能单继承 |
7. object:单例和匿名对象
单例对象
object AppConfig {
var appName = "我的应用"
fun init() = println("初始化")
}
// 使用:直接类名调用
AppConfig.init()
AppConfig.appName = "新名字"
匿名内部类
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
// 处理点击
}
})
// 简化:SAM 转换
button.setOnClickListener { /* 处理点击 */ }
伴生对象:替代 static
class User {
val name: String
// 伴生对象:相当于 Java 的 static
companion object {
const val TAG = "User"
fun create() = User("默认用户")
}
constructor(name: String) {
this.name = name
}
}
// 使用
val user = User.create()
Log.d(User.TAG, "创建用户")
8. 数据类:快速定义 POJO
data class User(
val id: Long,
val name: String,
val age: Int = 0
)
// 自动生成:
// - equals/hashCode
// - toString()
// - copy()
// - componentN() 函数(解构)
// 使用
val user1 = User(1, "张三", 25)
val user2 = user1.copy(name = "李四") // 只改 name
val (id, name, _) = user1 // 解构
9. 内部类:持有外部引用
class Outer {
private val outerVar = 1
// 嵌套类:不持有外部引用
class Nested {
fun test() = 2 // 不能访问 outerVar
}
// 内部类:持有外部引用(加 inner)
inner class Inner {
fun test() = outerVar // 可以访问
}
}
常见问题
Q:为什么 Kotlin 类默认是 final?
A:遵循《Effective Java》的最佳实践——「要么为继承而设计,要么禁止继承」。默认 final 避免脆弱基类问题,提升代码安全性。
Q:override 的方法可以继续被重写吗?
A:可以。Kotlin 的 override 方法默认是 open 的。如果想禁止重写,加 final override。
总结
表格
| 特性 | 关键点 |
|---|---|
| 构造函数 | 主构造 + init,次构造必须调用主构造,优先用默认参数 |
| getter/setter | 自动生成,可用 field 自定义 |
| open | 类/方法默认 final,想被继承必须加 open |
| 继承实现 | 统一用冒号,继承类加括号,接口不加 |
| object | 单例对象、匿名对象、伴生对象 |
| 数据类 | 自动生成常用方法,copy、解构 |