类与接口
Kotlin 重新审视了传统的面向对象设计,通过引入主构造函数、默认 final 设计以及接口属性,极简地表达了对象模型,同时避免了许多 Java 中常见的继承陷阱。
类的声明与构造
主构造函数
在 Kotlin 中,构造函数直接紧跟在类名后面。
class User(val name: String, var age: Int)这行代码等价于 Java 中:声明字段 + 构造函数赋值 + Getter + Setter。
- init 初始化块:主构造函数不能包含代码,初始化逻辑需放在
init关键字包裹的代码块中。 - 属性声明:如果在主构造函数参数前加上
val或var,该参数将自动成为类的属性;否则它仅仅是一个构造函数参数(除非在init或属性初始化中使用了它)。
初始化顺序
在类实例化期间,属性初始化器和 init 块是按照它们在类体中出现的顺序执行的。
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints $name")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}避免前向引用
确保你的 init 块只访问那些“已经初始化”的属性,否则可能导致空指针异常或未定义行为。
属性与幕后字段
在 Kotlin 中,属性 (Property) 是全功能的:它包含字段 (Field)、Getter 和 Setter。
传统的幕后字段
在自定义访问器时,使用 field 标识符来访问生成的幕后字段,以避免递归调用。
var counter = 0
set(value) {
if (value >= 0) field = value // 使用 field 更新值
// counter = value // ❌ 错误:这会递归调用 setter,导致 StackOverflow
}显式幕后字段 Kotlin 2.3.0
在 Kotlin 2.3.0 中,引入了显式幕后字段语法,用于解决“对外只读、对内可变”的常见需求,特别是在处理 StateFlow 或集合时。
在此之前,通常需要定义一个私有属性 _prop 和一个公有只读属性 prop。现在可以直接在属性内部声明 field 关键字。
启用配置 该特性目前处于预览阶段,需要在 build.gradle.kts 中配置编译器参数:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}语法演进
private val _city = MutableStateFlow<String>("")
val city: StateFlow<String> get() = _city
fun updateCity(newCity: String) {
_city.value = newCity
}val city: StateFlow<String>
field = MutableStateFlow("")
fun updateCity(newCity: String) {
// 智能转换自动生效,在类内部 field 被视为 MutableStateFlow
city.value = newCity
}核心优势
这种语法消除了为了封装而引入的 _city 命名污染,使逻辑更加内聚,同时编译器能够自动处理属性与其幕后字段之间的类型转换。
属性的高级特性
1. 延迟初始化 (Lateinit)
通常,非空属性必须在构造函数中初始化。但在依赖注入(DI)或单元测试场景中,我们无法立即提供值。此时可以使用 lateinit var。
class MyService {
lateinit var provider: Provider // 稍后注入
fun perform() {
if (::provider.isInitialized) { // 检查是否已初始化
provider.action()
}
}
}限制条件
- 只能用于
var属性。 - 属性类型不能是原生类型(如
Int,Boolean),必须是引用类型。 - 不能有自定义 getter/setter。
2. Setter 可见性控制
如果你不需要显式幕后字段那样复杂的逻辑,仅仅想实现“对外只读,对内可写”,最简单的办法是改变 setter 的可见性。
class User {
var name: String = "Init"
private set // 外部只能读取,不能修改
fun changeName(newName: String) {
name = newName // 类内部可以修改
}
}3. 编译时常量 (Const Val)
const val 声明的是编译时常量(Compile-time constant),它的值在编译期就确定了,并会被内联到调用处。
const val MAX_COUNT = 100 // 必须位于顶层或 object/companion object 中
class Demo {
// const val INSIDE = 1 // ❌ 错误:不能在类内部声明
}const val vs val:
val:运行时常量(类似于 Java 的final变量),可以由函数计算得出。const val:编译时常量(类似于 Java 的public static final常量),只能是 String 或原生类型。
继承与多态
Kotlin 遵循“为继承而设计,否则就禁止”的原则,类和成员默认都是 final 的。
open 与 override
- open:必须显式使用
open关键字,类才能被继承,函数才能被重写。 - abstract:抽象类和抽象成员默认是
open的。
open class Base {
open fun v() {}
fun nv() {} // 默认为 final,子类不可重写
}
class Derived : Base() {
override fun v() {}
}接口
Kotlin 的接口支持抽象方法、默认实现和抽象属性。
interface MyInterface {
val prop: Int // 抽象属性,实现类必须覆盖它并提供存储方式
val propertyWithImplementation: String
get() = "foo" // 提供访问器的属性,没有幕后字段
fun foo() {
print(prop) // 默认方法体
}
}接口与抽象类的区别
- 状态:接口不能保存状态(即不能有幕后字段
field),它的属性要么是抽象的,要么通过自定义 getter 计算得出。抽象类可以保存状态。 - 继承:类只能继承一个父类,但可以实现多个接口。
解决覆盖冲突
当实现多个接口且存在同名方法时:
interface A { fun foo() { print("A") } }
interface B { fun foo() { print("B") } }
class C : A, B {
override fun foo() {
super<A>.foo() // 显式调用 A 的实现
super<B>.foo() // 显式调用 B 的实现
}
}函数式接口 (SAM)
只有一个抽象方法的接口称为函数式接口。使用 fun interface 声明后,可以通过 Lambda 表达式进行实例化。
fun interface KRunnable {
fun invoke()
}
// 通过 Lambda 直接实例化
val r = KRunnable { println("Run!") }类型别名 (Type Aliases)
typealias 用于为现有类型提供一个更短或更具语义的替代名称。它不会创建新类型,编译器会在编译时将其替换为底层类型。
// 缩短长泛型
typealias NodeSet = Set<Network.Node>
// 为函数类型提供语义名称
typealias ClickHandler = (View, Int) -> Unit
// 结合 import 使用,解决类名冲突
// import com.example.MyClass as OldClass // 这是 import alias,不同于 typealias与值类的区别
typealias 不提供类型安全(NodeSet 仅仅是 Set<...> 的别名,可以互换使用)。如果需要强类型安全(防止参数混淆),请使用 值类。
可见性修饰符
- private:类内部可见。
- protected:类及其子类可见。在 Kotlin 中,此类成员对同包下的其他类不可见。
- internal:模块(Module)内可见。这是组件化开发的核心控制符。
- public(默认):随处可见。
总结
- 构造顺序:属性初始化器与
init块按声明顺序执行。 - 显式字段:利用 Kotlin 2.3.0 特性精简代码(需显式开启配置)。
- 严格继承:默认 final,提倡组合优于继承。
- Internal:利用模块级可见性实现良好的封装。