3、Kotlin面向对象:类与对象、继承、接口、数据类、密封类、枚举类、伴生对象
说实话,很多从Java转过来的朋友,刚接触Kotlin的面向对象时,会觉得有点「怪」。
比如没有new关键字,比如class默认是final的。但用久了你会发现,Kotlin的设计其实更符合直觉。我当年在重构一个电商项目时,把Java的POJO全部换成数据类,代码量直接砍掉三分之一。嗯,那种感觉确实很爽。
3.1 类与对象:从基础说起
Kotlin里定义一个类,用class关键字。实例化时,不需要new。
class User(val name: String, var age: Int)
fun main() {
val user = User("张三", 25) // 直接调用构造器
println(user.name) // 张三
user.age = 26 // var 属性可修改
}
这里有个细节:val修饰的属性只有getter,var修饰的既有getter也有setter。我个人习惯把不可变的字段都用val声明,能少踩很多坑。
var age: Int = 0
get() = if (field < 0) 0 else field
set(value) { field = value.coerceAtMost(150) }
3.2 继承:默认是封闭的
Kotlin的类默认是final的,不能被继承。这跟Java正好相反。为什么要这么设计?说白了,就是鼓励你用组合而非继承。我在项目中见过太多滥用继承导致代码难以维护的例子。
想让一个类可以被继承,得加上open关键字:
open class Animal(val name: String) {
open fun speak() {
println("$name 发出声音")
}
}
class Dog(name: String) : Animal(name) {
override fun speak() {
println("$name 汪汪叫")
}
}
注意看,override是强制要求写的。这跟Java的@Override注解不同,Kotlin把它变成了关键字。我曾经在团队里推行这个规范后,重写方法时漏写注解的问题再也没出现过。
open,子类是不能重写的。编译期就会报错,这其实是个好事。
3.3 接口:可以有默认实现
Kotlin的接口跟Java 8有点像,可以定义抽象方法,也可以提供默认实现。
interface Flyable {
fun fly() // 抽象方法
fun takeOff() {
println("准备起飞...") // 默认实现
}
}
class Bird : Flyable {
override fun fly() {
println("小鸟飞翔")
}
}
你想想看,这在做多态设计时特别方便。比如我们有个支付模块,Payable接口里可以放一个通用的日志记录方法,子类不用每个都去实现一遍。
3.4 数据类:专为保存数据而生
数据类是我在Kotlin里最喜欢的特性之一。用data关键字修饰,编译器会自动生成equals()、hashCode()、toString()、copy()等方法。
data class User(val id: Int, val name: String, val email: String)
fun main() {
val user1 = User(1, "李四", "li@example.com")
val user2 = user1.copy(name = "王五") // 只修改name
println(user1) // User(id=1, name=李四, email=li@example.com)
println(user2) // User(id=1, name=王五, email=li@example.com)
}
我曾经在做一个IM项目时,消息体全部用数据类定义。配合copy()方法做不可变更新,再也不用担心多线程下的数据竞争问题。
val或var标记。另外,数据类不能是抽象、开放、密封或内部类。
3.5 密封类:受限的类层次结构
密封类用sealed关键字修饰,它表示一个受限的类层次结构。说白了,就是告诉编译器:「这个类的子类只有我知道的这几个」。
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("成功: ${result.data}")
is Result.Error -> println("失败: ${result.message}")
Result.Loading -> println("加载中...")
} // 不需要else分支,因为所有情况都覆盖了
}
这个特性在写网络请求状态管理时特别好用。我习惯把请求的成功、失败、加载中三种状态定义成密封类的子类,配合when表达式,编译器会帮我检查有没有遗漏的情况。
3.6 枚举类:比Java更强大
Kotlin的枚举类除了能定义常量,还可以有属性和方法:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF);
fun hexString() = "#${rgb.toString(16).padStart(6, '0')}"
}
fun main() {
println(Color.RED.hexString()) // #ff0000
}
注意枚举常量之间的分号是必须的,否则编译器会报错。嗯,这个细节我刚开始也经常忘。
3.7 伴生对象:Kotlin的「静态成员」
Kotlin没有static关键字。那静态方法怎么办?用伴生对象(companion object)。
class Config {
companion object {
const val BASE_URL = "https://api.example.com"
fun createDefault(): Config = Config()
}
}
fun main() {
println(Config.BASE_URL) // 直接访问
val config = Config.createDefault() // 调用静态方法
}
伴生对象本质上是一个单例对象,它属于类本身。你还可以给它起名字:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
// 调用时两种方式都可以
MyClass.create()
MyClass.Factory.create()
我个人习惯在工具类或者工厂模式里用伴生对象。比如写一个DateUtils,把所有的日期格式化方法都放在伴生对象里,调用起来特别清爽。
-keepclassmembers class * { companion object *; }。
小结
Kotlin的面向对象设计,说白了就是在Java的基础上做了很多「减法」和「加法」。减掉了冗余的样板代码,加上了更安全的类型约束。数据类和密封类是我日常开发中使用频率最高的两个特性,强烈建议你在项目中多试试。
下一章我们会聊到Kotlin的函数式编程,包括高阶函数、Lambda表达式和内联函数。到时候你会发现,Kotlin的精华其实在函数式这一块。