Kotlin D3
面向对象
一、课程目标
本次课程旨在让学员全面且深入地了解面向对象编程的核心概念,透彻掌握类与对象之间的紧密关系,熟练运用 Kotlin 语言中各类常用类进行程序开发。通过理论知识的系统讲解、丰富多样的实际案例分析以及详细的代码解读,帮助学员将面向对象编程思想融入到 Kotlin 编程实践中,提升解决实际问题的能力,为后续开发复杂的 Kotlin 应用程序奠定坚实基础。
二、面向对象的概念
2.1 什么是面向对象编程
面向对象编程(Object - Oriented Programming,简称 OOP)是一种强大的编程范式,它模拟了现实世界中事物的组织和交互方式。在 OOP 中,程序被看作是一组相互协作的对象集合,每个对象都有自己独特的状态(属性)和行为(方法)。例如,在一个电商系统中,商品、订单、用户等都可以被抽象为对象。商品对象有名称、价格、库存等属性,以及添加到购物车、修改价格等行为;订单对象有订单号、下单时间、商品列表等属性,以及支付、取消等行为;用户对象有用户名、密码、收货地址等属性,以及登录、下单等行为。这些对象之间通过消息传递进行交互,共同完成系统的各项功能。
2.2 面向对象编程的优点
2.2.1 封装性
封装是面向对象编程的重要特性之一,它就像一个黑盒子,将对象的内部数据和操作数据的方法封装在一起,对外只暴露必要的接口。这样做有以下几个好处:
- 数据安全性:外部代码无法直接访问和修改对象的内部数据,只能通过对象提供的方法进行操作,从而避免了非法数据的输入和数据的意外修改。例如,在一个银行账户类中,账户余额是敏感数据,外部代码不能直接修改余额,只能通过存款、取款等方法来操作,这些方法内部会进行合法性检查,确保数据的安全性。
- 代码可维护性:当对象的内部实现发生变化时,只要对外接口不变,其他代码不需要进行修改。例如,银行账户类的内部实现可能从简单的余额管理升级为复杂的账户状态管理,但只要存款、取款等接口方法不变,调用这些方法的代码就不需要修改,降低了维护成本。
2.2.2 继承性
继承允许一个类(子类)继承另一个类(父类)的属性和方法,实现了代码的复用和扩展。继承的优点如下:
- 代码复用:子类可以直接使用父类的属性和方法,避免了重复编写相同的代码。例如,在一个图形绘制系统中,圆形、矩形、三角形等图形类都有一些共同的属性(如颜色、位置)和方法(如绘制、移动),可以将这些共同的部分提取到一个父类(图形类)中,子类只需要实现自己特有的属性和方法即可。
- 扩展性:子类可以在父类的基础上添加新的属性和方法,或者重写父类的方法,以实现特定的功能。例如,在图形类的基础上,圆形类可以添加半径属性和计算面积的方法,矩形类可以添加长和宽属性和计算周长的方法。
2.2.3 多态性
多态是指同一个方法调用可以根据对象的不同类型表现出不同的行为。多态提高了代码的灵活性和可扩展性,主要体现在以下方面:
- 可替换性 :子类对象可以替换父类对象使用,程序的功能不会受到影响。例如,在一个动物叫声模拟器中,动物类有一个
makeSound
方法,狗类和猫类继承自动物类并重写了makeSound
方法。在调用makeSound
方法时,可以传入狗对象或猫对象,程序会根据实际传入的对象类型调用相应的方法。 - 可扩展性 :当需要添加新的子类时,不需要修改现有的代码。例如,在动物叫声模拟器中,如果要添加一个鸟类,只需要创建一个鸟类继承自动物类并重写
makeSound
方法,原有的代码不需要做任何修改就可以支持鸟类的叫声模拟。
三、类与对象的关系
3.1 类的定义
类是对象的抽象描述,它是创建对象的模板。在 Kotlin 中,使用 class
关键字来定义类,类可以包含属性和方法。
kotlin
// 定义一个简单的类
class Person {
// 定义类的属性
// var 关键字表示属性是可变的
var name: String = ""
var age: Int = 0
// 定义类的方法
// 该方法用于输出个人信息
fun introduce() {
println("我叫 $name,今年 $age 岁。")
}
// 该方法用于修改年龄
fun updateAge(newAge: Int) {
if (newAge > 0) {
age = newAge
} else {
println("年龄不能为负数。")
}
}
}
class Person
:使用class
关键字定义了一个名为Person
的类。var name: String = ""
和var age: Int = 0
:定义了类的两个属性,name
是字符串类型,用于存储人的姓名,初始值为空字符串;age
是整数类型,用于存储人的年龄,初始值为 0。var
关键字表示这些属性是可变的,可以在对象创建后进行修改。fun introduce()
:定义了类的一个方法,方法名为introduce
,用于输出个人信息。在方法内部,使用字符串模板将name
和age
的值插入到输出语句中。fun updateAge(newAge: Int)
:定义了一个用于修改年龄的方法,接收一个整数参数newAge
。在方法内部,会检查新年龄是否大于 0,如果是则更新age
属性,否则输出错误信息。
3.2 对象的创建
对象是类的具体实例,通过类可以创建多个不同的对象。在 Kotlin 中,使用类名后面跟括号的方式来创建对象。
kotlin
fun main() {
// 创建 Person 类的对象
val person1 = Person()
// 给对象的属性赋值
person1.name = "张三"
person1.age = 20
// 调用对象的方法
person1.introduce()
val person2 = Person()
person2.name = "李四"
person2.age = 25
person2.introduce()
// 调用修改年龄的方法
person2.updateAge(30)
person2.introduce()
}
val person1 = Person()
:创建了Person
类的一个对象person1
,val
关键字表示person1
是一个不可变的引用,即不能再将person1
指向其他对象。person1.name = "张三"
和person1.age = 20
:给person1
对象的属性赋值,将姓名设置为"张三"
,年龄设置为 20。person1.introduce()
:调用person1
对象的introduce
方法,输出个人信息。- 同样的方式创建了另一个对象
person2
并进行操作,每个对象都有自己独立的属性值。 person2.updateAge(30)
:调用person2
对象的updateAge
方法,将年龄修改为 30,然后再次调用introduce
方法输出更新后的个人信息。
3.3 类与对象的关系
类是对象的模板,它定义了对象的属性和方法;对象是类的实例,通过类可以创建多个不同的对象,每个对象都有自己独立的属性值。类就像是一个设计蓝图,规定了对象应该具备的特征和行为;而对象则是根据这个蓝图制造出来的具体产品,每个产品都有自己独特的状态。例如,Person
类是一个蓝图,person1
和 person2
就是根据这个蓝图创建出来的具体的人,他们有不同的姓名和年龄。
四、构造函数
4.1 主构造函数
主构造函数是类头的一部分,紧跟在类名后面。可以在主构造函数中定义类的属性,并进行初始化。
kotlin
// 定义一个带有主构造函数的类
class Student constructor(name: String, age: Int) {
// 将主构造函数的参数赋值给类的属性
var name: String = name
var age: Int = age
// 该方法表示学生正在学习
fun study() {
println("$name 正在学习。")
}
// 该方法表示学生参加考试
fun takeExam() {
println("$name 正在参加考试。")
}
}
// 简化写法,直接在主构造函数中定义属性
class StudentSimplified(var name: String, var age: Int) {
// 该方法表示学生正在学习
fun study() {
println("$name 正在学习。")
}
// 该方法表示学生参加考试
fun takeExam() {
println("$name 正在参加考试。")
}
}
fun main() {
// 创建 Student 类的对象
val student1 = Student("王五", 18)
student1.study()
student1.takeExam()
val student2 = StudentSimplified("赵六", 22)
student2.study()
student2.takeExam()
}
class Student constructor(name: String, age: Int)
:定义了一个带有主构造函数的类Student
,constructor
关键字可以省略。主构造函数接收两个参数name
和age
,用于初始化对象的属性。var name: String = name
和var age: Int = age
:将主构造函数的参数赋值给类的属性。class StudentSimplified(var name: String, var age: Int)
:简化写法,直接在主构造函数中定义属性,这样可以减少代码的冗余。- 在
main
函数中创建了Student
和StudentSimplified
类的对象,并调用了study
和takeExam
方法。
4.2 次构造函数
次构造函数用于提供额外的构造方式,它必须直接或间接地调用主构造函数。
kotlin
class Teacher(var name: String, var age: Int) {
// 次构造函数
constructor(name: String) : this(name, 0) {
println("次构造函数被调用。")
}
// 该方法表示老师正在授课
fun teach() {
println("$name 正在授课。")
}
// 该方法表示老师批改作业
fun gradeHomework() {
println("$name 正在批改作业。")
}
}
fun main() {
// 使用主构造函数创建对象
val teacher1 = Teacher("李老师", 30)
teacher1.teach()
teacher1.gradeHomework()
// 使用次构造函数创建对象
val teacher2 = Teacher("王老师")
teacher2.teach()
teacher2.gradeHomework()
}
constructor(name: String) : this(name, 0)
:定义了一个次构造函数,它接收一个参数name
,并通过this(name, 0)
调用主构造函数,将年龄初始化为 0。- 在
main
函数中,分别使用主构造函数和次构造函数创建了Teacher
类的对象,并调用了teach
和gradeHomework
方法。当使用次构造函数创建对象时,会输出 "次构造函数被调用。" 的信息。
4.3 构造函数中的初始化块
在 Kotlin 中,构造函数还可以包含初始化块,用于在对象创建时执行一些额外的初始化操作。
kotlin
class Book(var title: String, var author: String) {
var publishedYear: Int = 0
init {
println("正在初始化图书对象...")
// 可以在这里进行一些复杂的初始化操作
if (title.isBlank()) {
title = "未知书名"
}
if (author.isBlank()) {
author = "未知作者"
}
}
fun displayInfo() {
println("书名: $title,作者: $author,出版年份: $publishedYear")
}
}
fun main() {
val book = Book("", "")
book.displayInfo()
}
init
块是初始化块,在对象创建时会被执行。在这个例子中,初始化块会检查title
和author
是否为空,如果为空则设置为默认值。- 在
main
函数中创建Book
对象时,会先执行初始化块,然后调用displayInfo
方法输出图书信息。
五、继承
5.1 基类和派生类
在 Kotlin 中,所有类默认是 final
的,即不能被继承。如果要让一个类可以被继承,需要使用 open
关键字修饰。
kotlin
// 定义一个基类
open class Animal(var name: String) {
// 该方法表示动物发出声音
open fun makeSound() {
println("$name 发出声音。")
}
// 该方法表示动物移动
open fun move() {
println("$name 正在移动。")
}
}
// 定义一个派生类,继承自 Animal 类
class Dog(name: String) : Animal(name) {
// 重写基类的 makeSound 方法
override fun makeSound() {
println("$name 汪汪叫。")
}
// 重写基类的 move 方法
override fun move() {
println("$name 四条腿奔跑。")
}
// 狗类特有的方法
fun fetch() {
println("$name 正在捡东西。")
}
}
fun main() {
// 创建 Animal 类的对象
val animal = Animal("动物")
animal.makeSound()
animal.move()
// 创建 Dog 类的对象
val dog = Dog("旺财")
dog.makeSound()
dog.move()
dog.fetch()
}
open class Animal
:使用open
关键字修饰Animal
类,使其可以被继承。open fun makeSound()
和open fun move()
:使用open
关键字修饰方法,使其可以被重写。class Dog(name: String) : Animal(name)
:定义了一个派生类Dog
,继承自Animal
类,并在主构造函数中调用基类的主构造函数,将name
参数传递给基类。override fun makeSound()
和override fun move()
:使用override
关键字重写基类的方法,实现了狗独特的发声和移动方式。fun fetch()
:狗类特有的方法,体现了子类的扩展性。- 在
main
函数中,分别创建了Animal
类和Dog
类的对象,并调用了相应的方法。
5.2 继承的特点
5.2.1 属性和方法的继承
子类可以继承父类的属性和方法,这样可以避免代码的重复编写。例如,Dog
类继承了 Animal
类的 name
属性和 move
方法。
5.2.2 方法重写
子类可以重写父类的 open
方法,实现自己的行为。重写方法时,需要使用 override
关键字,并且方法的签名(方法名、参数列表、返回类型)要与父类的方法一致。在重写方法时,还可以使用 super
关键字调用父类的方法。
kotlin
open class Shape {
open fun draw() {
println("绘制一个形状。")
}
}
class Circle : Shape() {
override fun draw() {
super.draw()
println("绘制一个圆形。")
}
}
fun main() {
val circle = Circle()
circle.draw()
}
- 在
Circle
类的draw
方法中,使用super.draw()
调用了父类Shape
的draw
方法,然后再输出自己的绘制信息。
5.2.3 功能扩展
子类可以添加自己的属性和方法,扩展功能。例如,Dog
类可以添加一个 fetch()
方法,表示狗会捡东西。
六、接口
1. 接口的定义
接口是一种抽象类型,它定义了一组方法的签名,但不包含方法的实现。在 Kotlin 中,使用 interface
关键字来定义接口。
kotlin
// 定义一个接口
interface Flyable {
fun fly()
}
// 定义一个类实现 Flyable 接口
class Bird : Flyable {
override fun fly() {
println("鸟儿在飞翔。")
}
}
fun main() {
// 创建 Bird 类的对象
val bird = Bird()
bird.fly()
}
interface Flyable
:使用interface
关键字定义了一个名为Flyable
的接口,其中包含一个抽象方法fly()
。class Bird : Flyable
:定义了一个类Bird
,实现了Flyable
接口。override fun fly()
:使用override
关键字实现了接口中的fly
方法。- 在
main
函数中,创建了Bird
类的对象,并调用了fly
方法。
2. 接口的特点
- 一个类可以实现多个接口。
- 接口中的方法默认是
abstract
的,不需要使用abstract
关键字修饰。 - 接口可以包含属性,但属性必须是抽象的,不能有初始值。
七、常用类
1. String
类
String
类是 Kotlin 中用于表示字符串的类,它提供了许多实用的方法。
kotlin
fun main() {
val str = "Hello, Kotlin!"
// 获取字符串的长度
val length = str.length
println("字符串长度: $length")
// 截取子字符串
val subStr = str.substring(0, 5)
println("截取的子字符串: $subStr")
// 转换为大写
val upperCaseStr = str.toUpperCase()
println("大写字符串: $upperCaseStr")
}
val str = "Hello, Kotlin!"
:定义了一个字符串变量str
。str.length
:获取字符串的长度。str.substring(0, 5)
:截取从索引 0 到索引 5(不包含 5)的子字符串。str.toUpperCase()
:将字符串转换为大写。
2. List
类
List
类是 Kotlin 中用于表示列表的类,它是一个只读的集合。
kotlin
fun main() {
// 创建一个只读列表
val numbers = listOf(1, 2, 3, 4, 5)
// 遍历列表
for (number in numbers) {
println(number)
}
// 获取列表的元素
val firstNumber = numbers[0]
println("第一个元素: $firstNumber")
}
val numbers = listOf(1, 2, 3, 4, 5)
:创建了一个只读列表numbers
。for (number in numbers)
:使用for
循环遍历列表中的元素。numbers[0]
:获取列表中索引为 0 的元素。
3. Map
类
Map
类是 Kotlin 中用于表示键值对映射的类。
kotlin
fun main() {
// 创建一个只读映射
val map = mapOf("apple" to 1, "banana" to 2, "cherry" to 3)
// 遍历映射
for ((key, value) in map) {
println("$key: $value")
}
// 获取映射中的值
val appleValue = map["apple"]
println("apple 的值: $appleValue")
}
val map = mapOf("apple" to 1, "banana" to 2, "cherry" to 3)
:创建了一个只读映射map
。for ((key, value) in map)
:使用for
循环遍历映射中的键值对。map["apple"]
:获取键为"apple"
的值。
八、总结
通过本次课程的学习,我们了解了面向对象的概念,掌握了类与对象的关系,熟悉了 Kotlin 中的常用类。面向对象编程是一种强大的编程范式,通过封装、继承和多态等特性,可以提高代码的可维护性、复用性和扩展性。在实际开发中,我们可以根据具体的需求,合理运用面向对象编程的思想,编写出高质量的代码。