在编程领域,面向对象编程(OOP)是一种主流的编程范式,它将数据和操作数据的方法封装在一起,通过类、对象、继承、多态等特性,让代码更具模块化、可复用性和可维护性。Kotlin 作为一门现代编程语言,对面向对象编程提供了非常优雅且强大的支持,今天我们就从 "类" 这个核心概念入手,逐步深入讲解 Kotlin 面向对象编程的关键知识点。
一、类的定义与实例化:OOP 的基础单元
类是面向对象编程的 "模板",它定义了对象的属性(数据)和方法(行为);而对象则是类的 "实例",是根据模板创建出的具体实体。在 Kotlin 中,我们通过 class
关键字来定义类,整个过程比 Java 更简洁。
1. 类的基本定义:class 关键字
Kotlin 中类的最小定义非常简单,甚至可以只有一个类名(无属性、无方法),语法格式为:
class 类名
比如定义一个空的 Person
类:
Kotlin
// 空类:无属性、无方法,仅作为模板
class Person
但实际开发中,类通常会包含属性 (成员变量,存储数据)和方法 (成员函数,处理逻辑)。比如给 Person
类添加 "姓名""年龄" 属性和 "自我介绍" 方法:
Kotlin
class Person {
// 类的属性(成员变量):var(可变)/ val(不可变)+ 名称 + 类型 + 可选默认值
var name: String = "未知"
var age: Int = 0
// 类的方法(成员函数)
fun introduce() {
println("大家好,我叫$name,今年$age 岁")
}
}
2. 构造函数:初始化对象的 "入口"
构造函数是创建对象时自动调用的方法,用于初始化对象的属性。Kotlin 中的构造函数分为主构造函数 和次构造函数,两者分工不同但可配合使用。
(1)主构造函数:类头中的 "简洁入口"
主构造函数是类的 "主要初始化入口",直接定义在类名后面,语法格式:
class 类名(参数列表) { ... }
它的特点是:无函数体,初始化逻辑需放在 init
初始化块中;参数可直接用于初始化属性(甚至可省略属性定义,直接用 var/val
修饰参数,自动转为类属性)。
举个例子,用主构造函数简化 Person
类:
Kotlin
// 主构造函数:(name: String, age: Int),用var修饰参数,自动转为类属性
class Person(var name: String, var age: Int) {
// init 初始化块:主构造函数的"函数体",创建对象时会执行
init {
println("Person 初始化:name=$name, age=$age")
// 可在这里添加初始化逻辑(如参数校验)
require(age >= 0) { "年龄不能为负数" }
}
fun introduce() {
println("大家好,我叫$name,今年$age 岁")
}
}
(2)次构造函数:主构造函数的 "补充入口"
次构造函数是主构造函数的补充,用于支持更多的初始化场景(比如不同的参数组合)。它通过 constructor
关键字定义,且必须直接或间接调用主构造函数 (用 this()
表示)。
比如给 Person
类添加一个 "只传姓名,年龄默认 18" 的次构造函数:
Kotlin
class Person(var name: String, var age: Int) {
init {
println("Person 初始化:name=$name, age=$age")
require(age >= 0) { "年龄不能为负数" }
}
// 次构造函数:只传姓名,年龄默认18,通过this()调用主构造函数
constructor(name: String) : this(name, 18) {
println("次构造函数执行:年龄默认18")
}
fun introduce() {
println("大家好,我叫$name,今年$age 岁")
}
}
3. 创建类的实例:省略 new 关键字
在 Java 中创建对象需要 new
关键字,但 Kotlin 简化了这一步 ------ 直接调用构造函数即可创建实例,语法:
val/var 对象名 = 类名(构造函数参数)
以上面的 Person
类为例,创建实例:
Kotlin
// 调用主构造函数:name="张三", age=20
val zhangsan = Person("张三", 20)
zhangsan.introduce() // 输出:大家好,我叫张三,今年20岁
// 调用次构造函数:只传name,age默认18
val lisi = Person("李四")
lisi.introduce() // 输出:大家好,我叫李四,今年18岁
二、类的属性详解:控制数据的访问与初始化
类的属性是存储数据的核心,Kotlin 对属性的控制非常灵活 ------ 支持自定义访问器、延迟初始化、惰性初始化等特性,解决了 Java 中 "字段 + getter/setter" 的冗余问题。
1. 属性的定义:四要素缺一不可
Kotlin 中属性的完整定义格式为:
var/val 属性名: 数据类型 = 默认值
var
:表示可变属性(可读写,会自动生成 getter 和 setter);val
:表示不可变属性(只读,仅自动生成 getter,初始化后不能修改);- 数据类型:必须显式声明(除非有默认值且能被 Kotlin 自动推导);
- 默认值:可选,若没有默认值,必须在构造函数或初始化块中赋值。
示例:
Kotlin
class Student {
// val 不可变属性:只能读,不能改
val studentId: String = "2024001" // 有默认值,类型可推导(可省略: String)
// var 可变属性:可读写
var score: Double = 0.0 // 无默认值会报错,必须初始化
// 无默认值的属性:需在init块或构造函数中赋值
var grade: Int
init {
grade = 1 // 在init块中初始化grade
}
}
2. 属性的访问器:自定义 getter 与 setter
Kotlin 会为属性自动生成默认的 getter(读取属性时调用)和 setter(修改属性时调用),但我们也可以根据需求自定义访问器,实现数据校验、格式转换等逻辑。
自定义访问器的语法:
Kotlin
var 属性名: 类型 = 默认值
get() { // 自定义getter
return 处理后的结果
}
set(value) { // 自定义setter,value是赋值时的参数(类型与属性一致)
// 自定义逻辑(如参数校验)
field = value // field 是"幕后字段",表示属性的实际存储值
}
示例:给 "年龄" 属性添加范围校验
Kotlin
class Person {
var name: String = "未知"
// 自定义age的setter:确保年龄在0-150之间
var age: Int = 0
set(value) {
if (value < 0 || value > 150) {
throw IllegalArgumentException("年龄必须在0-150之间")
}
field = value // 符合条件,赋值给幕后字段
}
// 自定义name的getter:
get() {
return field
}
}
// 测试
val person = Person()
person.name = "zhangsan"
println(person.name) // 输出:ZHANGSAN(getter自动转大写)
person.age = 200 // 抛出异常:年龄必须在0-150之间
注意:val
属性不能自定义 setter(因为它是只读的),只能自定义 getter。
3. 延迟初始化属性:lateinit 关键字
当属性的初始化逻辑不能在声明时完成(比如依赖后续的网络请求、数据库查询),但我们能保证在使用前一定会初始化,此时可以用 lateinit
关键字标记属性,避免 "必须初始化" 的编译错误。
用法与限制:
- 仅用于
var
属性(不可变的val
不行,因为val
必须在初始化时确定值); - 仅用于非基本数据类型(如
String
、自定义类,不能用于Int
、Double
等,基本类型需用lateinit var age: Int? = null
或Delegates.notNull()
); - 使用前必须初始化,否则会抛出
UninitializedPropertyAccessException
; - 可通过
::属性名.isInitialized
判断是否已初始化。
示例:
Kotlin
class UserManager {
// lateinit 标记:延迟初始化,声明时不赋值
lateinit var user: User
// 模拟网络请求后初始化user
fun loadUser() {
user = User("张三", "123456") // 初始化
}
fun printUser() {
// 判断是否已初始化,避免异常
if (::user.isInitialized) {
println(user.name)
} else {
println("user 未初始化")
}
}
}
class User(val name: String, val password: String)
// 测试
val manager = UserManager()
manager.printUser() // 输出:user 未初始化
manager.loadUser()
manager.printUser() // 输出:张三
4. 惰性初始化属性:by lazy 关键字
"惰性初始化" 指属性在首次被访问时才初始化 (而非创建对象时),适用于初始化成本较高的场景(如加载大文件、初始化复杂工具类)。Kotlin 用 by lazy
实现惰性初始化,语法:
val 属性名: 类型 by lazy { 初始化逻辑 }
用法与特点:
- 仅用于
val
属性(因为初始化后值不会变,符合惰性初始化的语义); - 初始化逻辑在首次访问时执行 ,且仅执行一次(线程安全,默认用
LazyThreadSafetyMode.SYNCHRONIZED
,多线程下仅一个线程执行初始化); - 初始化逻辑是一个 lambda 表达式,返回值即为属性的初始值。
示例:模拟加载配置文件(初始化成本高)
Kotlin
class ConfigLoader {
// 惰性初始化:首次访问config时才加载配置
val config: Map<String, String> by lazy {
println("开始加载配置文件...")
// 模拟加载逻辑(如读取文件、解析JSON)
mapOf(
"serverUrl" to "https://api.example.com",
"timeout" to "5000"
)
}
}
// 测试
val loader = ConfigLoader()
println("ConfigLoader 创建完成,未加载配置") // 此时config未初始化
val serverUrl = loader.config["serverUrl"] // 首次访问,执行初始化
println("服务器地址:$serverUrl") // 输出:服务器地址:https://api.example.com
val timeout = loader.config["timeout"] // 再次访问,不执行初始化
println("超时时间:$timeout") // 输出:超时时间:5000
三、继承与多态:实现代码复用与扩展
继承是面向对象的核心特性之一,它允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码复用;多态则允许子类重写父类的方法,让同一行为有不同的实现。
Kotlin 为了避免 "菱形继承" 等问题,默认所有类都是不可继承的 (相当于 Java 中的 final
类),若要允许继承,必须用 open
关键字标记父类。
1. 父类与子类:open 关键字与继承语法
(1)定义可继承的父类:open 关键字
要让一个类可被继承,需在 class
前加 open
;若要让父类的方法可被子类重写,需在方法前加 open
。
示例:定义可继承的 Animal
父类
Kotlin
// open 关键字:允许Animal被继承
open class Animal(val name: String) {
// open 关键字:允许eat()方法被重写
open fun eat() {
println("$name 在吃食物")
}
fun sleep() {
println("$name 在睡觉")
}
}
(2)定义子类:继承语法 : 父类名(构造函数参数)
子类通过 :
继承父类,且必须在子类的构造函数中调用父类的构造函数(用 父类名(参数)
表示,类似 Java 的 super()
)。
示例:定义 Dog
子类继承 Animal
Kotlin
// 子类Dog:继承Animal,主构造函数参数name传给父类
class Dog(name: String) : Animal(name) {
// override 关键字:重写父类的open方法
override fun eat() {
println("$name 在吃骨头") // 子类的具体实现
}
}
2. 方法重写:override 关键字
子类重写父类的方法时,必须满足两个条件:
- 父类方法必须用
open
标记; - 子类方法必须用
override
标记(强制显式声明,避免误重写)。
重写后,子类对象调用方法时,会优先执行子类的实现(多态特性):
Kotlin
// 测试多态
val animal1: Animal = Animal("通用动物")
animal1.eat() // 输出:通用动物 在吃食物(父类实现)
val animal2: Animal = Dog("旺财") // 父类引用指向子类对象(多态的核心)
animal2.eat() // 输出:旺财 在吃骨头(子类实现)
animal2.sleep() // 输出:旺财 在睡觉(父类未重写的方法)
3. 属性重写:override 同样适用
除了方法,Kotlin 也支持属性重写 ------ 子类可以重写父类的 open
属性,实现不同的取值逻辑。
属性重写的规则:
- 父类属性必须用
open
标记; - 子类属性必须用
override
标记; val
子类属性可以重写val
或var
父类属性(因为val
是只读,扩展为var
是允许的);var
子类属性不能重写val
父类属性(因为val
没有 setter,无法扩展为可写)。
示例:属性重写
Kotlin
open class Person {
// open 属性:允许被重写
open val description: String
get() = "这是一个人"
}
class Student : Person() {
// 重写父类的description属性
override val description: String
get() = "这是一个学生"
}
// 测试
val person: Person = Student()
println(person.description) // 输出:这是一个学生(子类实现)
4. 抽象类与抽象方法:强制子类实现
抽象类是 "不完全的类",它包含抽象方法 (只有声明,没有实现),必须由子类实现。抽象类用 abstract
关键字标记,抽象方法也用 abstract
标记(无需 open
,因为抽象方法默认就是可重写的)。
特点:
- 抽象类不能直接实例化(必须通过子类);
- 子类继承抽象类后,必须重写所有抽象方法(除非子类也是抽象类);
- 抽象类中可以包含非抽象方法(带实现的方法)。
示例:定义抽象类 Shape
Kotlin
// 抽象类:不能实例化
abstract class Shape(val name: String) {
// 抽象方法:只有声明,无实现,子类必须重写
abstract fun calculateArea(): Double
// 非抽象方法:带实现,子类可直接使用
fun printName() {
println("图形名称:$name")
}
}
// 子类Circle:继承抽象类,必须重写calculateArea()
class Circle(name: String, val radius: Double) : Shape(name) {
override fun calculateArea(): Double {
return Math.PI * radius * radius // 圆的面积公式
}
}
// 测试
val circle = Circle("圆形", 5.0)
circle.printName() // 输出:图形名称:圆形
println("圆的面积:${circle.calculateArea()}") // 输出:圆的面积:78.5398...
四、接口:实现多 "能力" 扩展
接口是一种 "行为契约",它定义了一组方法(或属性)的声明,类通过 "实现接口" 来获取对应的能力。Kotlin 中的接口支持抽象方法、默认方法和属性声明,且一个类可以实现多个接口(解决了类单继承的限制)。
1. 接口的定义:interface 关键字
接口用 interface
关键字定义,语法:
Kotlin
interface 接口名 {
// 1. 抽象方法:无需abstract,默认就是抽象的
fun 方法名(参数列表): 返回值类型
// 2. 默认方法:带实现的方法,用fun...body
fun 默认方法名(参数列表): 返回值类型 {
// 实现逻辑
}
// 3. 属性声明:只能是抽象的(无幕后字段),或提供getter(不能有setter)
val 属性名: 类型 // 抽象属性,子类必须实现
val 带Getter的属性名: 类型
get() = 初始值 // 提供getter,无需子类实现
}
示例:定义 "学习" 和 "工作" 接口
Kotlin
// 学习接口:定义"学习"能力
interface Study {
// 抽象方法:学习
fun study()
// 默认方法:复习(带实现)
fun review() {
println("复习所学内容")
}
// 抽象属性:学习时长
val studyHours: Int
// 带Getter的属性:学习状态
val studyStatus: String
get() = if (studyHours > 2) "认真学习" else "摸鱼中"
}
// 工作接口:定义"工作"能力
interface Work {
// 抽象方法:工作
fun work()
// 带Getter的属性:工作类型
val workType: String
get() = "全职"
}
2. 类实现接口:多实现语法
类通过 :
实现接口,多个接口用逗号分隔。实现接口时,必须重写所有接口的抽象方法 和抽象属性;默认方法和带 Getter 的属性可直接使用,也可重写。
示例:定义 Student
类,同时实现 Study
和 Work
接口
Kotlin
// 实现多个接口:Study和Work
class Student(
val name: String,
override val studyHours: Int // 重写Study接口的抽象属性
) : Study, Work {
// 重写Study接口的抽象方法:study()
override fun study() {
println("$name 在学习Kotlin")
}
// 可选:重写Study接口的默认方法review()
override fun review() {
println("$name 在复习Kotlin面向对象编程")
}
// 重写Work接口的抽象方法:work()
override fun work() {
println("$name 在做兼职开发")
}
// 可选:重写Work接口的带Getter属性workType
override val workType: String
get() = "兼职"
}
// 测试
val student = Student("李四", 3)
student.study() // 输出:李四 在学习Kotlin
student.review() // 输出:李四 在复习Kotlin面向对象编程
student.work() // 输出:李四 在做兼职开发
println("学习状态:${student.studyStatus}") // 输出:学习状态:认真学习
println("工作类型:${student.workType}") // 输出:工作类型:兼职
3. 接口与抽象类的区别
很多初学者会混淆接口和抽象类,其实两者的设计目的和用法有本质区别:
对比维度 | 接口(Interface) | 抽象类(Abstract Class) |
---|---|---|
继承 / 实现数量 | 一个类可实现多个接口(多实现) | 一个类只能继承一个抽象类(单继承) |
属性与字段 | 无幕后字段,属性只能是抽象的或带 Getter | 可包含普通字段(如 var age: Int),支持完整属性 |
构造函数 | 无构造函数(不能实例化) | 有构造函数(但不能直接实例化) |
方法实现 | 可包含抽象方法和默认方法 | 可包含抽象方法和非抽象方法 |
设计目的 | 定义 "能力契约"(has-a 关系),如 "可学习""可工作" | 定义 "is-a 关系"(父子类),如 "Dog is an Animal" |
简单来说:需要复用代码、表示父子关系时用抽象类;需要扩展能力、支持多实现时用接口。
五、数据类与密封类:Kotlin 的 "特色类"
除了普通类、抽象类,Kotlin 还提供了两种特殊的类 ------ 数据类(data class
)和密封类(sealed class
),分别用于简化 "数据存储" 和 "有限子类" 场景。
1. 数据类:自动生成常用方法
数据类是专门用于存储数据的类(如实体类、DTO),Kotlin 会为数据类自动生成以下方法,无需手动编写:
equals()
:判断两个对象的属性是否相等;hashCode()
:根据属性生成哈希值;toString()
:返回 "类名 (属性 1 = 值 1, 属性 2 = 值 2, ...)" 格式的字符串;componentN()
:用于解构赋值(如val (name, age) = person
);copy()
:复制对象并修改部分属性(避免修改原对象)。
定义数据类:data class 关键字
语法:data class 类名(主构造函数参数)
注意事项:
- 主构造函数至少包含一个参数;
- 参数最好用
val
修饰(确保不可变,符合数据类的语义); - 不能是抽象类、密封类、内部类或枚举类。
示例:定义 User
数据类
Kotlin
// 数据类:自动生成equals、hashCode、toString、copy等方法
data class User(val id: Int, val name: String, val age: Int)
// 测试
val user1 = User(1, "张三", 20)
val user2 = User(1, "张三", 20)
val user3 = User(2, "李四", 18)
// 1. toString():自动生成格式化字符串
println(user1) // 输出:User(id=1, name=张三, age=20)
// 2. equals():判断属性是否相等
println(user1 == user2) // 输出:true(属性相同)
println(user1 == user3) // 输出:false(属性不同)
// 3. copy():复制对象并修改部分属性
val user4 = user1.copy(age = 21) // 复制user1,修改age为21
println(user4) // 输出:User(id=1, name=张三, age=21)
// 4. 解构赋值:通过componentN()获取属性
val (id, name, age) = user1
println("id=$id, name=$name, age=$age") // 输出:id=1, name=张三, age=20
2. 密封类:限制子类范围,优化 when 判断
密封类(sealed class
)是一种 "有限层次结构" 的类,它的子类范围被严格限制 ------子类只能定义在密封类的内部或同一个文件中,不能在其他文件中定义子类。
密封类的核心优势是:在 when
表达式中匹配子类时,无需添加 else
分支(Kotlin 编译时能确定所有子类,避免遗漏),从而提高代码的安全性和可读性。
定义密封类:sealed class 关键字
语法:
Kotlin
sealed class 密封类名 {
// 子类1:定义在密封类内部
class 子类1(参数列表) : 密封类名()
// 子类2:定义在密封类内部
object 子类2 : 密封类名() // 若子类无属性,可用object(单例)
}
// 子类3:定义在同一个文件中
class 子类3(参数列表) : 密封类名()
示例:定义 "网络请求结果" 密封类 Result
Kotlin
// 密封类:限制子类只能是Success、Error、Loading
sealed class Result<out T> { // 泛型T:表示成功时的数据类型
// 成功:带数据
data class Success<out T>(val data: T) : Result<T>()
// 失败:带错误信息
data class Error(val message: String) : Result<Nothing>()
// 加载中:无数据,用object(单例)
object Loading : Result<Nothing>()
}
// 处理请求结果的函数
fun <T> handleResult(result: Result<T>) {
// when 匹配:无需else,因为编译时知道所有子类
when (result) {
is Result.Success -> println("请求成功,数据:${result.data}")
is Result.Error -> println("请求失败,原因:${result.message}")
Result.Loading -> println("请求中,请等待...")
}
}
// 测试
val successResult = Result.Success("Kotlin 面向对象教程")
val errorResult = Result.Error("网络连接超时")
val loadingResult = Result.Loading
handleResult(successResult) // 输出:请求成功,数据:Kotlin 面向对象教程
handleResult(errorResult) // 输出:请求失败,原因:网络连接超时
handleResult(loadingResult) // 输出:请求中,请等待...
六、对象表达式与对象声明(单例)
除了类和接口,Kotlin 还提供了 "对象" 相关的特性,用于简化匿名内部类和单例模式的实现。
1. 对象表达式:替代匿名内部类
在 Java 中,我们常用匿名内部类(如 new OnClickListener() { ... }
),而 Kotlin 用 "对象表达式"(object : 父类/接口 { ... }
)替代,语法更简洁。
示例:模拟按钮点击监听
Kotlin
// 定义点击监听接口
interface OnClickListener {
fun onClick()
}
// 模拟按钮类
class Button {
var onClickListener: OnClickListener? = null
fun click() {
onClickListener?.onClick()
}
}
// 测试:用对象表达式实现OnClickListener
val button = Button()
button.onClickListener = object : OnClickListener {
override fun onClick() {
println("按钮被点击了!")
}
}
button.click() // 输出:按钮被点击了!
2. 对象声明:实现单例模式
单例模式是一种常用的设计模式,确保一个类只有一个实例。Kotlin 用 "对象声明"(object 类名 { ... }
)直接实现单例,无需手动处理线程安全和实例创建。
特点:
- 对象声明是饿汉式单例(类加载时创建实例,线程安全);
- 不能有构造函数(因为是单例,只能有一个实例);
- 通过
类名.属性/方法
直接访问。
示例:定义单例 Singleton
Kotlin
// 对象声明:单例类
object Singleton {
var count = 0
fun increment() {
count++
println("当前计数:$count")
}
}
// 测试:直接访问单例,无需实例化
Singleton.increment() // 输出:当前计数:1
Singleton.increment() // 输出:当前计数:2
Singleton.increment() // 输出:当前计数:3