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
相关推荐
初听于你1 小时前
缓存技术揭秘
java·运维·服务器·开发语言·spring·缓存
小蒜学长2 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
奥尔特星云大使4 小时前
MySQL 慢查询日志slow query log
android·数据库·mysql·adb·慢日志·slow query log
zizisuo5 小时前
解决在使用Lombok时maven install 找不到符号的问题
java·数据库·maven
笨蛋少年派5 小时前
JAVA基础语法
java·开发语言
Haooog5 小时前
654.最大二叉树(二叉树算法)
java·数据结构·算法·leetcode·二叉树
我真的是大笨蛋5 小时前
依赖倒置原则(DIP)
java·设计模式·性能优化·依赖倒置原则·设计规范
东方芷兰6 小时前
JavaWeb 课堂笔记 —— 20 SpringBootWeb案例 配置文件
java·开发语言·笔记·算法·log4j·intellij-idea·lua
Roye_ack6 小时前
【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
java·vue.js·spring boot·后端·mybatis
人间有清欢6 小时前
java数据权限过滤
java·mybatis·权限控制·数据过滤