第11章 面向对象编程
Kotlin语言目前还是以面向对象编程为主,函数式编程为辅。面向对象是Kotlin是重要的特性之一。本章将介绍Kotlin面向对象编程知识。
11.1 面向对象概述
面向对象的编程思想:按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建的软件系统就存在什么样的实体。
例如:在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作。学生只是抽象的描述,这个抽象的描述称为“类”。在学校里活动是学生个体,即:张同学、李同学等,这些具体的个体称为“对象”,“对象”也称为“实例”。
在现实世界有类和对象,面向对象软件世界也会有,只不过它们会以某种计算机语言编写的程序代码形式存在,这就是面向对象编程(Object Oriented Programming,OOP)。
11.2 面向对象三个基本特性
面向对象思想有三个基本特性:封装性、继承性和多态性。
11.2.1 封装性
在现实世界中封装的例子到处都是。例如:一台计算机内部极其复杂,有主板、CPU、硬盘和内存,而一般用户不需要了解它的内部细节,不需要知道主板的型号、CPU主频、硬盘和内存的大小,于是计算机制造商将用机箱把计算机封装起来,对外提供了一些接口,如鼠标、键盘和显示器等,这样当用户使用计算机就变非常方便。
那么,面向对象的封装与真实世界的目的是一样的。封装能够使外部访问者不能随意存取对象的内部数据,隐藏了对象的内部细节,只保留有限的对外接口。外部访问者不用关心对象的内部细节,使得操作对象变得简单。
11.2.2 继承性
在现实世界中继承也是无处不在。例如:轮船与客轮之间的关系,客轮是一种特殊轮船,拥有轮船的全部特征和行为,即数据和操作。在面向对象中轮船是一般类,客轮是特殊类,特殊类拥有一般类的全部数据和操作,称为特殊类继承一般类。在面向对象计算机语言中一般类称为“父类”或“超类”,特殊类称为“子类”或“派生类”,本书采用“父类”和“子类”提法。
11.2.3 多态性
多态性是指在父类中成员变量和成员函数被子类继承之后,可以具有不同的状态或表现行为。有关多态性会在12.4节详细解释,这里不再赘述。
11.3 类声明
类是Kotlin中的一种重要的数据类型,是组成Kotlin程序的基本要素。它封装了一类对象的数据和操作。为了方便使用Kotlin中的类有很多种形式:标准类、枚举类、数据类、内部类、嵌套类和密封类等,此外还有抽象类和接口。
Kotlin中的类声明的语法与Java非常相似。使用class关键词声明,它们的语法格式如下:
class 类名 {
声明类的成员
}
Kotlin中的类成员包括:
o 构造函数
o 初始化代码块
o 成员函数
o 属性
o 内部类和嵌套类
对象表达式声明
声明动物(Animal)类代码如下:
class Animal {
//类体
}
上述代码声明了动物(Animal)类,大括号中是类体,如果类体中没有任何的成员,可以省略大括号。代码如下:
class Animal
类体一般都会包括一些类成员,下面看一个声明属性示例:
class Animal {
// 动物年龄
var age = 1
// 动物性别
var sex = false
// 动物体重
private val weight = 0.0
}
下面看一个声明成员函数示例:
class Animal {
// 动物年龄
var age = 1
// 动物性别
var sex = false
// 动物体重
private val weight = 0.0
private fun eat() { ①
// 函数体
}
fun run(): Int { ②
// 函数体
return 10
}
fun getMaxNumber(n1: Int, n2: Int) = if (n1 > n2) n1 else n2 ③
}
上述代码第①、②、③行声明了三个成员函数。成员函数在类中声明的函数,它的声明与顶层函数没有区别,只是在调用时需要类的对象才能调用,示例代码如下:
//代码文件:chapter11/src/com/a51work6/section3/ch11.3.1.kt
package com.a51work6.section3
fun main(args: Array) {
val animal = Animal() ①
println(animal.getMaxNumber(12,16))//16 ②
}
上述代码第①行中Animal()表达式是实例化Animal类,创建一个animal对象。创建对象与Java相比省略了new关键字,与Swift相同。代码第②行是通过animal对象调用getMaxNumber成员函数。
11.4 属性
属性是为了方便访问封装后的字段而设计的,属性本身并不存储数据,数据是存储在支持字段(backing field)中的。
11.4.1 回顾JavaBean
JavaBean 是一种Java语言的可重用组件技术,它能够与JSP(Java Server Page)标签绑定,很多Java框架也使用JavaBean。JavaBean的字段(成员变量)往往被封装称为私有的,为了能够在类的外部访问这些字段,则需要通过getter和setter访问器访问。动物(Animal)类Java代码如下:
//代码文件:chapter11/src/com/a51work6/section4/s1/Animal.java
package com.a51work6.section4;
public class Animal {
// 动物年龄
private int age = 1; ①
// 动物性别
private boolean sex = false; ②
public int getAge() { ③
return age;
}
public void setAge(int age) { ④
this.age = age;
}
public boolean isSex() { ⑤
return sex;
}
public void setSex(boolean sex) { ⑥
this.sex = sex;
}
}
上述Java代码中有两个字段age和sex,见代码第①行和第②行。sex字段是布尔类型,为了访问私有字段age,需要提供getter访问器(见代码第③行),setter访问器(见代码第④行)。getter访问器是一个函数,它的命名规则是:get+第一个字母大写的字段。setter访问器也是一个函数,它的命名规则是:set+第一个字母大写的字段。但如果是布尔类型字段,getter访问器它的命名规则是:is+第一个字母大写的字段。
如果使用Kotlin语言同样的类,代码如下:
//代码文件:chapter11/src/com/a51work6/section4/s1/Animal.kt
package com.a51work6.section4
class Animal {
// 动物年龄
var age = 1
// 动物性别
var sex = false
}
可见Kotlin代码非常的简洁,注意上述Animal类中的age和sex不是字段而属性,一个属性对应一个字段,以及 setter和getter访问器,如果是只读属性则没有setter访问器。
11.4.2 声明属性
Kotlin中声明属性的语法格式如下:
var|val 属性名
[ : 数据类型] [= 属性初始化 ]
[getter访问器]
[setter访问器]
从上述属性语法可见,属性最基本形式与声明一个变量或常量是一样的。val所声明的属性是只读属性。如果需要还可以重写属性的setter和getter访问器。
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section4/s2/Employee.kt
package com.a51work6.section4.s2
// 员工类
class Employee {
var no: Int = 0 // 员工编号属性
var job: String? = null // 工作属性 ①
var firstName: String =“Tony” ②
var lastName: String =“Guan” ③
var fullName: String //全名 ④
get() { ⑤
return firstName +"." + lastName
}
set (value) { ⑥
val name =value.split(".") ⑦
firstName = name[0]
lastName = name[1]
}
var salary: Double = 0.0 // 薪资属性 ⑧
set(value) {
if (value >= 0.0) field =value ⑨
}
}
//代码文件:chapter11/src/com/a51work6/section4/s2/ch11.4.2.kt
package com.a51work6.section4.s2
fun main(args: Array) {
val emp = Employee()
println(emp.fullName)//Tony.Guan
emp.fullName = "Tom.Guan"
println(emp.fullName)//Tom.Guan
emp.salary = -10.0 //不接收负值
println(emp.salary)//0.0
emp.salary = 10.0
println(emp.salary)//10.0
}
上述代码第①行是声明员工的job它是一个可空字符串类型。代码第②行是声明员工的firstName属性,第③行代码是声明员工的lastName属性。代码第④行的声明全名属性fullName,fullName属性值是通过firstName属性和lastName属性拼接而成。代码第⑤行重写getter访问器,可以写成表达式形式。
get() = firstName + “.” + lastName
代码第⑥行是重写setter访问器,value是新的属性值,代码⑦行是通过String的split函数分割字符串,返回的是String数组。
代码第⑧行是声明salary薪资属性,薪资是不能为负数的,这里重写了setter访问器。代码第⑨行的判断如果薪水大于等于0.0 时,才将新的属性值赋值给field变量,field变量是访问支持字段(backing field),属于field软关键字。
11.4.3 延迟初始化属性
假设公司管理系统中两个类Employee(员工)和Department(部门),它们的类图如图11-1所示,它们有关联关系,Employee所在部门的属性dept与Department关联起来。这种关联关系体现为:一个员工必然隶属于一个部门,一个员工实例对应于一个部门实例。
下面看一下代码示例:
//代码文件:chapter11/src/com/a51work6/section4/s3/Employee.kt
package com.a51work6.section4.s3
// 员工类
class Employee {
…
var dept = Department() // 所在部门属性 ①
}
// 部门类
class Department {
var no: Int = 0 // 部门编号属性
var name: String = “” // 部门名称属性
}
//代码文件:chapter11/src/com/a51work6/section4/s3/ch11.4.3.kt
package com.a51work6.section4.s3
fun main(args: Array) {
val emp = Employee()
…
println(emp.dept)
}
在创建Employee对象时,需要同时需要实例化Employee的所有属性,也包括实例化dept(部门)属性,代码第①行声明dept属性的同时进行了初始化,创建Department对象。如果是一个新入职的员工,有时不关心员工在哪个部门,只关心他的no(编号)和name(姓名)。但上述代码虽然不使用dept对象,但是仍然会实例化它,这样会占用内存。Kotlin可以对属性设置为延迟初始化的,修改代码如下:
//代码文件:chapter11/src/com/a51work6/section4/s3/Employee.kt
package com.a51work6.section4.s3
// 员工类
class Employee {
...
lateinit var dept: Department // 所在部门属性 ①
}
// 部门类
class Department {
var no: Int = 0 // 部门编号属性
var name: String = “” // 部门名称属性
}
//代码文件:chapter11/src/com/a51work6/section4/s3/ch11.4.3.kt
package com.a51work6.section4.s3
fun main(args: Array) {
val emp = Employee()
…
emp.dept = Department()
println(emp.dept)
}
在代码第①行在声明dept属性前面添加了关键字lateinit,这样dept属性就是延时初始化。顾名思义,延时初始化属性就是不必在类实例化时初始化它,可以根据需要在程序运行期初始化。而没有lateinit声明的非可空类型属性必须在类实例化时初始化。
11.4.4 委托属性
Kotlin提供一种委托属性,使用by关键字声明,示例代码如下:
//代码文件:chapter11/src/com/a51work6/section4/s4/ch11.4.4.kt
package com.a51work6.section4.s4
import kotlin.reflect.KProperty
class User {
var name:String by Delegate() ①
}
class Delegate {
operator fun
getValue(thisRef: Any, property: KProperty<*>): String = property.name ②
operator fun
setValue(thisRef: Any?, property: KProperty<*>, value: String) { ③
println(value)
}
}
fun main(args: Array) {
val user =User()
user.name ="Tom" ④
println(user.name) ⑤
}
运行结果
Tom
name
上述代码第①行是声明委托属性,by是委托运算符,它后面的Delegate()就是属性name的委托对象,通过by运算符属性name的setter访问器被委托给Delegate对象的setValue函数,属性name的getter访问器被委托给Delegate对象的getValue函数。Delegate对象不必实现任何接口,只需要实现getValue和 setValue函数即可,见代码第②行和第③行。注意这两个函数前面都有operator关键字修饰,operator所修饰的函数是运算符重载函数,本例中说明了getValue和 setValue函数重载by运算符。
代码第④行给name属性赋值,这会调用委托对象的setValue函数,代码第⑤行是读取name数组值,这会调用委托对象的getValue函数。
11.4.5 惰性加载属性
实际开发中自己声明委托属性很少使用,而是通过使用Kotlin标准库中提供的一些委托属性,如:惰性加载属性和可观察属性,本节先介绍惰性加载属性。
惰性加载属性与延迟初始化属性类似,只有第一次访问该属性时才进行初始化。不同的是惰性加载属性使用的lazy函数声明委托属性,而延迟初始化属性lateinit关键字修饰属性。还有惰性加载属性必须是val的,而延迟初始化属性必须是var的。
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section4/s5/Employee.kt
package com.a51work6.section4.s5
// 员工类
open class Employee {
var no: Int =0 // 员工编号属性
var firstName:String = "Tony"
var lastName:String = "Guan"
val fullName:String by lazy { ①
firstName +"." + lastName
}
lateinit var dept: Department ②
}
// 部门类
class Department {
var no: Int =0 // 部门编号属性
var name:String = “” // 部门名称属性
}
//代码文件:chapter11/src/com/a51work6/section4/s5/ch11.4.5.kt
package com.a51work6.section4.s5
fun main(args: Array) {
val emp =Employee()
println(emp.fullName