1.3 类与对象
面向对象编程思想中,类是一个封装好的程序模块。该模块具有一定业务功能,能完成指定的工作,而且类可以被其他程序重用。一个类具有两种技术特征:动态特征和静态特征。类的静态特征通过类中的属性(也称为成员变量)来体现,而类的动态特征通过类的方法(也称为成员函数)来体现。类中的属性用于记录与业务功能有关或与类实例状态有关的数值,而类中的方法则可实现一定的业务功能,并能实现对类属性值的访问、维护和计算。
类是通过程序来描述客观事物的结果。类本身不会被计算机运行,应用程序中,类需要被实例化以后才能运行。类实例化的过程实际上是系统为类分配相关计算资源的过程,一个类被实例化以后成为对象。对象是类的实例化结果,而一个类可以被实例化成为多个对象。对象是计算机系统中实际可运行的实体。
本节将主要讨论Kotlin面对对象的程序实现方法。相关内容涉及类、继承、接口、扩展等。
1.3.1 类的声明
Kotlin在定义类时,使用关键字class,类声明的基本格式如下:
class 类名{
…
}
类中的属性可以使用var或val来直接定义。其中,var用于定义变量,val用于定义常量,基本格式为:
class 类名{
var 属性名:属性类型 = …
…
val属性名: 属性类型 = …
…
}
1.3.2 类的构建器
类的构建器是一种特殊的方法。构建器在类被实例化时由系统调用,构建器的主要工作是对类中变量或所需资源进行赋值或申请。Kotlin程序中的类具有两种构建器:主构建器和非主构建器。
(1)主构建器
Kotlin类的主构建器使用constructor关键字说明,语句位置位于类声明处,基本形式为:
class 类名 constructor(参数列表){
var 属性名:属性类型 = …
…
val 属性名:属性类型 = …
}
若主构建器不包含注释(annotation)或访问权限说明,则关键字constructor可省略,则程序结构为:
class 类名(参数列表){
var 属性名:属性类型 = …
…
val 属性名:属性类型 = …
…
}
若主构建器包含注释(annotation)或访问权限说明,则关键字constructor不可省略,例如:
1 class Machine public @Inject constructor(type:String){
2 …
3 }
主构建器参数列表中的参数可用于初始化类中的属性;例如,在下列程序中,构建器中的变量t被用于初始化类中的属性type:
1 class Machine (t:String){
2 val type:String = t
3 }
主构建器参数列表中的参数还可以在类中的初始化块中使用,基本形式为:
class 类名(参数列表){
init{//初始化块
关于主构建器中参数列表的执行语句
}
}
最后,主构建器参数列表中的参数可作为类的属性直接使用。
(2)非主构建器
非主构建器的定义位于类定义的内部,使用关键字constructor说明,基本结构为:
class 类名{
…
constructor(参数列表){
…
}
}
若一个类存在主构建器,在定义非主构建器时,该构建器要么需要调用主构建器,要么需要调用另一个已定义的非主构建器。例如:
1 class Machine(t:String, n:Int){
2 val type = t
3 val sum = n
4 //非主构建器1
5 constructor(t:String) : this(t, 0)
6 //非主构建器2
7 constructor(n:Int) : this("equipment", n)
8 //非主构建器3
9 constructor() : this(0)
10 }
上述程序包含1个主构建器和3个非主构建器。其中,程序第5行和第7行中的非主构建器1和2是调用主构建器来完成初始化工作(使用this操作符);程序第9行中的非主构建器3则调用非主构建器2来完成初始化工作。需要说明的是,在构建器调用说明中,使用冒号并使用关键字this来实现基本的自调用;例如第5行中“…:this(t, 0)”,以及第7行中“…: this("equipment", n)”等。
一般情况下,构建器用于初始化类中的属性。但若在程序创建时还无法确定属性的具体值时,相关属性需要使用lateinit进行说明,且lateinit所修饰的变量只能是可变更变量,例如:
1 lateinit var txt: Text View
1.3.3 类的实例化
类在实例化时,可使用的基本形式为:
val 对象名 = 类名(参数列表)
var 对象名 = 类名(参数列表)
当一个类被实例化为一个对象以后,对象中的属性和方法通过点操作符(.)来访问,基本的形式有对象名.属性、对象名.方法名(参数列表)。
1.3.4 设值器和取值器(setter和getter)
Kotlin类针对类属性可使用相应的设值器和取值器。设值器是用来设置类中指定属性的数值,而取值器则是用来帮助外部程序访问特定属性的数值。Kotlin类中的属性分为两种:普通变量和只读变量。针对普通变量属性,在定义变量时,系统会指定默认的设值器和取值器;针对只读变量属性,在定义常量时,系统会指定默认的取值器(不指定设值器)。
若应用程序想修改系统指定的设值器和取值器,则可采用以下结构来完成工作:
class 类名(参数列表){
var 变量: 变量类型 = 赋值语句
变量取值器定义
变量设值器定义
…
val 只读变量: 常量类型 = 赋值语句
变量取值器定义
…
}
取值器在定义时使用关键字get;设值器在定义时使用关键字set。下列示例程序展示了设值器和取值器的修改过程:
1 class Simple Class (str:String){
2 var att1 = str
3 var att2: String? = null
4 get(){ //自定义取值器
5 if (field == null){
6 return "an attribute"
7 }else{
8 return field
9 }
10 }
11 set(s: String?){ //自定义设值器
12 field = "att2 again"
13 }
14 }
上述程序定义了一个名为Simple Class的类,该类中有两个属性att1和att2,其中,att1属性使用了系统默认设值器和取值器;而att2则自定义了设值器和取值器。程序第4行至第10行,定义了att2的取值器,该取值器可根据属性的情况返回不同的结果;当att2为空值时,取值器会自动返回字符串“an attribute”;若att2 不为空值时,取值器返回实际值。程序第11行至第13行定义了att2的设值器,而从程序可见,该设值器会将att2设置为“att2 again”。程序第4行至第13行中,程序使用了field关键字,该关键字指代一个属性实例;而在上述示例程序中,field实际指代的是类属性att2。
下列程序展示了使用Simple Class类属性设值器和取值器的方法:
1 fun main(args: Array<String>){
2 var cls = Simple Class("a class")
3 println(cls.att1)
4 cls.att1 = "a value"
5 println(cls.att1)
6
7 println(cls.att2)
8 cls.att2 = "att"
9 println(cls.att2)
10 }
上述程序中,第3行中的println(cls.att1)语句是调用att1的默认取值器;第4行中cls.att1 ="a value"调用att1的默认设值器;第5行中println(cls.att1)语句再次调用att1的默认取值器。第7行中的println(cls.att2)语句会调用att2的自定义取值器,获得的值为“an attribute”。第8行中cls.att2="att"调用att2的自定义设值器,该设值器设置了参数“att”;然而,根据程序定义,设值器中的参数并没有被使用(程序中直接使用field ="att2 again")。因此,第9行中的println(cls.att2)语句调用att2的自定义取值器,获得的结果为“att2 again”。程序运行结果如下:
1 a class
2 a value
3 an attribute
4 att2 again
1.3.5 类的继承
类的继承机制是实现类重用的重要方式之一。通过继承,父类中的方法和属性在子类中得以重用。Kotlin中的所有类从Any类开始继承。Any类包含几个基本方法:equals、hash Code、to String等[2]。需要注意的是,在Kotlin程序中,类在默认状态下是不能被继承的,允许被继承的类必须使用open关键字来进行说明。实现继承的基本程序结构如下所示:
open class 父类名(参数列表){
…
}
class 子类名(参数列表): 父类名(参数列表){
…
}
上述结构中,父类在声明时使用了open关键字,说明该类可被继承。Kotlin类的继承结构为“…子类名(…):父类名(…)”。继承实现时,若父类定义了主构建器,则子类必须在声明时直接调用父类主构建器。例如:
1 open class Simple Class (str:String){
2 var att1 = str
3 }
4
5 class My Class(s:String): Simple Class(s)
上述程序中,子类My Class在定义时直接调用了父类Simple Class的主构建器。另外,上述程序中,My Class没有程序内容,因此,该类在声明时没有使用程序体(即{…}),这样的语法在Kotlin编程中是允许的。若在继承过程中,父类没有使用主构建器,则子类可在声明时调用父类非主构建器,子类也可在自己的非主构建器定义时调用父类中的非主构建器(使用关键字super),例如:
1 open class Simple Class{
2 var att1:String
3 constructor(s: String){
4 att1 = s
5 }
6 constructor(n: Int){
7 att1 = n.to String()
8 }
9 }
10
11 class My Class(n: Int): Simple Class(n)
12
13 class My Class2: Simple Class{
14 constructor(s: String): super(s)
15 }
上述程序中,My Class子类在声明时调用了父类的非主构建器,而My Class2子类在定义非主构建器时,使用super调用父类的非主构建器。
1.3.6 继承中方法的覆盖
Kotlin类在继承过程中,父类中的方法可以被子类中的方法覆盖。方法覆盖时,子类的方法签名必须和父类中的方法签名相同。而通过覆盖,子类可为被覆盖方法提供一种新的技术实现。Kotlin类中允许被覆盖的方法必须使用open关键字进行说明(若类中某方法没有包含open关键字,则说明该方法不能被覆盖)。如果子类覆盖了父类中的某个方法,则该方法必须使用override关键字进行说明。下列程序说明了继承中方法覆盖的情形:
1 open class Simple Class(str: String){
2 var att1:String = str
3 open fun service(): String {
4 return att1
5 }
6 }
7
8 class My Class(s: String): Simple Class(s){
9 override fun service(): String{
10 return "serivce"
11 }
12 }
上述程序中My Class类中的service方法覆盖了父类Simple Class中的service方法。需要特别说明的是,在子类中的方法如果使用了override关键字,则该方法还可被其子类覆盖。若想杜绝这样的现象,则可在override前增加final关键字,这样的声明可禁止该方法被进一步覆盖。方法覆盖时,若被覆盖的方法中某参数包含默认值,则覆盖方法中该参数不能定义新的默认值,即覆盖方法中该参数的默认值与被覆盖方法对应参数默认值相同。
1.3.7 继承中属性的覆盖
Kotlin类中的属性允许被覆盖(这个技术特点与Java程序不同)。父类中允许被覆盖的属性必须使用open关键字进行说明。如果子类覆盖了父类中的某个属性,则该属性必须使用override关键字进行说明。属性覆盖时,可使用var属性(普通变量)覆盖val属性(只读变量),但不能使用val属性(只读变量)覆盖var属性(普通变量)。
1.3.8 抽象类与接口
区别于普通类,抽象类是一种包含了抽象方法的类。所谓抽象方法,是指只有方法签名,但没有实现定义的方法。抽象类使用abstract关键字进行说明,最基本语法为abstract class 抽象类名(参数列表) {…}。抽象类不能被实例化,不能直接参与程序运行。抽象方法在定义时需通过abstract关键字进行说明。下列示例程序定义了一个抽象类:
1 abstract class My Class {
2 abstract fun service()
3 fun other(){
4 print("My Class is an abstract class.")
5 }
6 }
上述示例中,因为My Class中包含了一个抽象方法service,所以My Class是一个抽象类。抽象类中可以包含非抽象方法,例如,My Class中的other方法为一个非抽象方法。Kotlin允许使用抽象方法覆盖非抽象方法。例如,在下列程序中,抽象类My Class中的service方法覆盖了父类Class A中的service方法:
1 open class Class A{
2 open fun service(){println("service")}
3 }
4
5 abstract class My Class: Class A() {
6 abstract override fun service()
7 }
抽象类可用于构建其他类,基本的语法为class 类名:抽象类名(参数列表) {…}。下列示例程序中,AClass是一个基于抽象类My Class所定义的类:
1 abstract class My Class {
2 abstract fun service()
3 fun other(){
4 print("My Class is an abstract class.")
5 }
6 }
7
8 class AClass:My Class(){
9 override fun service(){
10 print("this is a service")
11 }
12 }
在抽象类基础上定义一个类时,抽象类中的抽象方法必须被完整定义,并使用override关键字进行说明。上述示例程序中,AClass中的service方法提供了方法的定义,并使用override来说明该方法是My Class中service方法的一个具体实现。
面向对象程序中,接口(interface)的程序结构与类的程序结构相似。但接口中的所有方法必须是抽象方法,而且接口中的属性一般为不带具体数值的抽象属性。声明一个接口时,必须使用interface关键字,但接口中所包含方法或属性声明不需要包含abstract关键字。接口不能被实例化,不能直接参与程序运行。在程序中,接口是实现类的一种约定或规范,这意味着可以基于接口来定义一个具体的类,但所定义的类必须提供所有抽象方法的完整定义。
程序实现中,基于接口定义的类必须在声明中使用冒号,并指定接口名称,基本的形式为class类名:接口名{…},以下示例程序展示了定义接口并基于接口定义一个类的过程:
1 interface My Interface{ //My Interface是一个接口
2 fun service1()
3 fun service2()
4 }
5
6 class My Class:My Interface{ //My Class是My Interface接口的一种实现
7 override fun service1(){
8 print("service1")
9 }
10 override fun service2(){
11 print("service2")
12 }
13 }
基于Kotlin语言定义一个接口时,可为接口中的属性定制相关的设值器和取值器;另外,Kotlin允许为接口中的抽象方法提供默认的实现(定义)。例如:
1 interface My Interface{
2 val att: String
3 var att1: Int
4 fun service1()
5 fun service2(){
6 println("service #2")
7 }
8 }
上述示例程序中,接口My Interface中包含service1和service2方法声明,其中,service2方法具有一个默认实现定义。针对这样的接口,可采用以下方式定义一个类(My Class未对service2方法进行额外定义):
1 class My Class(n: Int): My Interface{
2 override val att = "myclass"
3 override var att1 = n
4 override fun service1() {
5 println(att+": "+ att1)
6 }
7 }
1.3.9 多重继承
多重继承是指子类可同时继承多个父类。Kotlin中的继承机制不支持直接实现类之间的多重继承关系,但是,定义一个类时,可通过以下方式来实现多重继承。
● 基于多个接口定义一个类;定义的基本格式为:
class 类名(参数列表):接口1名称, 接口2名称, …{…}
● 基于多个接口和一个父类定义一个类;定义的基本格式为:
class 类名(参数列表):父类名称(参数列表), 接口1名称, 接口2名称, …{…}
需要特别注意的是,按上述方法实现多重继承过程中,可能存在方法签名冲突的问题,即父类或接口中可能存在多个签名相同的方法。在这样的条件下,子类声明中必须对存在签名冲突的方法进行覆盖。例如,在下列程序中,Class A类和接口Comp都包含一个service方法和一个show方法,当基于它们定义My Class时,这些方法之间会存在冲突。因此,在定义My Class时,所有的service和show方法必须被覆盖。
1 open class Class A(str: String){
2 var att1:String = str
3 open fun service(): String = att1
4 open fun show(){ println(att1) }
5 }
6
7 interface Comp{
8 fun service(): String
9 fun show(){ println("Component") }
10 }
11
12 class My Class(s: String): Class A(s), Comp{
13 override fun service(): String{
14 val str = super<Class A>.service()
15 return str
16 }
17 override fun show(){
18 super<Class A>.show()
19 super<Comp>.show()
20 }
21 }
上述程序中,由于存在多重继承,所以super需要使用<>操作来标识被继承的多个组成部分(类或接口)。
实现多重继承过程中,若某父类存在签名冲突的方法不允许被覆盖,则多重继承在实现时会出现程序语法错误。
1.3.10 程序对象的可见性说明
Kotlin中可见性说明符有public、internal、protected、private。程序在未指明具体可见性说明符时,程序对象的可见范围为public,即任意外部程序代码都可访问该程序。
(1)包
包(package)中可直接定义的程序对象包含[2]函数、类和属性、对象和接口等,这些对象的可见范围如下。
● 当使用public时,所有程序都能访问;
● 当使用private时,声明文件内可见;
● 当使用internal时,模块(开发环境、构建等软件工具工作时指定的代码单元)内可见;
● protected不可使用。
(2)类与接口
类与接口中成员的可见范围如下。
● 当使用public时,所有程序都能访问;
● 当使用private时,本类或本接口内可见;
● 当使用internal时,模块(开发环境、构建等软件工具工作时指定的代码单元)内的程序可见;
● 当使用protected时,本类和子类可见。
1.3.11 扩展
Kotlin支持通过声明来对类进行直接扩展,扩展的内容项可以是类的属性和方法。扩展声明的基本形式为:
fun 类名.方法名(参数列表): 返回值类型{
执行语句
…
return 返回值
}
val 类名.属性名
取值器声明
下列示例程序展示了扩展的实现方式。
1 class My Class(s: String){ //待扩展的一个类
2 var att = s
3 fun show(){
4 println(att)
5 }
6 }
7 val My Class.att1: String //扩展属性
8 get()="att1"
9 fun My Class.service(){ //扩展方法
10 println("working with: " + att1)
11 this.show()
12 }
13 fun main(args: Array<String>){
14 val c = My Class("cls")
15 c.show()
16 c.service()
17 }
上述程序中,My Class是一个预先定义的类,att1是扩展属性,service是扩展方法。
Kotlin程序中的扩展语法所产生的结果不会改变原有类的结构;同时,在使用扩展技术时,类中所增加的属性和方法为静态类型,这也意味着被扩展的属性不能进行初始化赋值操作。
当扩展声明位于一个程序包中,且该包(带扩展定义语句的包)以外的程序需要访问这些扩展时,则需要首先使用import语句进行导入声明。扩展技术也可以在不同的类定义中使用,例如,定义一个类A,再定义一个类B,在类B定义中,可直接使用扩展声明来扩展类A。另外,可基于扩展技术来定义匿名方法。例如,在下列程序中,匿名方法都是基于扩展技术来进行定义的:
1 fun main(args: Array<String>){
2 val add1 = fun Int.(n: Int): Int = this + n
3 val add2: Int.(n: Int) -> Int = {n -> this + n}
4 println(6.add1(3))
5 println(3.add2(6))
6 }
1.3.12 数据类
数据类是一个持有数据的简单类,定义的格式为data class 类名(参数列表)。例如:data class Item(var name: String, val type: String)。编译器会为数据类增加以下内容[2]:
● equals方法;
● has Code方法;
● to String方法;
● copy方法;
● component N方法(N为参数列表中的参数序号)。
上述方法中,copy方法用于复制一个数据类实例的数据,而且,该方法可以在执行时根据要求修改部分属性值。例如,下列程序运行的结果为“it: items”:
1 data class Item(var name: String, val type: String)
2 fun main(args: Array<String>){
3 val c = Item("it", "item")
4 val cc = c.copy(type = "items")
5 println(cc.name+": "+cc.type)
6 }
数据类的定义必须满足下列要求[2]:
● 主构建器中至少有一个参数;
● 主构建器中的参数必须被定义为val或var;
● 数据类不能是abstract、open、sealed和inner类型的类。
其中,sealed类型的类为密封类。Kotlin中的密封类必须使用关键字sealed进行说明。密封类是一种限制继承的类,具体而言,密封类的子类只能和密封类在相同文件中;除此之外,密封类是不能在其他文件中被继承的。
1.3.13 拆分结构
拆分结构的基本结构为(变量或常量名, 变量或常量名, …, 变量或常量名)。拆分结构可实现对一个对象中的多个数据项分拆使用。例如,在下列程序中,一个Object对象中的数据项被分别设置到a、b和c变量中。
1 data class Object(var it1: String, var it2: Int, var it3: Float)
2
3 fun main(args: Array<String>){
4 var obj = Object("item", 1, 0.1f)
5 var (a, b, c) = obj
6 println(a+" : "+b+" : "+c)
7 }
拆分结构还可在循环语句中使用,例如for((i, j) in collection){…};此外,针对Kotlin的Map对象也可以拆分结构。
拆分结构还可在方法的返回值中使用,例如:
1 data class Object(var it1: String, var it2: Int, var it3: Float)
2 fun func(): Object{
3 return Object("return", 2, 0.2f)
4 }
5 fun main(args: Array<String>){
6 var (d, e, f) = func()
7 println(d+" : "+e+" : "+f)
8 }
在拆分结构中,如果不使用某个变量或常量,可使用符号_(下画线)进行说明。例如,(_, e, f) = func()语句所运行的结果只包含两个值,分别为e和f所指代的值。
1.3.14 嵌套类和内部类
类可以在另一类的内部进行定义,这样的类称为嵌套类。与此相似,还可在一个类的内部定义内部类(也叫inner类)。两者的区别在于,嵌套类可通过外部类名来进行访问,而内部类必须通过外部类的实例来访问。例如,在下列程序中,A类中定义了一个嵌套类B;而AA类中定义了一个内部类BB;B类是通过A.B的方式进行访问的,而BB类是通过AA().BB的方式进行访问的:
1 class A{
2 class B{}
3 }
4 class AA{
5 inner class BB{}
6 }
7 fun main(args: Array<String>){
8 val c = A.B()
9 val cc = AA().BB()
10 }
一个类中还可使用匿名内部类,定义时需要使用“对象表达式”。
1.3.15 枚举类
枚举类被用于组织一组相互关联且类型相同的常量,例如,针对一周中的7天,可将周一至周日按常量的方式组织成一个枚举类。枚举类定义格式为:
enum class 类名{
项目1, 项目2, …, 项目n
}
例如:
1 enum class Transports{
2 car, airplane, boat
3 }
枚举类的使用方法为枚举类名.项目名,如Transports.car。枚举类中每个项目的位置都可通过ordinal属性获得,如Transports.car.ordinal。枚举类中的项目还可进一步指定属性值,例如:
1 enum class Transports(val s: Int){
2 car(60), airplane(1000), boat(40)
3 }
上述示例程序中,枚举类为Transports,其元素为car、airplane和boat,且它们被指定了具体的属性值,这些值被访问的方式类似于Transportans.boat.s。
1.3.16 this操作符
操作符this一般指代本类的实例。Kotlin中的this在使用时还可有更多的操作,例如:
1 class Simple Class{
2 val s="sa"
3 }
4 class Outer{
5 var o = 1
6 fun func(){
7 this@Outer.o //this@Outer是指Outer的实例
8 this.o //this是指Outer的实例
9 }
10 inner class Inner{
11 val i = "i"
12 fun func(){
13 this.i //this是指Inner的实例
14 this@Inner.i //this@Inner是指Inner的实例
15 this@Outer.o //this@Outer是指Outer的实例
16 }
17 fun Simple Class.service(){
18 this.s //this是指Simple Class的实例
19 this@service.s //this@service是指Simple Class的实例
20 this@Outer.o //this@Outer是指Outer的实例
21 this@Inner.i //this@Inner是指Inner的实例
22 }
23 }
24 }
上述示例程序中,this在不同的语境中所指代的实例不尽相同。首先,需要特别说明的是:在类内部定义的其他类扩展方法时,this是指代被扩展类的实例,例如,Simple Class.service方法中的this是指代Simple Class实例;其次,由于this在程序中具有不同的含义,可在this后可使用@操作符来进行实例的定位。