重学Kotlin(四)面向对象
一、Kotlin 的类构造器
Kotlin 的类构造器(Constructors)分为 主构造器 和 次构造器 ,再加上 初始化块 init,一起组成完整的对象初始化流程。
1. 主构造器(Primary Constructor)
最常用、最简洁的构造方法,直接写在类名后面:
kotlin
class User(val name: String, val age: Int)
特点:
- 定义在类名后面的小括号中
- 默认是
public - 参数如果加
val/var会自动生成对应字段 - 不能包含逻辑(逻辑写在 init 中)
2. init 初始化块
主构造器不能写逻辑,所以逻辑写在 init 里:
kotlin
class User(val name: String, val age: Int) {
init {
println("User created: $name, $age")
}
}
执行顺序:主构造器参数 -> init 块
3. 次构造器(Secondary Constructor)
写在类内部,用 constructor 关键字:
kotlin
class User(val name: String) {
var age: Int = 0
constructor(name: String, age: Int) : this(name) {
this.age = age
}
}
特点:
- 用
constructor(...) - 必须最终调用主构造器(如果有主构造器)
- 可以包含逻辑
4. 没有主构造器时
kotlin
class User {
var name: String = ""
var age: Int = 0
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
}
如果没有主构造器,则 次构造器不需要调用 this()
5. 主构造器与默认值
最常用的写法:
kotlin
class User(
val name: String,
val age: Int = 18
)
这样可以直接:
kotlin
val u = User("Tom") // age = 18
val u2 = User("Tom", 20)
Kotlin 强烈推荐这种写法代替多个构造器重载,更简洁。
6. 主构造器可以私有化
常用于工厂方法:
kotlin
class User private constructor(val name: String) {
companion object {
fun create(name: String) = User(name)
}
}
DCL 双重检查锁定单例:
kotlin
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance(): Singleton =
instance ?: synchronized(this) {
instance ?: Singleton().also { instance = it }
}
}
}
二、Kotlin 的可见性
| 修饰符 | 类内部 | 子类 | 同一文件 | 同一模块 | 其他模块 |
|---|---|---|---|---|---|
| public(默认) | ✔ | ✔ | ✔ | ✔ | ✔ |
| internal | ✔ | ✔ | ✔ | ✔ | ✘ |
| protected | ✔ | ✔ | ✘ | ✘ | ✘ |
| private | ✔ | ✘ | ✔ | ✘ | ✘ |
与 Java 差异:
(1)internal
表示模块内可访问 例如一个 Gradle module 不同 module 之间不能访问 internal。
kotlin
internal class ApiClient
(2)顶级声明可见性
顶级声明指的是在文件内直接定义类、函数、属性等。
- 顶级声明不支持
protected,顶级声明没有父类概念,所以 Kotlin 语义不允许顶级使用protected - 顶级声明被
private修饰,表示文件内可见
三、Kotlin 属性
1. Kotlin 属性是什么?
Kotlin 中 属性 = 字段(field) + getter/setter 方法 的组合。
kotlin
class User {
var name: String = "Tom" // 属性
val age: Int = 18 // 只读属性
}
var可读写val只读(相当于final+ getter)
Kotlin 不像 Java,需要手动写 getter/setter,编译器自动生成。
2. 自定义 getter / setter
kotlin
class User {
var name: String = "Tom"
get() = field.uppercase() // 自定义 getter
set(value) { field = value.trim() } // 自定义 setter
}
field= 实际存储的字段- getter/setter 可以加逻辑
- val 只能写 getter(不能写 setter)
3. 常量属性
kotlin
const val VERSION = "1.0" // 顶层常量
- 必须是顶层或 companion object 内
- 类型必须是基本类型或 String
- 编译期常量,可在 Java 中访问
4. 延迟初始化属性
(1)初始化为null
kotlin
var name: String? = null
fun init(name: String?) {
this.name = name
name?.substring(0, 1)
}
不推荐,写法等价 Java 代码,使用的时候需要判断空指针。
(2)使用lateinit
kotlin
class User {
lateinit var token: String
fun initToken() {
token = "xxx"
}
}
- 只能用于 var
- 非原始类型(非 Int/Long 等)
- 使用前未初始化会报异常
UninitializedPropertyAccessException - 适用于:依赖注入(DI)、开发者完全确认变量生命周期的场景
(3)lazy
本质上用到的是属性代理
kotlin
val data: String by lazy {
println("lazy init")
"Hello"
}
kotlin
// 相关源码
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
...
override val value: T
get() {
...
return synchronized(lock) {
...
val typedValue = initializer!!() // 调用传入的初始化函数
_value = typedValue
initializer = null
typedValue
}
}
}
- 只读 val
- 第一次访问时初始化
- 默认线程安全
四、代理 Delegate
1. 属性代理
上文已经介绍了 lazy 属性代理,本质是代理了 getter。 下面介绍 Kotlin 自带的另外几种属性代理,都在 Delegates 类下。
(1)observable ------ 可观察属性
- 类型 :可变
var - 作用:属性值变化时回调
- 常用场景:UI 数据绑定、监控状态变化、日志记录
kotlin
import kotlin.properties.Delegates
var name: String by Delegates.observable("Tom") { prop, old, new ->
println("$old -> $new")
}
fun main() {
name = "Jerry" // 输出:Tom -> Jerry
name = "Spike" // 输出:Jerry -> Spike
}
(2)vetoable ------ 可阻止修改的属性
- 类型 :可变
var - 作用:属性值变化时通过 lambda 判断是否允许修改
- 返回 true → 修改属性;false → 拒绝修改
- 常用场景:值校验、约束属性、范围限制
kotlin
var age: Int by Delegates.vetoable(0) { prop, old, new ->
new >= 0
}
fun main() {
age = 10 // OK
age = -5 // 被拒绝,age 仍为 10
}
(3)notNull ------ 非空延迟初始化(var)
- 类型 :可变
var - 作用:属性在使用前必须初始化,否则抛异常
- 常用场景:依赖注入(DI)、View 或 late binding
kotlin
var data: String by Delegates.notNull()
fun main() {
// println(data) // ❌ 抛异常
data = "Hello"
println(data) // ✅ Hello
}
(4)自定义代理
可实现接口 ReadWriteProperty 或 ReadOnlyProperty 实现自定义代理:
kotlin
class CustomDelegate(var value: String = "") : ReadWriteProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Delegate $value"
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = "new $value"
}
}
var name: String by CustomDelegate("Tom")
fun main() {
println(name) // 输出: Delegate Tom
name = "jerry"
println(name) // 输出: Delegate new jerry
}
2. 接口代理
基本用法:
- 声明一个接口并且实现
- 在类中声明
by 代理对象 - 类就会自动实现接口,并把接口方法调用委托给这个对象
本质 = 组合 + 委托,替代 Java 中的手动代理模式。
kotlin
interface Api {
fun a()
fun b()
}
class ApiImpl : Api {
override fun a() {
println("invoke a")
}
override fun b() {
println("invoke b")
}
}
class ApiWrapper(val api: ApiImpl) : Api by api {
override fun a() {
println("Before")
api.a()
println("After") // 输出: Before \n invoke a \n After
}
}
适用场景:
- 装饰器 / Wrapper 模式:不改变原接口实现,添加行为
- 代理模式(Proxy):Kotlin 语法简洁,不用手动写每个方法的转发代码
五、单例 object
1. 基本用法
在 Kotlin 中,object 是最简单、最安全的单例实现方式,相当于 Java 中的"懒汉式"写法。
kotlin
object Singleton {
val name = "Tom"
fun printName() {
println(name)
}
}
fun main() {
Singleton.printName() // Tom
}
2. 扩展用法
(1)接口实现
kotlin
interface Logger {
fun log(msg: String)
}
object ConsoleLogger : Logger {
override fun log(msg: String) {
println("Log: $msg")
}
}
(2)伴生对象 companion object
kotlin
class MyClass {
companion object {
fun create() = MyClass()
}
}
fun main() {
val obj = MyClass.create()
}
- 伴生对象也是单例
- 可以实现接口或委托
3. @JvmField 注解与 @JvmStatic 注解
由于 Kotlin 是一门跨平台语言(不局限于 JVM),语言本身没有 Java 风格的静态字段/静态方法语义。为增强与 Java 的互操作性,Kotlin 提供了 @JvmField 和 @JvmStatic 两个注解:
kotlin
object Singleton {
@JvmField val name = "Tom"
@JvmStatic fun printName() {
println(name)
}
}
@JvmField 的作用:
java
// 如果没有加入注解,Java 需要通过 INSTANCE 的 getter 来访问
Singleton.INSTANCE.getName();
// 加入注解后,不会生成 getter/setter 方法,并且可以直接访问属性
String name = Singleton.name;
@JvmStatic 的作用:
java
// 如果没有加入注解,Java 需要通过 INSTANCE 来访问
Singleton.INSTANCE.printName();
// 加入注解后,生成静态方法,可以直接访问
Singleton.printName();
六、数据类 data class
Kotlin 的 data class 是一个非常核心的语言特性,用来快速定义携带数据的类,同时自动生成很多常用方法(如 toString、equals、hashCode、copy 等)。
1. 基本使用
kotlin
data class User(
val name: String,
val age: Int
)
- 关键词 :
data - 主构造函数:至少有一个参数
- 参数 :必须至少有一个
val或var,用于生成字段和方法
2.componentN()(解构函数)
kotlin
fun main() {
val user = User("Tom", 20)
val (name, age) = user
println(user.component1()) // Tom
println(user.component2()) // 20
println(name) // Tom
println(age) // 20
// 字节码反编译成 Java,可以看到生成了 componentN() 函数
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
}
3. data class 局限性
| 限制 | 说明 |
|---|---|
| 构造函数参数 | 必须至少有一个 val 或 var |
| 继承 | 不能直接继承其他类(除非是 open data class,但是 Kotlin 不推荐) |
| 可变性 | 可用 var 修改字段,但 val 保持不可变 |
这些限制可能导致某些 ORM(例如 JPA)使用不便 ,因此 data class 更适合用于 DTO 层。Kotlin 官方提供了 allopen 与 noarg 插件来改善兼容性:
kotlin
// 让指定注解的类自动变成 open
// 自动把它的成员方法也变成 open
kotlin("plugin.allopen") version "1.9.23"
// 给带特定注解的类自动生成一个 无参构造函数
kotlin("plugin.noarg") version "1.9.23"
// 打上自定义注解@DomainModel的会生效
allOpen {
annotation("com.example.DomainModel")
}
noArg {
annotation("com.example.DomainModel")
}
七、枚举类 enum class
1. 最基础的枚举
kotlin
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
使用:
kotlin
val d = Direction.NORTH
2. 枚举带构造函数(携带属性)
Kotlin 的枚举本质上和普通类差不多,也能有构造器:
kotlin
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
使用:
kotlin
val red = Color.RED.rgb
3. 枚举可以定义方法
kotlin
enum class State {
IDLE,
RUNNING,
FINISHED;
fun canRun(): Boolean = this == IDLE
}
4. 每个枚举常量定义自己的行为(匿名类)
跟 Java 类似,可以重写抽象方法:
kotlin
enum class Animal {
DOG {
override fun sound() = "Wang!"
},
CAT {
override fun sound() = "Meow"
};
abstract fun sound(): String
}
5. 枚举实现接口
kotlin
interface Printable {
fun print()
}
enum class Level : Printable {
LOW {
override fun print() = println("Low")
},
HIGH {
override fun print() = println("High")
}
}
6. 枚举工具方法
name → 字符串名称
kotlin
val name = Direction.NORTH.name // "NORTH"
ordinal → 序号(从 0 开始)
kotlin
val index = Direction.NORTH.ordinal // 0
values() → 所有枚举值
kotlin
Direction.values()
valueOf() → 根据名称获取枚举
kotlin
val d = Direction.valueOf("SOUTH")
八、密封类 sealed class
Kotlin 密封类(sealed class) 是 Kotlin 非常强大的特性,用来表示受限制的类层级。它的核心作用是: **让编译器知道某个类型所有可能的子类,从而实现更安全的 when 判断、建模各种状态、减少错误。**这点在单元测试里面很有用
1. 密封类是什么?
kotlin
sealed class Result
密封类的特点:
- 同一个文件里才能定义它的子类(Kotlin 1.5+ 允许同一个包的不同文件,但仍有限制)
- 所有子类集合已知且有限
- 适合表示「有限状态机」、「固定状态」
2. 密封类 vs 抽象类
| 特性 | sealed class | abstract class |
|---|---|---|
| 子类是否有限 | ✔ 是(受限制) | ✘ 否 |
| when 是否强制覆盖所有子类 | ✔ 是 | ✘ 否 |
| 适合表示状态 | ✔ 最佳 | 一般 |
3. 密封类可以是 data class、object、普通 class
kotlin
sealed class LoginState {
object Idle : LoginState()
data class Success(val user: User) : LoginState()
data class Error(val reason: String) : LoginState()
object Loading : LoginState()
}
4. 密封接口(sealed interface)
Kotlin 1.5+ 引入:
kotlin
sealed interface UiState
data class Success(val list: List<String>) : UiState
object Loading : UiState
object Empty : UiState
与密封类类似,但可以多继承。
5. 密封类的优势总结
最安全的状态表达
避免写"字符串状态"、"枚举状态",更类型安全。
when 表达式无 else
编译器知道所有可能子类 → 帮你检查有没有漏掉分支。在做单元测试可以避免else死分支
可以携带不同类型的数据
比 enum 更灵活(enum 不能为每个 case 定义不同字段)。
更适合 UI 层、状态管理、网络层建模 Compose、MVI 最常用模式。
6.示例:UI 页面状态
例如列表页面:
kotlin
sealed class PageState<out T> {
object Loading : PageState<Nothing>()
object Empty : PageState<Nothing>()
data class Success<T>(val data: T) : PageState<T>()
data class Error(val message: String) : PageState<Nothing>()
}
使用:
kotlin
when (state) {
PageState.Loading -> showLoading()
PageState.Empty -> showEmpty()
is PageState.Success -> showData(state.data)
is PageState.Error -> showError(state.message)
}
7. sealed class vs enum class(区别)
| 比较点 | sealed class | enum class |
|---|---|---|
| 子类数据结构 | 每个子类不同 | 所有枚举结构相同 |
| 可携带不同数据 | ✔ | ✘ |
| 是否可以继承其他类 | ✔ | ✘ |
| 适用场景 | 状态有限且可携带数据 | 固定常量 |
如果你要 固定常量 → enum 如果你要 状态 + 可携带不同数据 → sealed class