基于Kotlin的Android应用程序开发
上QQ阅读APP看书,第一时间看更新

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后可使用@操作符来进行实例的定位。