Kotlin 对开发面向对象应用提供了全面支持。不过面向对象编程是一个内容庞大的领域,因此面向对象开发的详细概述超出了本专栏的范围,所以本章将简单介绍面向对象编程涉及的基本概念,然后再展开讲解这些概念与 Kotlin 应用开发的关系。
面向对象

类就是创建对象的模板,类会规定对象中包含哪些属性和方法。
对象就是类的实例,通过访问对象的属性和调用方法来完成相应功能。在面向对象编程中一切皆对象,以对象作为程序的基本单元,将算法和数据封装其中。
面向对象编程思想就是将现实中的复杂事物抽象为对象,通过对象来实现对现实的模拟。
面向对象三大特征:
- 封装:核心思想是将对象的属性和操作封装在一起,对外隐藏内部实现细节。
- 继承:允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码复用、减少冗余并提高代码的可维护性和扩展性
- 多态:同一个接口可以表现出不同的行为。通过多态程序可以在运行时根据实际对象的类型动态调用对应的方法,从而实现灵活性和扩展性,在实际编码中就是父类或接口定义的变量可以指向子类或具体实现类的实例对象。
举个例子,生活中微波炉很常见,我们都会用它加热食物,但是其工作原理很很杂,包含电路知识、磁场、干涉效应等等,但是你不需要懂这些原理却可以轻松使用,因为厂商已经把这些复杂的原理进行封装,暴露在你面前的只有几个按钮,这就是封装 的重要性。在代码中体现为有个微波炉类,属性有加热时间,方法有加热功能,调用者只要调用加热方法而言无需关心怎么加热的。如果微波炉需要加个热牛奶功能,直接继承之前微波炉类而无需重新创建,因为核心功能没变,只需添加一个热牛奶方法供调用者调用,从而实现代码复用。
类
在实例化对象之前,我们首先需要定义该对象的类"。
本章将通过创建一个银行账户类,来演示 Kotlin 面向对象编程的基本概念。
声明一个新的 Kotlin 类时,可指定一个可选的父类(新类将从该父类派生),并定义类的属性和方法。
定义类的基本语法如下:
kotlin
class NewClassName: ParentClass {
// 属性...
// 方法...
}
属性用于定义类中包含的变量和常量,方法用于定义可在类及其实例上调用的方法。
我们将 BankAccount 类定义如下:
kotlin
class BankAccount {
}
定义属性
面向对象编程的一个核心目标是 "数据封装",其核心思想是:数据应存储在类内部,且只能通过该类中定义的方法进行访问。封装在类中的数据被称为属性或实例变量。
BankAccount 类的实例需要存储一些数据,具体来说就是银行账号和账户当前余额。
kotlin
class BankAccount {
/**
* 账号
*/
var accountNumber: Int = 0
/**
* 余额
*/
var accountBalance: Double = 0.0
}
定义方法
类的方法本质上是一些业务代码,这些方法将允许我们操作属性,同时遵循数据封装的模式。
为示例类声明一个显示账户及余额的方法,代码如下:
kotlin
class BankAccount {
/**
* 账号
*/
var accountNumber: Int = 0
/**
* 余额
*/
var accountBalance: Double = 0.0
fun displayBalance() {
println("账号: $accountNumber")
println("余额: $accountBalance")
}
}
实例化对象
目前所做的只是定义了类。要使用这个类需要创建它的实例。
先定义一个变量,用于存储创建实例时的引用,代码如下:
kotlin
val account = BankAccount()
上述代码 BankAccount 类的一个实例将被创建,可通过 account 变量访问该实例。
主构造函数和次构造函数
类在创建实例时,通常需要执行一些初始化操作,这些操作可以通过类中的构造函数来实现。
对于 BankAccount 类来说,创建实例时为账号和余额两个属性初始化具体值,实现这一点可以在类中声明一个次构造函数。
kotlin
class BankAccount {
/**
* 账号
*/
var accountNumber: Int = 0
/**
* 余额
*/
var accountBalance: Double = 0.0
/**
* 次构造函数:初始化账号和余额
* @param accountNumber 账号
* @param accountBalance 余额
*/
constructor(accountNumber: Int, accountBalance: Double) {
this.accountNumber = accountNumber
this.accountBalance = accountBalance
}
fun displayBalance() {
println("账号: $accountNumber")
println("余额: $accountBalance")
}
}
现在创建类的实例时,需要提供账号和余额的初始值。
kotlin
val account: BankAccount = BankAccount(456456234, 342.98)
一个类可以包含多个次构造函数,允许通过不同的构造函数实例化对象。
下面的 BankAccount 类变体新增了一个次构造函数,用于在初始化实例时除了账号和余额外,还传入姓名。
kotlin
class BankAccount {
/**
* 账号
*/
var accountNumber: Int = 0
/**
* 余额
*/
var accountBalance: Double = 0.0
/**
* 姓名
*/
var name: String = ""
/**
* 次构造函数:初始化账号和余额
* @param accountNumber 账号
* @param accountBalance 余额
*/
constructor(accountNumber: Int, accountBalance: Double) {
this.accountNumber = accountNumber
this.accountBalance = accountBalance
}
/**
* 次构造函数:初始化账号、余额、姓名
* @param accountNumber 账号
* @param accountBalance 余额
* @param name 姓名
*/
constructor(accountNumber: Int, accountBalance: Double, name: String) {
this.accountNumber = accountNumber
this.accountBalance = accountBalance
this.name = name
}
fun displayBalance() {
println("账号: $accountNumber")
println("余额: $accountBalance")
}
}
现在也可以用以下方式创建 BankAccount 类的实例。
kotlin
val account: BankAccount = BankAccount(456456234, 342.98, "Ming")
使用主构造函数执行初始化操作,主构造函数在类的头部声明。
注意参数只有加 val/val 才自动成为类的属性,不加只能在 init 代码块和构造函数中访问。
kotlin
class BankAccount(val accountNumber: Int,var accountBalance: Double) {
fun displayBalance() {
println("账号: $accountNumber")
println("当前余额: $accountBalance")
}
}
注意:现在两个属性已在主构造函数中声明,因此无需在类体中重复声明变量。由于账号在实例创建后不会改变,所以用 val 关键字将该属性声明为不可变。
一个类只能有一个主构造函数,但允许在主构造函数之外声明多个次构造函数。 在下面的类声明中,处理账号和余额的构造函数被声明为主构造函数,而额外接收用户姓名的构造函数则被声明为次构造函数。
kotlin
class BankAccount(val accountNumber: Int,var accountBalance: Double) {
/**
* 姓名
*/
var name: String = ""
/**
* 次构造函数:初始化账号、余额、姓名
* @param accountNumber 账号
* @param accountBalance 余额
* @param name 姓名
*/
constructor(accountNumber: Int, accountBalance: Double, name: String) : this(accountNumber, accountBalance) {
this.name = name
}
}
尽管 accountNumber 和 accountBalance 这两个属性是作为次构造函数的参数接收的,但变量的声明仍然由主构造函数处理,因此无需重复声明。不过,为了将次构造函数中这些属性的引用与主构造函数关联起来,必须使用 this 关键字将其关联到主构造函数。
kotlin
: this(accountNumber, accountBalance)
初始化代码块
除了主构造函数和次构造函数,类中还可以包含初始化代码块,它们会在构造函数执行后被调用。由于主构造函数不能包含任何代码,因此初始化代码块就成为创建实例初始化逻辑的位置。初始化块使用 init 关键字声明,初始化代码放在大括号内,示例如下:
kotlin
class BankAccount(val accountNumber: Int, var accountBalance: Double) {
init {
// 初始化代码写在这里
}
}
属性访问和方法调用
回顾下本章内容已创建了一个名为 BankAccount 的 Kotlin 类,在这个类中声明了主构造函数和次构造函数,用于接收并初始化账号、余额、姓名等属性。
接下来,我们将学习如何调用实例方法以及访问类中定义的属性,这可以通过点语法轻松实现。
点语法是指:通过指定类实例,后跟一个点,再跟上属性名或方法名来访问属性或调用方法,格式如下:
kotlin
classInstance.propertyname
classInstance.methodname()
例如,要获取 accountBalance 实例变量的当前值:
kotlin
val balance = account.accountBalance
点语法也可用于设置实例属性的值:
kotlin
account.accountBalance = 6789.98
调用类实例的方法也使用同样的方式。例如,调用 BankAccount 类实例的 displayBalance 方法:
kotlin
account.displayBalance()
自定义访问器
上一节访问 accountBalance 属性时,代码使用的是 Kotlin 自动提供的属性访问器。除了这些默认访问器,我们还可以实现自定义访问器,在返回或设置属性值之前执行计算或其他逻辑。
自定义访问器通过创建 getter 和可选的 setter 实现,这些方法包含返回属性前需要执行的业务逻辑。例如,BankAccount 类可能需要一个额外的属性,用于表示扣除手续费后的当前余额,使用自定义访问器在请求时动态计算该值。修改后的 BankAccount 类如下:
kotlin
class BankAccount(val accountNumber: Int, var accountBalance: Double) {
val fees: Double = 25.00 // 手续费
// 自定义 getter:计算扣除手续费后的余额
val balanceLessFees: Double
get() {
return accountBalance - fees
}
}
上述代码添加了一个 getter,它返回一个基于当前余额减去手续费的计算属性。也可以以类似方式声明一个可选的 setter,用于设置扣除手续费后的余额值:
kotlin
val fees: Double = 25.00
// 带自定义 getter 和 setter 的属性
var balanceLessFees: Double
get() {
return accountBalance - fees
}
set(value) {
accountBalance = value - fees // 设置时从传入值中扣除手续费
}
}
新的 setter 接收一个 Double 类型的参数 value,先从中扣除手续费,再将结果赋值给当前余额属性。尽管是自定义访问器,但其访问方式与存储属性相同,仍使用点语法。以下代码先获取扣除手续费后的当前余额,再为该属性设置新值:
kotlin
val balance1 = account.balanceLessFees // 调用 getter
account.balanceLessFees = 12123.12 // 调用 setter
嵌套类和内部类
Kotlin 允许类嵌套在另一个类内部。例如,在以下代码中,ClassB 嵌套在 ClassA 内部:
kotlin
class ClassA {
class ClassB { // 嵌套类
}
}
在上述示例中,ClassB 无法访问外部类(ClassA)的任何属性。如果需要访问,嵌套类必须用 inner 关键字声明为内部类。在下面的示例中,ClassB 现在可以访问 ClassA 的 myProperty 变量:
kotlin
class ClassA {
var myProperty: Int = 10
inner class ClassB { // 内部类
val result = 20 + myProperty // 可访问外部类的 myProperty
}
}
伴生对象
Kotlin 类中可以包含一个伴生对象(companion object)。伴生对象中包含的方法和变量是该类所有实例共有的。这些属性不仅可在类的实例中访问,还能在类级别直接访问(即无需创建类的实例)。
在类中声明伴生对象的语法如下:
kotlin
class ClassName : ParentClass {
// 属性
// 方法
companion object {
// 伴生对象的属性
// 伴生对象的方法
}
}
可使用 Kotlin 在线编辑器体验伴生对象(https://try.kotl.in),输入以下代码:
kotlin
class MyClass {
fun showCount() {
println("counter = " + counter)
}
companion object {
var counter = 1 // 伴生对象的变量
fun counterUp() { // 伴生对象的方法
counter += 1
}
}
}
fun main(args: Array<String>) {
println(MyClass.counter) // 直接通过类访问伴生对象的变量
}
该类的伴生对象包含一个 counter 变量和一个用于递增该变量的方法,类中还有一个显示当前 counter 值的方法。main() 方法仅打印 counter 变量的值,但它是通过类本身(而非类的实例)调用的。
修改 main() 方法,增加递增 counter 的逻辑,并打印递增前后的值:
kotlin
fun main(args: Array<String>) {
println(MyClass.counter) // 打印初始值
MyClass.counterUp() // 调用伴生对象的方法递增
println(MyClass.counter) // 打印递增后的值
}
运行代码,控制台将输出以下结果:
kotlin
1
2
接下来,添加代码创建 MyClass 的实例,并调用 showCount() 方法:
kotlin
fun main(args: Array<String>) {
println(MyClass.counter)
MyClass.counterUp()
println(MyClass.counter)
val instanceA = MyClass()
instanceA.showCount() // 实例调用方法访问伴生对象的变量
}
执行后,控制台输出如下:
kotlin
1
2
counter = 2
显然,类的实例可以访问伴生对象中包含的变量和方法。
伴生对象另一个实用的特性是:包含它的类的所有实例都共享同一个伴生对象(包括其中变量的当前值)。为验证这一点,创建第二个 MyClass 实例,并调用其 showCount() 方法:
kotlin
fun main(args: Array<String>) {
println(MyClass.counter)
MyClass.counterUp()
println(MyClass.counter)
val instanceA = MyClass()
instanceA.showCount()
val instanceB = MyClass()
instanceB.showCount() // 第二个实例访问伴生对象的变量
}
运行后,控制台输出如下:
kotlin
1
2
counter = 2
counter = 2
注意,两个实例都返回递增后的值 2,这表明两个类实例共享同一个伴生对象的数据。
数据类
数据类(Data Class)主要用于保存数据,数据类使用 data 关键字标记,
编译器会自动为数据类生成一些常用的函数:
- equals()/hashCode() 对。
- toString() 格式是 "User(name=John, age=42)"。
- componentN() 函数 按声明顺序对应于所有属性。
- copy() 函数。
为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:
- 主构造函数必须至少有一个参数。
- 主构造函数的所有参数必须标记为 val 或 var。
- 数据类不能是抽象、开放、密封或者内部的。
示例如下:
kotlin
fun main() {
val person = Person("Ming")
println(person.name)
}
data class Person(var name: String) {
}
类的继承
继承的概念为编程带来了一种贴近现实世界的视角:它允许先定义一个具有特定特征(方法和属性)的类,再基于该类派生其他类。派生类会继承父类的所有特性,并且通常还会添加自身特有的功能。Kotlin 中的所有类最终都是 Any 超类的子类。
通过派生类可以创建所谓的 "类层次结构"。层次结构顶端的类称为基类(base class)或根类(root class),派生类则称为子类(subclass)或子级类(child class)。一个类可以派生出任意数量的子类,而子类所派生的那个类称为父类(parent class)或超类(superclass)。
在 Kotlin 中,一个子类只能继承一个父类,这一概念称为单继承(single inheritance)。
子类语法
Kotlin 提供了降低代码出错概率的安全机制,所以若要从父类派生子类,必须先将父类声明为可继承的(即使用 open 关键字)。只需在类的头部添加 open 关键字即可实现,示例如下:
kotlin
open class MyParentClass { // 父类用 open 声明,允许被继承
var myProperty: Int = 0
}
对于这类简单结构的父类,子类可按以下方式创建:
kotlin
class MySubClass : MyParentClass() { // 用 : 关联父类,() 表示调用父类无参构造
}
若父类包含主构造函数或次构造函数,子类的创建规则会稍复杂一些。先来看包含主构造函数的父类示例:
kotlin
open class MyParentClass(var myProperty: Int) { // 父类带主构造函数(接收 Int 参数)
}
要为该父类创建子类,子类声明需引用父类的参数,同时通过以下语法初始化父类:
kotlin
class MySubClass(myProperty: Int) : MyParentClass(myProperty) { // 子类参数传递给父类主构造函数
}
另一方面,若父类包含一个或多个次构造函数,则子类声明中也必须实现对应的构造函数,且要调用父类的次构造函数,并将子类次构造函数接收的参数作为实参传递过去。在子类中,可通过 super 关键字引用父类。
先定义一个带次构造函数的父类:
kotlin
open class MyParentClass {
var myProperty: Int = 0
// 父类的次构造函数(接收 Int 参数)
constructor(number: Int) {
myProperty = number
}
}
对应的子类代码需按以下方式实现:
kotlin
class MySubClass : MyParentClass { // 父类无主构造,此处不写 ()
// 子类次构造函数,通过 super(number) 调用父类次构造
constructor(number: Int) : super(number)
}
若需要在子类的构造函数中执行额外操作,可将代码放在构造函数声明后的大括号内:
kotlin
class MySubClass : MyParentClass {
constructor(number: Int) : super(number) {
// 此处编写子类构造函数的额外逻辑
}
}
继承示例
之前我们创建了一个名为 BankAccount 的类,用于存储银行账号和对应的当前余额。该类包含属性和方法,其简化声明如下,本章的子类化示例将以此为基础展开:
kotlin
class BankAccount {
var accountNumber = 0 // 账号属性
var accountBalance = 0.0 // 余额属性
// 次构造函数:初始化账号和余额
constructor(number: Int, balance: Double) {
accountNumber = number
accountBalance = balance
}
// 显示余额的方法
fun displayBalance() {
println("账号: $accountNumber")
println("当前余额: $accountBalance")
}
}
虽然这个类功能相对基础,不过假设除了 BankAccount 类,我们还需要一个用于 "储蓄账户" 的类。储蓄账户同样需要存储账号和当前余额,也需要访问这些数据的方法。此时有两种选择:
- 完全创建一个新类,复制 BankAccount 类的所有功能,再添加储蓄账户所需的新特性;
- 更高效的方式:创建一个 BankAccount 类的子类,该子类会继承 BankAccount 的所有功能,再扩展添加储蓄账户特有的功能。
要创建 BankAccount 类的子类,首先需要修改其声明,用 open 关键字标记为 "可继承":
kotlin
open class BankAccount { // 添加 open,允许被继承
// 类内容与前文一致
var accountNumber = 0
var accountBalance = 0.0
constructor(number: Int, balance: Double) {
accountNumber = number
accountBalance = balance
}
fun displayBalance() {
println("账号: $accountNumber")
println("当前余额: $accountBalance")
}
}
接下来,创建一个名为 SavingsAccount 的子类,指定 BankAccount 为父类,并在子类中调用父类的构造函数:
kotlin
class SavingsAccount : BankAccount {
// 子类次构造函数:通过 super 调用父类的次构造函数
constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance)
}
虽然我们没有为 SavingsAccount 添加任何新属性或方法,但它已继承了父类 BankAccount 的所有属性和方法。因此我们可以像使用 BankAccount 类一样创建 SavingsAccount 的实例、设置变量和调用方法。不过只有为其扩展新功能才能真正体现子类作用。
扩展子类的功能
目前为止我们已经创建了一个包含父类所有功能的子类。但要让这个子类真正有意义,还需要对其进行扩展,添加存储储蓄账户信息所需的特有功能。实现方式很简单:像创建其他类一样直接为子类添加提供新功能的属性和方法即可,示例如下:
kotlin
class SavingsAccount : BankAccount {
var interestRate: Double = 0.0 // 储蓄账户特有属性:年利率
// 子类构造函数:调用父类构造初始化账号和余额
constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance)
// 储蓄账户特有方法:计算利息(利息 = 年利率 × 账户余额)
fun calculateInterest(): Double {
return interestRate * accountBalance
}
}
重写继承的方法
使用继承时,常会遇到父类的某个方法 "大致符合需求但需要修改才能实现特定功能" 的情况;也可能遇到方法名完全符合需求,但实现逻辑与预期相差甚远的情况。这种场景下,一种选择是忽略继承的方法,重新写一个全新名称的方法;但更优的选择是重写(override)该继承方法,在子类中编写新的实现版本。
在看示例前,必须遵守重写方法的三条规则:
- 子类中的重写方法,必须与父类中被重写方法的参数数量和类型完全一致;
- 重写方法的返回值类型,必须与父类方法相同;
- 父类中的原方法,必须用 open 关键字声明为 "可重写",编译器才允许子类重写。
示例如下:
kotlin
class SavingsAccount : BankAccount {
var interestRate: Double = 0.0 // 储蓄账户特有:年利率
// 调用父类构造函数初始化账号和余额
constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance)
// 储蓄账户特有:计算利息
fun calculateInterest(): Double {
return interestRate * accountBalance
}
// 重写父类的 displayBalance 方法,新增年利率显示
override fun displayBalance() {
println("账号: $accountNumber")
println("当前余额: $accountBalance")
println("当前年利率: $interestRate") // 子类新增的显示逻辑
}
}
要让上述代码通过编译,必须先将 BankAccount 类中的 displayBalance 方法声明为 "可重写"(添加 open 关键字):
kotlin
// 父类方法添加 open,允许子类重写
open fun displayBalance() {
println("账号: $accountNumber")
println("当前余额: $accountBalance")
}
此外还可以在子类的重写方法中,调用父类中被重写的方法。例如,可先调用父类的 displayBalance 方法显示账号和余额,再补充显示年利率,从而避免代码重复:
kotlin
override fun displayBalance() {
super.displayBalance() // 调用父类的 displayBalance 方法(显示账号和余额)
println("当前年利率: $interestRate") // 子类补充显示年利率
}
自定义次构造函数
当前 SavingsAccount 类的构造函数,会调用父类 BankAccount 的次构造函数,代码如下:
kotlin
constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance)
显然,这个构造函数能完成账号和余额属性的初始化,但 SavingsAccount 类还有一个额外属性 interestRate(年利率),因此SavingsAccount 类需要自己的构造函数,确保在创建实例时能初始化 interestRate 属性。
最后修改 SavingsAccount 类,新增一个次构造函数,允许在初始化实例时同时指定年利率:
kotlin
class SavingsAccount : BankAccount {
var interestRate: Double = 0.0 // 储蓄账户特有属性:年利率
// 构造函数1:仅初始化账号和余额(调用父类构造)
constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance)
// 构造函数2(新增):初始化账号、余额和年利率
constructor(accountNumber: Int, accountBalance: Double, rate: Double) : super(accountNumber, accountBalance) {
interestRate = rate // 初始化子类特有属性:年利率
}
.
.
}
使用 SavingsAccount 类
我们已经完成了 SavingsAccount 类的编写,现在可以像使用父类 BankAccount 一样,在示例代码中使用它。示例如下:
kotlin
// 使用新增的构造函数创建 SavingsAccount 实例:账号12311、余额600.00、年利率0.07
val savings1 = SavingsAccount(12311, 600.00, 0.07)
// 调用子类特有方法:计算利息
println(savings1.calculateInterest())
// 调用重写后的方法:显示账号、余额和年利率
savings1.displayBalance()
小结
像 Kotlin 这样的面向对象编程语言,鼓励通过创建类来提升代码复用性,并将数据封装在类的实例中。本章涵盖了 Kotlin 中类与实例的基本概念,同时概述了主构造函数、次构造函数、初始化块、属性、方法、伴生对象、数据类以及自定义访问器等相关内容。
继承通过允许从现有类派生出新类,并在新类中扩展以添加新功能,进一步延伸了面向对象编程中 "对象复用" 的概念。当某个现有类能提供程序员所需的部分功能(而非全部)时,继承可将该类作为基础,用于创建新的子类。新子类会继承父类的所有能力,同时还能通过扩展补充缺失的功能。但随着时间推移现在在更推荐多用组合少用继承(😛)。