重学Kotlin(四)面向对象

92 阅读9分钟

重学Kotlin(四)面向对象

一、Kotlin 的类构造器

Kotlin 的类构造器(Constructors)分为 主构造器次构造器,再加上 初始化块 init,一起组成完整的对象初始化流程。

1. 主构造器(Primary Constructor)

最常用、最简洁的构造方法,直接写在类名后面:

class User(val name: String, val age: Int)

特点:

  • 定义在类名后面的小括号中
  • 默认是 public
  • 参数如果加 val/var 会自动生成对应字段
  • 不能包含逻辑(逻辑写在 init 中)

2. init 初始化块

主构造器不能写逻辑,所以逻辑写在 init 里:

class User(val name: String, val age: Int) {

    init {
        println("User created: $name, $age")
    }
}

执行顺序:主构造器参数 -> init 块

3. 次构造器(Secondary Constructor)

写在类内部,用 constructor 关键字:

class User(val name: String) {

    var age: Int = 0

    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

特点:

  • constructor(...)
  • 必须最终调用主构造器(如果有主构造器)
  • 可以包含逻辑

4. 没有主构造器时

class User {
    var name: String = ""
    var age: Int = 0

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

如果没有主构造器,则 次构造器不需要调用 this()

5. 主构造器与默认值

最常用的写法:

class User(
    val name: String,
    val age: Int = 18
)

这样可以直接:

val u = User("Tom")         // age = 18
val u2 = User("Tom", 20)

Kotlin 强烈推荐这种写法代替多个构造器重载,更简洁。

6. 主构造器可以私有化

常用于工厂方法:

class User private constructor(val name: String) {
    companion object {
        fun create(name: String) = User(name)
    }
}

DCL 双重检查锁定单例:

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。

internal class ApiClient
(2)顶级声明可见性

顶级声明指的是在文件内直接定义类、函数、属性等。

  1. 顶级声明不支持 protected,顶级声明没有父类概念,所以 Kotlin 语义不允许顶级使用 protected
  2. 顶级声明被 private 修饰,表示文件内可见

三、Kotlin 属性

1. Kotlin 属性是什么?

Kotlin 中 属性 = 字段(field) + getter/setter 方法 的组合。

class User {
    var name: String = "Tom"  // 属性
    val age: Int = 18          // 只读属性
}
  • var 可读写
  • val 只读(相当于 final + getter)

Kotlin 不像 Java,需要手动写 getter/setter,编译器自动生成。

2. 自定义 getter / setter

class User {
    var name: String = "Tom"
        get() = field.uppercase()       // 自定义 getter
        set(value) { field = value.trim() } // 自定义 setter
}
  • field = 实际存储的字段
  • getter/setter 可以加逻辑
  • val 只能写 getter(不能写 setter)

3. 常量属性

const val VERSION = "1.0" // 顶层常量
  • 必须是顶层或 companion object 内
  • 类型必须是基本类型或 String
  • 编译期常量,可在 Java 中访问

4. 延迟初始化属性

(1)初始化为null
    var name: String? = null

    fun init(name: String?) {
        this.name = name
        name?.substring(0, 1)
    }

不推荐,写法等价 Java 代码,使用的时候需要判断空指针。

(2)使用lateinit
class User {
    lateinit var token: String

    fun initToken() {
        token = "xxx"
    }
}
  • 只能用于 var
  • 非原始类型(非 Int/Long 等)
  • 使用前未初始化会报异常 UninitializedPropertyAccessException
  • 适用于:依赖注入(DI)、开发者完全确认变量生命周期的场景
(3)lazy

本质上用到的是属性代理

val data: String by lazy {
    println("lazy init")
    "Hello"
}
// 相关源码
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 数据绑定、监控状态变化、日志记录
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 → 拒绝修改
  • 常用场景:值校验、约束属性、范围限制
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
var data: String by Delegates.notNull()

fun main() {
    // println(data) // ❌ 抛异常
    data = "Hello"
    println(data) // ✅ Hello
}
(4)自定义代理

可实现接口 ReadWritePropertyReadOnlyProperty 实现自定义代理:


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. 接口代理

基本用法:

  1. 声明一个接口并且实现
  2. 在类中声明 by 代理对象
  3. 类就会自动实现接口,并把接口方法调用委托给这个对象

本质 = 组合 + 委托,替代 Java 中的手动代理模式。

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
    }
}

适用场景:

  1. 装饰器 / Wrapper 模式:不改变原接口实现,添加行为
  2. 代理模式(Proxy):Kotlin 语法简洁,不用手动写每个方法的转发代码

五、单例 object

1. 基本用法

在 Kotlin 中,object 是最简单、最安全的单例实现方式,相当于 Java 中的“懒汉式”写法。

object Singleton {
    val name = "Tom"

    fun printName() {
        println(name)
    }
}

fun main() {
    Singleton.printName()  // Tom
}

2. 扩展用法

(1)接口实现
interface Logger {
    fun log(msg: String)
}

object ConsoleLogger : Logger {
    override fun log(msg: String) {
        println("Log: $msg")
    }
}
(2)伴生对象 companion object
class MyClass {
    companion object {
        fun create() = MyClass()
    }
}

fun main() {
    val obj = MyClass.create()
}
  • 伴生对象也是单例
  • 可以实现接口或委托

3. @JvmField 注解与 @JvmStatic 注解

由于 Kotlin 是一门跨平台语言(不局限于 JVM),语言本身没有 Java 风格的静态字段/静态方法语义。为增强与 Java 的互操作性,Kotlin 提供了 @JvmField@JvmStatic 两个注解:

object Singleton {
    @JvmField val name = "Tom"

    @JvmStatic fun printName() {
        println(name)
    }
}

@JvmField 的作用:

// 如果没有加入注解,Java 需要通过 INSTANCE 的 getter 来访问
Singleton.INSTANCE.getName();

// 加入注解后,不会生成 getter/setter 方法,并且可以直接访问属性
String name = Singleton.name;

@JvmStatic 的作用:

// 如果没有加入注解,Java 需要通过 INSTANCE 来访问
Singleton.INSTANCE.printName();

// 加入注解后,生成静态方法,可以直接访问
Singleton.printName();

六、数据类 data class

Kotlin 的 data class 是一个非常核心的语言特性,用来快速定义携带数据的类,同时自动生成很多常用方法(如 toStringequalshashCodecopy 等)。

1. 基本使用

data class User(
    val name: String,
    val age: Int
)
  • 关键词data
  • 主构造函数:至少有一个参数
  • 参数:必须至少有一个 valvar,用于生成字段和方法

2.componentN()(解构函数)

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 局限性

限制说明
构造函数参数必须至少有一个 valvar
继承不能直接继承其他类(除非是 open data class,但是 Kotlin 不推荐)
可变性可用 var 修改字段,但 val 保持不可变

这些限制可能导致某些 ORM(例如 JPA)使用不便,因此 data class 更适合用于 DTO 层。Kotlin 官方提供了 allopennoarg 插件来改善兼容性:

// 让指定注解的类自动变成 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. 最基础的枚举

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

使用:

val d = Direction.NORTH

2. 枚举带构造函数(携带属性)

Kotlin 的枚举本质上和普通类差不多,也能有构造器:

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

使用:

val red = Color.RED.rgb

3. 枚举可以定义方法

enum class State {
    IDLE,
    RUNNING,
    FINISHED;

    fun canRun(): Boolean = this == IDLE
}

4. 每个枚举常量定义自己的行为(匿名类)

跟 Java 类似,可以重写抽象方法:

enum class Animal {
    DOG {
        override fun sound() = "Wang!"
    },
    CAT {
        override fun sound() = "Meow"
    };

    abstract fun sound(): String
}

5. 枚举实现接口

interface Printable {
    fun print()
}

enum class Level : Printable {
    LOW {
        override fun print() = println("Low")
    },
    HIGH {
        override fun print() = println("High")
    }
}

6. 枚举工具方法

name → 字符串名称
val name = Direction.NORTH.name // "NORTH"
ordinal → 序号(从 0 开始)
val index = Direction.NORTH.ordinal // 0
values() → 所有枚举值
Direction.values()
valueOf() → 根据名称获取枚举
val d = Direction.valueOf("SOUTH")

八、密封类 sealed class

Kotlin 密封类(sealed class) 是 Kotlin 非常强大的特性,用来表示受限制的类层级。它的核心作用是: **让编译器知道某个类型所有可能的子类,从而实现更安全的 when 判断、建模各种状态、减少错误。**这点在单元测试里面很有用

1. 密封类是什么?

sealed class Result

密封类的特点:

  • 同一个文件里才能定义它的子类(Kotlin 1.5+ 允许同一个包的不同文件,但仍有限制)
  • 所有子类集合已知且有限
  • 适合表示「有限状态机」、「固定状态」

2. 密封类 vs 抽象类

特性sealed classabstract class
子类是否有限✔ 是(受限制)✘ 否
when 是否强制覆盖所有子类✔ 是✘ 否
适合表示状态✔ 最佳一般

3. 密封类可以是 data class、object、普通 class

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+ 引入:

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 页面状态

例如列表页面:

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>()
}

使用:

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 classenum class
子类数据结构每个子类不同所有枚举结构相同
可携带不同数据
是否可以继承其他类
适用场景状态有限且可携带数据固定常量

如果你要 固定常量 → enum 如果你要 状态 + 可携带不同数据 → sealed class