Kotlin学习第 5 课:Kotlin 面向对象编程:类、对象与继承

在编程领域,面向对象编程(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、自定义类,不能用于 IntDouble 等,基本类型需用 lateinit var age: Int? = nullDelegates.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 关键字

子类重写父类的方法时,必须满足两个条件:

  1. 父类方法必须用 open 标记;
  2. 子类方法必须用 override 标记(强制显式声明,避免误重写)。

重写后,子类对象调用方法时,会优先执行子类的实现(多态特性):

Kotlin 复制代码
// 测试多态
val animal1: Animal = Animal("通用动物")
animal1.eat() // 输出:通用动物 在吃食物(父类实现)

val animal2: Animal = Dog("旺财") // 父类引用指向子类对象(多态的核心)
animal2.eat() // 输出:旺财 在吃骨头(子类实现)
animal2.sleep() // 输出:旺财 在睡觉(父类未重写的方法)

3. 属性重写:override 同样适用

除了方法,Kotlin 也支持属性重写 ------ 子类可以重写父类的 open 属性,实现不同的取值逻辑。

属性重写的规则:

  • 父类属性必须用 open 标记;
  • 子类属性必须用 override 标记;
  • val 子类属性可以重写 valvar 父类属性(因为 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 类,同时实现 StudyWork 接口

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
相关推荐
叽哥2 小时前
Kotlin学习第 6 课:Kotlin 集合框架:操作数据的核心工具
android·java·kotlin
心月狐的流火号2 小时前
Spring Bean 生命周期详解——简单、清晰、全面、实用
java·spring
little_xianzhong2 小时前
步骤流程中日志记录方案(类aop)
java·开发语言
半桔2 小时前
【STL源码剖析】二叉世界的平衡:从BST 到 AVL-tree 和 RB-tree 的插入逻辑
java·数据结构·c++·算法·set·map
用户3721574261353 小时前
Python 轻松实现替换或修改 PDF 文字
java
用户6083089290473 小时前
Java中的接口(Interface)与抽象类(Abstract Class)
java·后端
大白的编程日记.3 小时前
【MySQL】表的操作和数据类型
android·数据库·mysql
前行的小黑炭3 小时前
Android LayoutInflater 是什么?XML到View的过程
android·java·kotlin
尚久龙3 小时前
安卓学习 之 SeekBar(音视频播放进度条)
android·java·学习·手机·android studio