《Kotlin从小白到大牛》第11章:面向对象编程

第11章 面向对象编程

Kotlin语言目前还是以面向对象编程为主,函数式编程为辅。面向对象是Kotlin是重要的特性之一。本章将介绍Kotlin面向对象编程知识。

11.1 面向对象概述

面向对象的编程思想:按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建的软件系统就存在什么样的实体。
例如:在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作。学生只是抽象的描述,这个抽象的描述称为“类”。在学校里活动是学生个体,即:张同学、李同学等,这些具体的个体称为“对象”,“对象”也称为“实例”。
在现实世界有类和对象,面向对象软件世界也会有,只不过它们会以某种计算机语言编写的程序代码形式存在,这就是面向对象编程(Object Oriented Programming,OOP)。
《Kotlin从小白到大牛》第11章:面向对象编程_第1张图片

11.2 面向对象三个基本特性

面向对象思想有三个基本特性:封装性、继承性和多态性。

11.2.1 封装性
在现实世界中封装的例子到处都是。例如:一台计算机内部极其复杂,有主板、CPU、硬盘和内存,而一般用户不需要了解它的内部细节,不需要知道主板的型号、CPU主频、硬盘和内存的大小,于是计算机制造商将用机箱把计算机封装起来,对外提供了一些接口,如鼠标、键盘和显示器等,这样当用户使用计算机就变非常方便。
那么,面向对象的封装与真实世界的目的是一样的。封装能够使外部访问者不能随意存取对象的内部数据,隐藏了对象的内部细节,只保留有限的对外接口。外部访问者不用关心对象的内部细节,使得操作对象变得简单。

11.2.2 继承性
在现实世界中继承也是无处不在。例如:轮船与客轮之间的关系,客轮是一种特殊轮船,拥有轮船的全部特征和行为,即数据和操作。在面向对象中轮船是一般类,客轮是特殊类,特殊类拥有一般类的全部数据和操作,称为特殊类继承一般类。在面向对象计算机语言中一般类称为“父类”或“超类”,特殊类称为“子类”或“派生类”,本书采用“父类”和“子类”提法。
《Kotlin从小白到大牛》第11章:面向对象编程_第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关联起来。这种关联关系体现为:一个员工必然隶属于一个部门,一个员工实例对应于一个部门实例。
《Kotlin从小白到大牛》第11章:面向对象编程_第3张图片
下面看一下代码示例:
//代码文件: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)//Tony.Guan

val dept =Department()
dept.no = 20 

emp.dept = dept
println(emp.dept)

}
上述代码第①行声明了的惰性加载属性fullName,by后面是lazy函数,注意lazy不是关键字,而是函数。lazy函数后面跟着的是尾随Lambda表达式。惰性加载属性使用val声明。
代码第②行声明了延迟初始化属性dept,使用关键字lateinit。延迟初始化属性使用var声明。

11.4.6 可观察属性
另一个使用委托属性示例是可观察属性,委托对象监听属性的变化,当属性变化时委托对象会被触发。
//代码文件:chapter11/src/com/a51work6/section4/s6/Department.kt
package com.a51work6.section4.s6

import kotlin.properties.Delegates

// 部门类
class Department {
var no: Int =0 // 部门编号属性
var name:
String by Delegates.observable("<无>") { p, oldValue, newValue -> ①

println("$oldValue -> $newValue")
}
}

//代码文件:chapter11/src/com/a51work6/section4/s6/ch11.4.6.kt
package com.a51work6.section4.s6

fun main(args: Array) {

val dept =Department()
dept.no = 20
dept.name ="技术部"
//输出<无> -> 技术部           ②
dept.name ="市场部"      //输出技术部 -> 市场部        ③ 

}
上述代码第①行是声明name委托属性,by关键字后面Delegates.observable()函数有两个参数:第一个参数是委托属性的初始化值,第二个参数是属性变化事件的响应器,响应器是函数类型,具体调用时可使用Lambda表达式作为实际参数。在用Lambda表达式中有三个参数,其中p是属性,oldValue是属性的旧值,newValue是属性的新值。

11.5 扩展

在面向对象分析与设计方法学(OOAD)中,为了增强一个类的新功能,可以通过继承机制从父类继承一些函数和属性,然后再根据需要在子类中添加一些函数和属性,这样就可以得到增强功能的新类了。但是这种方式受到了一些限制,继承过程比较烦琐,类继承性可能被禁止,有些功能也可能无法继承。
在Kotlin中可以使用一种扩展机制,在原始类型的基础上添加新功能。扩展是一种“轻量级”的继承机制,即使原始类型被限制继承,仍然可以通过扩展机制增强原始类型的功能。Kotlin中可以扩展原始类型的函数和属性,原始类型称为“接收类型”。扩展必须针对某种接收类型,所以顶层函数和属性没有扩展。
在这里插入图片描述
11.5.1 扩展函数
在接收类型上扩展函数,具体语法如下:
fun 接收类型.函数名(参数列表) : 返回值类型 {
函数体
return 返回值
}
可见扩展函数与普通函数区别是函数名前面加上“接收类型.”。 接收类型可以是任何Kotlin数据类型,包括基本数据类型和引用类型。示例代码如下:
//代码文件:chapter11/src/com/a51work6/section5/s1/ch11.5.1.kt
package com.a51work6.section5.s1

//基本数据类型扩展
fun Double.interestBy(interestRate: Double): Double { ①
return this * interestRate
}

//自定义账户类
class Account {
var amount: Double = 0.0 //账户金额
var owner: String = “” //账户名
}

//账户类扩展函数
fun Account.interestBy(interestRate: Double): Double { ②
return this.amount * interestRate
}

fun main(args: Array) {

val interest1 =10_000.00.interestBy(0.0668)           ③
println("利息1: $interest1")

val account = Account()
val interest2 =account.interestBy(0.0668)              ④
println("利息2: $interest2")

}
上述代码第①行是声明基本数据Double扩展函数,代码第②行是声明自定义类Account扩展函数。在两个扩展函数中都使用了this关键字,this表示当前类型的接收对象。

11.5.2 扩展属性
在接收类型上扩展属性,具体语法如下:
var|val 接收类型.属性名 [ : 数据类型]
[getter访问器]
[setter访问器]
可见扩展属性与普通属性在声明时区别是属性名前面加上“接收类型.”。接收类型可以是任何Kotlin数据类型,包括基本数据类型和引用类型。
在这里插入图片描述
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section5/s2/ch11.5.2.kt
package com.a51work6.section5.s2

// 部门类
class Department {
var no: Int = 0 // 部门编号属性
var name: String = “” // 部门名称属性
}

var Department.desc: String ①
get() {
return “Department [no= t h i s . n o , n a m e = {this.no}, name= this.no,name={this.name}]”
}

set(value) {
    println(value)
    //println(field)//编译错误    ②
} 

val Int.errorMessage: String ③
get() = when (this) {
-7 -> “没有数据。”
-6 -> “日期没有输入。”
-5 -> “内容没有输入。”
-4 -> “ID没有输入。”
-3 -> “数据访问失败。”
-2 -> “您的账号最多能插入10条数据。”
-1 -> “用户不存在,请到http://51work6.com注册。”
else -> “”
}

fun main(args: Array) {

val message = (-7).errorMessage ④
println("Error Code: -7 , Error Message:  $message")     

val dept = Department()
dept.name="智捷课堂"
dept.no = 100 

println(dept.desc)
}
运行结果
Error Code: -7 , Error Message: 没有数据。
Department [no=100, name=智捷课堂]
上代码第①行是声明一个扩展属性desc,它的接收类型是部门类Department,可见desc属性没有初始化,代码第②行是试图使用field变量,会发生编译错误。
代码第③行是声明Int类型的扩展属性errorMessage,errorMessage属性用于将错误编码转换为错误描述信息,其中使用了when表达式。代码第④行是访问errorMessage属性,(-7).errorMessage是获得-7编码对应的错误描述信息。注意整个-7包括负号是一个完整的Int对象,因此调用它的属性时需要将-7作为一个整体用小括号括起来。

11.5.3 “成员优先”原则
无论是扩展属性还是扩展函数,如果接收类型成员中已经有相同的属性和函数,那么在调用属性和函数时,始终是调用接收类型的成员属性和函数。这就是“成员优先”原则。
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section5/s3/ch11.5.3.kt
package com.a51work6.section5.s3

// 部门类
class Department {
var no: Int = 0 // 部门编号属性
var name: String = “” // 部门名称属性
var desc: String = “成员:${no} - ${name}” // 描述属性 ①

fun display() : String {                ②
    return  "成员: [no=${this.no}, name=${this.name}]"
}

}

val Department.desc: String ③
get() {
return “扩展: [no= t h i s . n o , n a m e = {this.no},name= this.no,name={this.name}]”
}

fun Department.display() : String { ④
return “扩展: [no= t h i s . n o , n a m e = {this.no}, name= this.no,name={this.name}]”
}

fun Department.display(f: String): String { ⑤
return "扩展: f , [ n o = f, [no= f,[no={this.no},name=${this.name}] "
}

fun main(args: Array) {

val dept = Department()
dept.name = "智捷课堂"
dept.no = 100

println(dept.desc)            ⑥
println(dept.display())       ⑦
println(dept.display("My"))⑧  

}
输出结果
成员:0 -
成员: [no=100, name=智捷课堂]
扩展: My, [no=100, name=智捷课堂]
上述代码第③行是声明一个扩展属性desc,读者发现代码第①行也有一个相同的名字desc属性。在代码第⑥行调用desc属性,实际上调用的是desc成员属性。
代码第④行是声明一个扩展函数display,读者发现代码第②行也有一个相同的函数(函数名和参数列表全部相同)。在代码第⑦行调用display函数,实际上调用的是display成员函数。但是如果只是函数名相同,而参数列表不同,见代码第⑤行的display(f: String)扩展函数。在接收类型中没有display(f: String)成员函数,因此在代码第⑧行调用的扩展函数。

11.5.4 定义中缀运算符
在前面的学习过程中已经接触到中缀运算符,中缀运算符本质上是一个函数。程序员也可以定义自己的中缀运算符。
在这里插入图片描述
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section5/s4/ch11.5.4.kt
package com.a51work6.section5.s4

//定义中缀函数interestBy
infix fun Double.interestBy(interestRate: Double): Double
{ ①
return this *interestRate
}

// 部门类
class Department { ②
var no: Int = 10

//定义中缀函数rp
infix fun rp(times: Int) {     ③       

repeat(times) {
println(no)
}
}
}

fun main(args: Array) {

//函数调用
val interest1 =10_000.00.interestBy(0.0668)
println("利息1: $interest1")

//中缀运算符interestBy
val interest2 =10_000.00 interestBy 0.0668           ④
println("利息1: $interest2")

val dept =Department() ⑤
dept rp 3 //中缀运算符rp
}
输出结果
利息1: 668.0
利息1: 668.0
10
10
10
上述代码第①行声明中缀函数使用关键字infix,它是Double的扩展函数,它有一个参数。在调用时函数名interestBy作为中缀运算符,见代码第④行。
代码第②行是声明一个部门类Department,它的成员函数rp也声明中缀函数,见代码第③行。代码第⑤行是使用中缀运算符rp。
在这里插入图片描述

11.6 构造函数

在11.3节使用了表达式Animal(),后面的小括号是调用构造函数。构造函数是类中特殊函数,用来初始化类的属性,它在创建对象之后自动调用。在Kotlin中构造函数有主次之分,主构造函数只能有一个,次构造函数可以有多个。

11.6.1主构造函数
主构造函数涉及到两个关键字constructor和init。主构造函数在类头中、类名的后面声明,使用关键字constructor。示例代码如下:
//代码文件:chapter11/src/com/a51work6/section6/Rectangle.kt
package com.a51work6.section6

class Rectangle constructor(w: Int, h: Int) { ①
// 矩形宽度
var width: Int ②
// 矩形高度
var height: Int ③
// 矩形面积
var area: Int ④

init {   //初始化代码块     ⑤      
    width = w
    height = h
    area = w * h// 计算矩形面积
}

}
Rectangle是一个矩形类,它有三个属性见代码第②~第④行。代码第①行是类头声明,其中constructor(w:
Int, h: Int)主构造函数声明,主构造函数本身不能包含代码的,所需要借助于初始化代码块,见代码第⑤行的init代码块,在init代码块中可以进行主构造函数需要的初始化处理。
对于Kotlion语言的设计者们会觉得这样的代码很臃肿,需要进行简化。首先可以将属性与主构造函数的参数合并,这样在函数体中就不需要属性声明了。示例代码如下:
//代码文件:chapter11/src/com/a51work6/section6/Rectangle.kt
package com.a51work6.section6
class Rectangle constructor(var width: Int, var height:
Int) {
// 矩形面积
var area: Int

init {//初始化代码块
    area = width * height// 计算矩形面积
}

}
Rectangle的width和height属性声明不在函数体中,而是放到了主构造函数的参数中,此时主构造函数的参数前面需要使用val或var声明。Kotlin编译器会根据主构造函数的参数列表生成相应的属性。如果所有的属性都在主构造函数中初始化,可以省略init代码块,示例代码如下:
//代码文件:chapter11/src/com/a51work6/section6/User.kt
package com.a51work6.section6

class User constructor(val name: String, var password: String)
上述代码是声明一个User类,它只有两个属性,它们都是在主构造函数中声明的,这样可以省略init代码块。这样类体中没有代码可以省略大括号。
在这里插入图片描述
省略后的User代码如下:
class User(val name: String, var password: String)
可见省略了constructor关键字的User类声明非常简单,这是最简单形式的类声明了。但需要注意的是,下面的User类不能省略constructor关键字,因为User前面private可见行修饰符。
class User private constructor(val name: String, var
password: String)
主构造函数与普通函数类似,可以声明带有默认值的参数,这样一来虽然只有一个主构造函数,但调用时可以省略一些参数,类似于多个构造函数重载。示例代码如下:
//代码文件:chapter11/src/com/a51work6/section6/Animal.kt
package com.a51work6.section6

class Animal(val age: Int = 0, val sex: Boolean = false)
上述代码声明了Animal类,它的主构造函数有两个参数,这个两个参数都有默认值。调用代码如下:
//代码文件:chapter11/src/com/a51work6/section6/ch11.6.kt
package com.a51work6.section6

fun main(args: Array) {
val animal1 = Animal()
val animal2 = Animal(10)
val animal3 = Animal(sex = true)
val animal4 = Animal(10,true)
}
在main函数中创建了4个Animal对象,都是使用同一个主构造函数,只是它们省略了不同参数。

11.6.2 次构造函数
由于主构造函数只能有一个,而且初始化时只有init代码块,有时候不够灵活,这时可以使用次构造函数。次构造函数是在函数体中声明的,使用关键字constructor声明,代码如下:
//代码文件:chapter11/src/com/a51work6/section6/Rectangle.kt
package com.a51work6.section6

class Rectangle(var width: Int, var height: Int) {
// 矩形面积
var area: Int

init {//初始化代码块
    area = width * height// 计算矩形面积
}

constructor(width: Int, height: Int,area: Int) : this(width, height) {        ①
    this.area = area
}

constructor(area: Int) : this(200,100) {//width=200 height=100          ②
    this.area = area
}

}
上述代码第①行和第②行分别声明了两个次构造函数,次构造函数需要调用主构造函数初始化部分属性,次构造函数后面的this(width, height)和this(200, 100)表达式就是调用当前对象的主构造函数。另外,当属性命名与参数命名有冲突时候,属性可以加上this.前缀,this表示当前对象。
调用Rectangle代码如下:
//代码文件:chapter11/src/com/a51work6/section6/ch11.6.1.kt
package com.a51work6.section6

fun main(args: Array) {

val rect1 = Rectangle(100, 90)
val rect2 =Rectangle(10, 9,900)
val rect3 =Rectangle(20000)

}

11.6.3 默认构造函数
如果一个非抽象类中根本看不到任何的构造函数,编译器会为其生成一个默认的构造函数,即无参数public的主构造函数。修改11.6.1节的User类代码如下:
//代码文件:chapter11/src/com/a51work6/section6/User.kt

//默认构造函数
class User {
// 用户名
val username: String?
// 用户密码
val password: String?

init {
    username =null
    password =null
}

}
从上述User类代码,只有两个属性,看不到任何的构造函数,但还是可以调用无参数的构造函数创建User对象,代码如下:
//创建User对象
val user = User()

11.7 封装性与可见性修饰符

Kotlin可见性有4种:公有、内部、保护和私有。具体规则如表11-1所示。
《Kotlin从小白到大牛》第11章:面向对象编程_第4张图片
Kotlin语言的可见性修饰符与Java等语言有比较大的不同。首先Kotlin语言中函数和属性可以是顶层声明也可以是类成员声明。其次,Kotlin中没有Java中包私有可见性,而具有模块可见性(internal)。

11.7.1 可见性范围
首先,需要搞清楚可见性范围,可见性范围主要有三个:模块、源文件和类。其中源文件和类很好理解,下面重点介绍一下模块的概念。
模块就是多个文件编译在一起的集合,模块可以指如下内容:
一个IntelliJ IDEA模块(module)
一个Eclipse项目
一个Maven项目
一个Gradle源代码集合
一个Ant编译任务管理的源代码集合
下面重点介绍一下在IntelliJ IDEA中创建模块。首先,通过选择菜单File→New→Module打开创建模块对话框,如图11-2所示。从图中可见与创建Kotlin/JVM类型项目非常类似,包括后面的具体步骤也非常相似,请读者参考3.2.1节,这里不再赘述。如果创建的模块名是module1,那么创建成功后会在左边的项目文件管理窗口中看到刚刚创建的模块module1,如图11-3所示。
《Kotlin从小白到大牛》第11章:面向对象编程_第5张图片
《Kotlin从小白到大牛》第11章:面向对象编程_第6张图片
此时,IntelliJ IDEA项目中以及有两个模块了,如图11-4所示,一个是本身项目就有的chapter11模块,另一个是刚刚创建的module1模块。
《Kotlin从小白到大牛》第11章:面向对象编程_第7张图片
如果模块chapter11要想访问module1模块中的属性、函数或类,在满足可见性的前提下,必须配置依赖关系。配置依赖关系chapter11依赖于module1关系,需要打开chapter11模块中的chapter11.iml文件。修改chapter11.iml文件内容如下:

① 代码第①行是自己添加的内容,声明了chapter11模块依赖于module1模块。

11.7.2 公有可见性
公有可见性使用public关键字,可以修饰顶层函数和属性,以及类成员函数和属性,所有被public修饰的函数和属性在任何地方都可见。
如果在模块module1中声明了一个Person类。示例代码如下:
//代码文件:chapter11/module1/src/com/a51work6/Person.kt
package com.a51work6

import java.util.Date

class Person(val name: String,// 名字
private val birthDate:Date,// 出生日期
internal val age: Int) // 年龄
{
internal fun display() {
println("[name: n a m e , b i r t h D a t e : name, birthDate: name,birthDate:birthDate, age:$age]")
}
}
上述代码Person类声明为public,省略了public关键字,name属性也是public。birthDate属性是private,age属性是internal。display()函数是internal。
在模块chapter11中调用Person类。示例代码如下:
//代码文件:chapter11/src/com/a51work6/section7/ch11.7.2.kt
package com.a51work6.section7

import com.a51work6.Person ①
import java.util.*

fun main(args: Array) {
val now = Date()
val person = Person(“Tony”,now, 18) ②
println(person.name) ③
//println(person.age) //不能访问age属性 ④
//println(person.birthDate)//不能访问birthDate属性 ⑤
//person.display()//不能访问display()函数 ⑥
}
代码第①行是引入com.a51work6.Person文件,代码第②行是创建Person对象,由于Person类被声明为public的,所以这里可以访问。代码第③行是访问name属性,由于name是public,所以这里可以访问该属性。代码第④行的不能访问age属性,因为age属性声明为internal。代码第⑤行的不能访问birthDate属性,因为birthDate属性声明为private。代码第⑥行的不能访问display()函数,因为display()函数声明为internal。

11.7.3 内部可见性
内部可见性使用internal关键字,在同一个模块内部与public可见性一样。如果在模块module1中访问module1中的Person类。示例代码如下:
//代码文件:chapter11/module1/src/com/a51work6/ch11.7.3.kt
package com.a51work6

import java.util.*

fun main(args: Array) {
val now = Date()
val person = Person(“Tony”,now, 18)
println(person.name)
println(person.age)
// println(person.birthDate)//不能访问birthDate属性
person.display()
}
从上述代码中可见,age属性和display()函数都可以访问,它们都声明为internal,当前访问代码与Person都在同一个模块中,所以可以访问它们。而age属性不能访问,这是因为age是private的。

11.7.4 保护可见性
保护可见性使用protected关键字,protected可以保证某个父类的子类都能继承该父类的protected属性和函数。无论父类和子类是否在同一个模块中,父类的protected属性和函数都可以被子类继承。
如果有一个父类ProtectedClass代码如下:
//代码文件:chapter11/src/com/a51work6/section7/ProtectedClass.kt
package com.a51work6.section7

open class ProtectedClass {

protected var x: Int = 0    ①

init {
    x = 100
}

protected fun printX() {    ②
    println("Value Of x is" + x)
}

}
继承ProtectedClass类的子类SubClass代码如下:
//代码文件:chapter11/src/com/a51work6/section7/SubClass.kt
package com.a51work6.section7

class SubClass : ProtectedClass() {

fun display() {        
    printX()     //printX()函数是从父类继承过来  ①          
    println(x)   //x属性是从父类继承过来              ②
}

}
代码第①行可以访问printX()函数,该函数是从父类继承过来。代码第②行可以访问x属性,该属性是从父类继承过来。

11.7.5 私有可见性
私有可见性使用private关键字,当private修饰类中的成员属性和函数时,这些属性和函数只能在类的内部可见。当private修饰顶层属性和函数时,这些属性和函数只能在当前文件中可见。
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section7/PrivateClass.kt
package com.a51work6.section7

class PrivateClass { ①

private var x:Int = 0  ②

private fun printX() {  ③      

println(“Value Of x is” + x)
}

fun display() {            ④
    x = 100
    printX()
}

}
上述代码第①行声明PrivateClass类,其中的代码第②行是声明private属性x,代码第③行是声明private函数printX()。代码第④行display()函数中访问了x属性和printX()函数,在同一个类的内部可以访问这些private成员。
调用PrivateClass代码如下:
//代码文件:chapter11/src/com/a51work6/section7/ch11.7.5.kt
package com.a51work6.section7

private var x: Int = 0 ①

private fun printX() { ②
println(“Value Of x is” + x)
}

fun main(args: Array) {
val p =PrivateClass() ③
// p.printX()
//PrivateClass中printX()函数不可见 ④
// p.x
//PrivateClass中x属性不可见 ⑤

println(x) ⑥
printX() ⑦
}
上述代码第①行声明顶层private属性x,代码第②行是声明顶层private函数printX()。代码第③行是实例化p对象,代码第④行试图访问p的printX()函数,会发生编译错误。代码第⑤行也会发生编译错误。代码第⑥行是访问顶层属性x,代码第⑥行是访问顶层函数printX(),它们都可以访问。

11.8 数据类

有的时候需要一种数据容器在各个组件之间传递。数据容器只需要一些属性保存数据以就可以了,例如11.6.1节的User:
class User(val name: String, var password: String)
但是11.6.1节的User作为数据容器还不完善,最好重写Any的如下三个函数:
equals。比较其他对象是否与当前对象“相等”,==运算符重载equals函数。
hashCode。返回该对象的哈希码值,可以提高对Hashtable和HashMap对象的访问效率。
toString。返回该对象的字符串表示。
在这里插入图片描述
虽然重写Any的三个函数,也不是很麻烦,但是如果有很多个属性,代码量还是很多的。Kotlin为此提供了一种数据类(Data Classes)。

11.8.1 声明数据类
数据类的声明很简单,只需要类头class前面加上data关键字即可,修改User类如下:
data class User(val name: String, var password: String)
添加一个data关键字后User类变成了数据类,事实上它的底层重写Any的三个函数,并增加了一个copy函数。对于equals函数的重写就比较所有属性全部相等,equals才返回true。对于toString函数是将所有属性连接拼接成一个字符串。
在这里插入图片描述
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section8/User.kt
package com.a51work6.section8

data class User(val name: String, var password: String) ①
调用代码如下:
//代码文件:chapter11/src/com/a51work6/section8/ch11.8.1.kt
package com.a51work6.section8

fun main(args: Array) {

//创建User对象
val user1 = User("Tony","123")           ②
val user2 = User("Tony","123")           ③

println(user1 == user2) //true             ④
println(user1.toString())

//User(name=Tony, password=123) ⑤
println(user2.toString())
//User(name=Tony, password=123) ⑥

println(user1.hashCode())  //81040716        ⑦
println(user2.hashCode())  //81040716        ⑧

}
输出结果如下:
true
User(name=Tony, password=123)
User(name=Tony, password=123)
81040716
81040716

代码第①行可以声明User数据类。代码第②行和第③行分别创建了两个对象。代码第④行比较user1和user2是否相等,分别比较name属性和password属性都相等,结果为true。从代码第⑤行和第⑥行可见user1和user2的toString函数输出结果都是User(name=Tony, password=123)字符串。从代码第⑦行和第⑧行可见user1和user2的hashCode函数输出结果也相等。
如果将代码第①行的data去掉变成普通类,那么输出结果如下:
false
com.a51work6.section8.User@5e2de80c
com.a51work6.section8.User@1d44bcfa
1580066828
491044090
从上面运行的结果可见数据类和普通类的不同。

11.8.2 使用copy函数
数据类中还提供了一个copy函数,通过copy可以复制一个新的数据类对象,示例代码如下:
//代码文件:chapter11/src/com/a51work6/section8/ch11.8.2.kt
package com.a51work6.section8

fun main(args: Array) {
//创建User对象
val user1 = User(“Tony”,“123”) ①
//复制User对象
val user2 = user1.copy(name =“Tom”) ②
val user3 = user1.copy() ③

println(user1 == user2)   //false
println(user1 == user3)   //true
println(user1.toString())

//User(name=Tony, password=123)
println(user2.toString())
//User(name=Tom, password=123)
println(user3.toString())
//User(name=Tony, password=123)

println(user1.hashCode()) //81040716
println(user2.hashCode()) //2661184
println(user3.hashCode()) //2661184
}
代码第①行是创建user1对象。代码第②行和第③行是使用copy函数复制出两个对象,copy函数参数是与属性对应的,而且每一个参数有默认值,代码第②行的user1.copy(name = “Tom”)语句复制了user1并重新设置了name属性。代码第③行的user1.copy()语句完全复制了user1给user3,因此user1等于user3。

11.8.3 解构数据类
数据对象是一个数据容器,可以理解为多个相关数据被打包到一个对象中。解构进行相反的操作,是将数据对象拆开将内部的属性取出,赋值给不同的变量。解构不仅仅适用于数据对象,也适用于集合对象。
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section8/ch11.8.3.kt
package com.a51work6.section8

fun main(args: Array) {
//创建User对象
val user1 = User(“Tony”,“123”)
//解构
val(name1,pwd1) = user1 ①
println(name1) //Tony
println(pwd1) //123
val(name2, ) = user1 //省略解构password ②
println(name2) //Tony
}
代码第①行对user1对象进行解构,解构出来的数据分别赋值给name1和pwd1中。代码第②行也是对user1对象进行解构,但是接收第二个属性的变量却是下划线“
”,这说明不需要解构第二个属性值。

11.9 枚举类

枚举用来管理一组相关的有限个数常量的集合,使用枚举可以提高程序的可读性,使代码更清晰且更易于维护。在Kotlin中提供枚举类型。
11.9.1 声明枚举类
Kotlin中是使用enum和class两个关键词声明枚举类,枚举的语法格式如下:
enum class
枚举名 {
枚举常量列表
}
enum是软关键字与class关键字结合使用,只有在声明枚举类时enum才作为关键字使用,其他场景可以作为标识符使用。“枚举名”是该枚举类的名称。它首先应该是有效的标识符,其次应该遵守Kotlin命名规范。它应该是一个名称,如果采用英文单词命名,首字母应该大写,且应尽量用一个英文单词。“枚举常量列表”是枚举的核心,它由一组相关常量组成,每一个常量就是枚举类的一个实例。

如果采用枚举类来表示工作日,最简单枚举类WeekDays具体代码如下:
//代码文件:chapter11/src/com/a51work6/section9/WeekDays.kt
package com.a51work6.section9

//最简单形式的枚举类
enum class WeekDays {
// 枚举常量列表
MONDAY,TUESDAY, WEDNESDAY, THURSDAY, FRIDAY

}
在枚举类WeekDays中声明了5个常量,使用枚举类WeekDays代码如下:
//代码文件:chapter11/src/com/a51work6/section9/ch11.9.1.kt
package com.a51work6.section9

fun main(args: Array) {
// day工作日变量
val day = WeekDays.FRIDAY ①
println(day) ②
when (day) {
WeekDays.MONDAY ->println(“星期一”)
WeekDays.TUESDAY ->println(“星期二”)
WeekDays.WEDNESDAY ->println(“星期三”)
WeekDays.THURSDAY ->println(“星期四”)
else //case FRIDAY: -> println(“星期五”)
}
}

输出结果:
FRIDAY
星期五

上述代码第①行中day是WeekDays枚举类型,取值是WeekDays.FRIDAY,是把枚举类WeekDays的FRIDAY实例赋值给day。代码第②行day对象日志输出结果不是整数,而是FRIDAY。
枚举类与when能够很好地配合使用,在when中使用枚举类型时,when中的分支应该对应枚举常量,不要多也不要少,当使用else时,else应该只表示等于最后一个枚举常量情况。上述示例代码中使用else分支表示的是FRIDAY情况。

11.9.2 枚举类构造函数
枚举类中可以像其他类一样包含属性和函数,可以通过构造函数初始化属性。11.9.1节示例添加构造函数,代码如下:
//代码文件:chapter11/src/com/a51work6/section9/WeekDays.kt
package com.a51work6.section9

//枚举类构造函数
enum class WeekDays(private val wname: String,
private val index: Int) { ①
// 枚举常量列表
MONDAY(“星期一”, 0), TUESDAY(“星期二”, 1),
WEDNESDAY(“星期三”, 2),
THURSDAY(“星期四”, 3), FRIDAY(“星期五”, 4); ②

// 重写父类中的toString()函数
override fun toString(): String {           ③
   return "$wname-$index"
}

}
调用代码如下:
//代码文件:chapter11/src/com/a51work6/section9/ch11.9.2.kt
package com.a51work6.section9

fun main(args: Array) {
// day工作日变量
val day =WeekDays.FRIDAY
//打印day默认调用枚举toString函数
println(day) //星期五-4
}
在这里插入图片描述
代码第①行是添加的构造函数,枚举类中的构造函数只能是私有的,这也说明了枚举类对象不允许在外部通过构造函数创建。枚举类的构造函数只是为了在枚举类内部创建枚举常量使用,所以一旦添加了有参数的构造函数,那么“枚举常量列表”也需要修改,见代码第②行,每一个枚举常量就是一个实例,都会调用构造函数,其中(“星期一”, 0)就是调用构造函数。
代码第③行是重写toString函数,它是由Any类提供的函数。

11.9.3 枚举常用属性和函数
枚举本身有一些常用的属性和函数:
o ordinal属性。返回枚举常量的顺序。这个顺序根据枚举常量声明的顺序而定,顺序从零开始。
o values()函数。返回一个包含全部枚举常量的数组。
o valueOf(value: String)函数。value是枚举常量对应的字符串,返回一个包含枚举类型实例。
WeekDays枚举类代码如下:
//代码文件:chapter11/src/com/a51work6/section9/ch11.9.3.kt
package com.a51work6.section9

fun main(args: Array) {

// 返回一个包含全部枚举常量的数组
val allValues = WeekDays.values()                ①
// 遍历枚举常量数值
for (value in allValues) {
    println("${value.ordinal} -$value")        ②
}

// 创建WeekDays对象
val day1 = WeekDays.FRIDAY
val day2 =WeekDays.valueOf("FRIDAY")      ③

println(day1 === WeekDays.FRIDAY) //true ④
println(day1 == WeekDays.FRIDAY) //true  ⑤
println(day1 === day2)   //true  ⑥

}
输出结果:
0 - 星期一-0
1 - 星期二-1
2 - 星期三-2
3 - 星期四-3
4 - 星期五-4
true
true
true
上述代码第①行是通过values函数获得所有枚举常量的数组,代码第②行是获得枚举常量value,其中value.ordinal获得当前枚举常量的顺序。
代码第③行是通过valueOf函数获得枚举对象WeekDays.FRIDAY,参数是枚举常量对应的字符串。
代码第④行~第⑥行是比较枚举对象,它们比较的结果都是true。
在这里插入图片描述

11.10 嵌套类

Kotlin语言中允许在一个类的内部声明另一个类,称为“嵌套类”(Nested Classes),嵌套类还有一种特殊形式——“内部类”(Inner Classes)。封装嵌套类的类称为“外部类”,
嵌套类与外部类之间存在逻辑上的隶属关系。

11.10.1 嵌套类
嵌套类可以声明为public、internal、protected和private,即4种可见性都可以。嵌套类示例代码如下:
//代码文件:chapter11/src/com/a51work6/section10/ch11.10.1.kt
package com.a51work6.section10

//外部类
class View { ①

// 外部类属性
val x = 20        

// 嵌套类
class Button {    ②
    // 嵌套类函数
    fun onClick() {           

println(“onClick…”)
//不能访问外部类的成员
//println(x) //编译错误 ③
}
}

// 测试调用嵌套类
fun test() {              ④
    val button = Button()          ⑤
    button.onClick()                  ⑥
}

}
上述代码第①行声明外部类View,而代码第②行是在View内部声明嵌套类Button,嵌套类不能引用外部类,也不能引用外部类的成员,见代码第③行试图访问外部类的x属性,会发生编译错误。代码第④行test函数用来调用嵌套类,代码第⑤行是实例化嵌套类Button,代码第⑥行是调用的嵌套类的onClick函数,可见在外部类中可以访问嵌套类。
在main函数测试嵌套类代码如下:
//代码文件:chapter11/src/com/a51work6/section10/ch11.10.1.kt
package com.a51work6.section10

fun main(args: Array) {

val button =View.Button()
button.onClick()

// 测试调用嵌套类
val view =View()
view.test()

}
从代码val button = View.Button()是实例化嵌套类,在外部类以外访问嵌套类,需要使用“外部类.嵌套类”形式。
在这里插入图片描述
11.10.2 内部类
内部类是一种特殊的嵌套类,嵌套类不能访问外部类引用,不能访问外部类的成员,而内部可以。
内部类示例代码如下:
//代码文件:chapter11/src/com/a51work6/section10/ch11.10.2.kt
package com.a51work6.section10

//外部类
class Outer {

// 外部类属性
val x = 10

// 外部类函数
fun printOuter() {
    println("调用外部函数...")
}

// 测试调用内部类
fun test() {
    val inner = Inner()
    inner.display()
}

// 内部类
 inner class Inner {           ①
 
    // 内部类属性
    private val x = 5
    
    // 内部类函数
    fun display() {
        // 访问外部类的属性x
        println("外部类属性 x = " + [email protected])        ②
        // 访问内部类的属性x                           
        println("内部类属性 x = " + this.x)                   ③
        println("内部类属性 x = " + x)                         ④
        
        // 调用外部类的成员函数
        [email protected]()         ⑤
        printOuter()                         ⑥
    }
}

}
上述代码第①行声明了内部类Inner,在class前面加inner关键字。内部类Inner有一个成员变量x和成员函数display(),在display()函数中代码第②行是访问外部类的x成员变量,代码第③行和第④行一样都是访问内部类的x成员变量。代码第⑤行和第⑥行都是访问外部类的printOuter()成员函数。
《Kotlin从小白到大牛》第11章:面向对象编程_第8张图片
测试内部代码如下:
//代码文件:chapter11/src/com/a51work6/section10/ch11.10.2.kt
package com.a51work6.section10

fun main(args: Array) {

// 通过外部类访问内部类
val outer = Outer()
outer.test()

// 直接访问内部类
val inner = Outer().Inner() ①
inner.display()

}
运行结果如下:
外部类属性 x = 10
内部类属性 x = 5
内部类属性 x = 5
调用外部函数…
调用外部函数…
外部类属性 x = 10
内部类属性 x = 5
内部类属性 x = 5
调用外部函数…
调用外部函数…
通常情况下,内部类不是为外部类之外调用使用的,只是为外部类自己内部使用的。但是如果一定要在外部类之外访问内部类,Kotlin也是支持的,见代码第①行内部类是实例化内部类对象,Outer().Inner()表达式说明先实例化外部类Outer,再实例化内部类Inner。

11.11 强大的object关键字

object关键字主要用于声明一个类的同时创建这个类的对象。具体而言它有三个方面的应用:对象表达式、对象声明和伴生对象。

11.11.1 对象表达式
object关键字可以声明对象表达式,对象表达式用来替代Java中的匿名内部类。就是在声明一个匿名类,并同时创建匿名类的对象。
对象表达式示例如下:
//代码文件:chapter11/src/com/a51work6/section11/ch11.11.1.kt
package com.a51work6.section11

//声明View类
class View {

fun handler(listener: OnClickListener) {
    listener.onClick()
}

}

//声明OnClickListener接口
interface OnClickListener {
fun onClick()
}

fun main(args: Array) {

var i = 10
val v = View()
// 对象表达式作为函数参数
v.handler(object : OnClickListener {              ①      

    override fun onClick() {
        println("对象表达式作为函数参数...")        

println(++i) ②
}

})       

}
上述代码第①行中v.handler函数的参数是对象表达式,object说明表达式是对象表达式,该表达式声明了一个实现OnClickListener接口的匿名类,同时创建对象。另外,在对象表达式中可以访问外部变量,并且可以修改,见代码第②行。
对象表达式的匿名类可以实现接口,也可以继承具体类或抽象类,示例代码如下:
//代码文件:chapter11/src/com/a51work6/section11/ch11.11.1.kt
package com.a51work6.section11

//声明Person类
open class Person(val name: String, val age: Int) ①

fun main(args: Array) {

//对象表达式赋值
val person = object :Person("Tony", 18), OnClickListener {        ②
    //实现接口onClick函数
    override fun onClick() {               
        println("实现接口onClick函数...")
    }
    
    //重写toString函数
    override fun toString(): String {    
        return

(“Person[name= n a m e , a g e = name, age= name,age=age]”)
}
}
println(person)
}
上述代码第①行是声明一个Person具体类,代码第②行是声明对象表达式,该表达式声明实现OnClickListener接口,且继承Person类的匿名类,之间用逗号(,)分隔。Person(“Tony”, 18)是调用Person构造函数。注意接口没有构造函数,所以在表达式中OnClickListener后面没有小括号。
有的时候没有具体的父类也可以使用对象表达式,示例代码如下:

//代码文件:chapter11/src/com/a51work6/section11/ch11.11.1.kt
package com.a51work6.section11

fun main(args: Array) {

//无具体父类对象表达式
var rectangle = object { ①

    // 矩形宽度
    var width: Int = 200
    // 矩形高度
    var height: Int = 300
    
    //重写toString函数
    override fun toString(): String {
        return ("[width=$width,height=$height]")
    }
}

println(rectangle)

}
代码第①行是声明一个对象表达式,没有指定具体的父类和实现接口,直接在object后面大括号中编写类体代码。

11.11.2 对象声明
单例设计模式(Singleton)可以保证在整个的系统运行过程中只有一个实例,单例设计模式在实际开发中经常使用的设计模式。Kotlin把单例设计模式上升到语法层面,对象声明将单例设计模式的细节隐藏起来,使得在Kotlin中使用单例设计模式变得非常的简单。
在这里插入图片描述public final class Singleton {
private static final Singleton INSTANCE = new Singleton()

private Singleton() {}

public static Singleton getInstance() {
    return INSTANCE;
}

}

对象声明示例代码如下:
//代码文件:chapter11/src/com/a51work6/section11/ch11.11.2.kt
package com.a51work6.section11

interface DAOInterface {
//插入数据
fun create(): Int
//查询所有数据
fun findAll(): Array?
}

object UserDAO : DAOInterface { ①
//保存所有数据属性
private var datas: Array?= null

override fun findAll():Array? {
    //TODO 查询所有数据
    return datas
}

override fun create(): Int {
    //TODO 插入数据
    return 0
}

}

fun main(args: Array) {

UserDAO.create()                              ②
var datas = UserDAO.findAll()             ③

}
上述代码第①行是对象声明,声明UserDAO单例对象,使用object关键字后面是类名。在对象声明的同时可以指定对象实现的接口或父类,本例中指定实现DAOInterface接口。在类体中可以有自己的成员函数和属性。在调用时,可以通过类名直接访问单例对象的函数和属性,见代码第②行和第③行。
在这里插入图片描述
示例代码如下:
//代码文件:chapter11/src/com/a51work6/section11/ch11.11.2.kt
package com.a51work6.section11

object UserDAO : DAOInterface {
//保存所有数据属性
private var datas: Array?= null

override fun findAll():Array? {
    //TODO 查询所有数据
    return datas
}

override fun create(): Int {

// object Singleton { ①
// val x = 10
// }
return 0;
}

object Singleton {            ②
    val x = 10
}

}

class Outer {
object Singleton { ③
val x = 10
}
}

fun main(args: Array) {

println(UserDAO.Singleton.x)

// object Singleton { ④
// val x = 10
// }
}
上述代码第①行和第④行试图在函数中嵌入Singleton对象声明,则会发生编译错误。代码第②行是Singleton对象声明嵌入在UserDAO对象声明中。代码第③行是Singleton对象声明嵌入在Outer类中。

11.11.3 伴生对象
在Java类有实例成员和静态成员,实例成员隶属于类的个体,静态成员隶属于类本身。例如:有一个Account(银行账户)类,它有三个成员属性:amount(账户金额)、interestRate(利率)和owner(账户名)。在这三个属性中,amount和owner会因人而异,对于不同的账户这些内容是不同的,而所有账户的interestRate都是相同的。amount和owner成员属性与账户个体有关,称为“实例属性”,interestRate成员属性与个体无关,或者说是所有账户个体共享的,这种变量称为“静态属性”或“类属性”。
1.声明伴生对象
在很多语言中静态成员的声明使用static关键字修饰,而Kotlin没有static关键字,也没有静态成员,它是通过声明伴生对象实现Java静态成员访问方式。示例代码如下:
//代码文件:chapter11/src/com/a51work6/section11/companion/ch11.11.3.kt
package com.a51work6.section11.companion

class Account {

// 实例属性账户金额
var amount =0.0
// 实例属性账户名
var owner: String? = null

// 实例函数
fun messageWith(amt: Double): String {
    //实例函数可以访问实例属性、实例函数、静态属性和静态函数
    val interest = Account.interestBy(amt)             ①
    return "${owner}的利息是$interest"
}

companion object {                           ②

    // 静态属性利率
    var interestRate: Double = 0.0     ③
    
    // 静态函数
    fun interestBy(amt: Double): Double {    ④
        // 静态函数可以访问静态属性和其他静态函数
        return interestRate * amt
    } 
    
    // 静态代码块
    init {                  ⑤           

println(“静态代码块被调用…”)
// 初始化静态属性
interestRate = 0.0668
}
} ⑥
}

fun main(args: Array) {
val myAccount = Account() ⑦
// 访问伴生对象属性
println(Account.interestRate) ⑧
// 访问伴生对象函数
println(Account.interestBy(1000.0)) ⑨
}
输出结果
静态代码块被调用…
0.0668
66.8
上述代码第②行~第⑥行是声明伴生对象,使用关键字companion和object。作为对象可以有成员属性和函数,代码第③行是声明interestRate属性,伴生对象的属性可以在容器类(Account)外部通过容器类名直接访问,见代码第⑧行Account.interestRate表达式,这种表达式形式与Java等语言中访问静态属性是类似的。类似代码第④行声明伴生对象函数,调用该属性见代码第①行和第⑨行。代码第⑤行是伴生对象的init初始化代码块,它相当于Java中的静态代码,C#中的静态构造函数,它可以初始化静态属性,该代码块会在容器类Account第一次访问时调用,代码第⑦行是第一次访问Account类,此时会调用伴生对象的init初始化代码块。
在这里插入图片描述
2.伴生对象非省略形式
在上面的示例中事实上省略的伴生对象名字,声明伴生对象时还可以添加继承父类或实现接口。示例代码如下:
//代码文件:chapter11/src/com/a51work6/section11/companion/ch11.11.3.kt
package com.a51work6.section11.companion

import java.util.*

//声明OnClickListener接口
interface OnClickListener {
fun onClick()
}

class Account {

// 实例属性账户金额
var amount = 0.0
// 实例属性账户名
var owner: String? = null

// 实例函数
fun messageWith(amt: Double): String

{
//实例函数可以访问实例属性、实例函数、静态属性和静态函数
val interest =Account.interestBy(amt)
return “ o w n e r 的 利 息 是 {owner}的利息是 owner{interest}”
}

companion object Factory : Date(),OnClickListener {       ①
    override fun onClick() {
   }
   
    // 静态属性利率
    var interestRate: Double = 0.0
    
    // 静态函数
    fun interestBy(amt: Double):Double {
        // 静态函数可以访问静态属性和其他静态函数
        return interestRate * amt
    }
    
    // 静态代码块
    init {
        println("静态代码块被调用...")
        // 初始化静态属性
        interestRate = 0.0668
    }
}

}

fun main(args: Array) {
val myAccount = Account()
// 访问伴生对象属性
println(Account.interestRate)
println(Account.Factory.interestRate) ②
// 访问伴生对象函数
println(Account.interestBy(1000.0))
println(Account.Factory.interestBy(1000.0)) ③
}
上述代码第①行是声明伴生对象,其中Factory是伴生对象名,Date()是继承Date类,OnClickListener是实现该接口。一旦显示指定伴生对象名后,在调用时可以加上伴生对象名,见代码第②行和第③行,当然省略伴生对象名也可以调用它的属性和函数。
3.伴生对象扩展
伴生对象中可以添加扩展函数和属性,示例代码如下:
//伴生对象声明扩展函数
fun Account.Factory.display() {
println(interestRate)
}

//访问伴生对象扩展函数
Account.Factory.display()
Account.display()
从上述代码可见,调用伴生对象的扩展函数与普通函数访问没有区别。

本章小结

本章主要介绍了面向对象基础知识。首先介绍了面向对象一些基本概念,面向对象三个基本特性。然后介绍了类声明、属性、扩展、构造函数和可见性修饰符。最后介绍了数据类、枚举类、嵌套类和使用object关键字。

你可能感兴趣的:(Kotlin从小白到大牛)