1 类和对象
类是 Kotlin 中的一种重要的数据类型,是组成 Kotlin 程序的基本元素。它封装了一类对象的数据和操作。为了方便使用,Kotlin 提供了很多种形式:标准类、枚举类、数据类、内部类、嵌套类、密封类等,此外还有抽象类和接口。
1.1 定义类
Kotlin 定义类的简单语法如下:
[修饰符J class 类名[constructor 主构造器]{
零个到多个次构造器定义 ...
零个到多个属性...
零个到多个方法 . ..
在上面的语法格式中,修饰符可以是 public | internal | private (只能出现其中之一)、 final | open | abstract (也只能出现其中之一),或者完全省略修饰符。
类名只要是一个合法的标识符即可,但这仅仅满足的是 Kotlin 的语法要求。如果从程序的可读性方面来看, Kotlin 的类名必须是由一个或多个有意义的单词连缀而成的,每个单词首字母大写,其他字母全部小写,单词与单词之间不要使用任何分隔符。
Kotlin 的类定义由类名、类头(指定其泛型声明、主构造器等)和用花括号包围的类体构成。类头和类体都是可选的。
对于一个类定义而言,可以包含三种最常见的成员:构造器、属性和方法,这三种成员都可以定义零个或多个,如果三种成员都只定义零个,就是一个空类。空类没有类体,可以省略花括号。例如:class Empty 通常来说,空类没有太大的实际意义。
属性用于定义该类的对象所包含的状态数据。
方法则用于定义该类的对象的行为特征或者 功能实现。
构造器用于构造该类的对象,Kotlin 通过调用构造返回该类的对象(无须使用 new)。
构造器是一个类创建对象的根本途径,如果一个类没有构造器,这个类通常无法创建对象。 一个 Kotlin 类可以有 0~1 个主构造器和 0~N 个次构造器。主构造器是类头的一部分,它跟在类名(和泛型声明)后。
class User constructor(firstName: String) {
}
由此可见,主构造器就是在类头使用 constructor 关键宇定义一个无执行体的构造器。虽然主构造器不能定义执行体,但可以定义多个形参,这些形参可以在属性声明、初始化块中使用。
如果主构造器没有任何注解或修饰符,则可以省略 constructor 关键字。即上面代码可改为如下形式:
class User (firstName: String) {
}
如果没有为非抽象类定义任何(主或次)构造器,系统会自动提供一个无参数的主构造器,该构造器默认用 public 修饰。一旦为一个类提供了构造器,系统将不再为该类提供构造器。
1.2 对象的产生和使用
创建对象的根本途径是构造器,调用某个类的构造器即可创建这个类的对象,并且无须使用 new 关键字。
fun main(args: Array<String>) {
// 使用 Person 类定义一个 Person 类型的变量
var p: Person
// 调用 Person 类的构造器,返回一个 Person 对象
// 将该 Person 对象赋给 p 变量
p = Person()
// 上面代码也可简写成如下形式:在定义 p 变量的同时为 p 变量赋值
// var p: Person = Person()
// 访问 p 的 name 属性,直接为该属性赋值
p.name = "刘德华"
// 调用 p 的 say() 方法,声明白 say() 方法时定义了一个形参
// 调用该方法必须为形参指定一个值
p.say("给我一杯忘情水,让我一夜不流泪!")
println(p.name)
}
class Person {
var name: String = ""
var age: Int = 0
fun say(content: String) {
println(content)
}
}
定义一个类就是为了重复创建该类的对象,同一个类的多个对象具有相同的特征,而类则定义了多个对象的共同特征。从某个角度来看,类定义的是多个对象的特征,因此类不是一种具体存在,对象才是具体存在。可以这样理解 : 刘德华不是人这个类,你也不是人这个类,我也不是人这个类,我们都只是人的对象。
1.3 对象的this引用
Kotlin 也提供了 this 关键字, this 关键宇总是指向调用该方法的对象 。根据 this 出现位置的不同, this 作为对象的默认引用有两种情形 。
- 构造器中引用该构造器正在初始化的对象。
- 方法中引用调用该方法的对象。
this 关键字最大的作用就是让类中的一个方法访问该类的另一个方法或属性。 假设定义了 一个 Dog 类,这个 Dog 对象的 run() 方法需要调用它的 jump() 方法,此时就可通过 this 关键字作为 jump()方法的调用者。
this 可以代表任何对象,当 this 出现在某个方法体中时,它所代表的对象是不确定的,但它的类型是确定的,它所代表的只能是当前类的实例:只有当这个方法被调用时,它所代表的对象才被确定下来。谁在调用这个方法, this 就代表谁。
例如,定义如下 Dog 类 。
class Dog {
// 定义一个 jump ()方法
fun jump() {
println("正在执行 jump 方法")
}
// 定义一个 run ()方法, run ()方法需要借助 jump ()方法
fun run() {
this.jump() // 使用 this 引用调用 run ()方法的对象
jump() // 可以省略this前缀
println("正在执行 run 方法")
}
}
上面 run()方法中的 this 代表了该方法的调用者:谁在调用 run()方法,那么 this 就代表谁 。 因此该方法表示:当一个 Dog 对象调用 run() 方法时, run() 方法需要依赖它自己的 jump() 方法。Kotlin 允许对象的一个成员直接调用另一个成员,可以省略this前缀。
除此之外, this 引用也可用于构造器中作为默认引用。由于构造器是直接调用的,而不是使用对象来调用的,所以 this 在构造器中代表该构造器正在初始化的对象。
class ThisInConstructor {
// 定义一个名为 foo 的属性
var foo: Int
constructor() {
// 在构造器中定义一个 foo 变量
val foo = 0
// 使用 this 代表该构造器正在初始化的对象
// 下面的代码将会把该构造器正在初始化的对象的 foo 属性设为 6
this.foo = 6
}
}
fun main(args: Array<String>) {
// 所有使用 ThisinConstructor 创建的对象的 foo 属性将被设为 6
println(ThisInConstructor().foo) // 输出6
}
与普通方法类似的是,大部分时候,在构造器中访问其他属性和方法时都可以省略 this 前缀,但如果构造器中有一个与属性同名的局部变量,又必须在构造器中访问这个被覆盖的属性,则必须使用 this 前缀,如上面的所示。
当 this 作为对象的默认引用使用时,程序可以像访问普通变量一样来访问这个 this 引用, 甚至可以把 this 当成普通方法的返回值。看下面程序。
class ReturnThis {
var age = 0
fun grow(): ReturnThis {
age++
// return this 返回调用该方法的对象
return this
}
}
fun main(args: Array<String>) {
val rt = ReturnThis()
// 可以连续调用同一个方法
rt.grow()
.grow()
.grow()
println("”口的 age 属性值是:" + rt.age)
}
从上面程序中可以看出,如果在某个方法中把 this 作为返回值,则可以多次连续调用同一个 方法,从而使得代码更加简洁。但是,这种把 this 作为返回值的方法可能造成实际意义的模糊。例如上面的 grow 方法,用于表示对象的生长,即 age 属性的值加 1,实际上不应该有返回值。
2 方法详解
方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。但 Kotlin 的方 法并不仅仅是单纯的方法, Kotlin 的方法与函数也有极大的关系 。
2.1 方法与函数的关系
Kotlin 的方法与函数其实是统一的,不仅定义函数和方法的语法相同,而且定义在类中的 方法依然可独立出来。 也就是说,即使我们将方法定义在类里面,这个方法也依然可以转换为 函数。
2.2 中缀表示法
Kotlin 的方法还可使用 infix 修饰,这样该方法就可通过中缀表示法调用,就像这些方法是双目运算符一样。 需要指出的是, infix 方法只能有一个参数,原因很简单,因为双目运 算符的后面只能带一个参数。
class ApplePack(weight: Double) {
var weight = weight
override fun toString(): String {
return "ApplePack[weight=${this.weight}]"
}
}
class Apple(weight: Double) {
var weight = weight
override fun toString(): String {
return "Apple[weight=${this.weight}]"
}
// 定义中缀方法,使用 infix 修饰
infix fun add(other: Apple): ApplePack {
return ApplePack(this.weight + other.weight)
}
// 定义中缀方法,使用 infix 修饰
infix fun drop(other: Apple): Apple {
this.weight -= other.weight
return this
}
}
fun main(args: Array<String>) {
var origin = Apple(3.4)
// 使用 add方法
val ap = origin add Apple(2.4)
println(ap)
origin drop Apple(1.4)
println(origin)
}
上面程序中定义了两个 infix 方法,其实没有任何特别的地方,只要注意两点:为方法添加 infix 修饰;该方法只有一个形参。
接下来 main() 函数中的两行粗体字代码示范了使用双目运算符的语法调用 infix 方法: 两个 Apple 执行 add 运算(调用 add()方法)时,程序返回了 一个 ApplePack 实例;两个 Apple 执行 drop 运算(执行 drop()方法)时,程序将名为 origin 的 Apple 对象的重量减少了。
输出:
ApplePack[weight=5.8]
Apple[weight=2.0]
2.3 componentN方法与解构
Kotlin 允许将一个对象的 N 个属性 “解构” 给多个变量,写法如下:
var (name, pass) = user
上面这行代码相当于将 user 对象的两个属性分别赋值给 name、 pass 两个变量,这两个变 量的类型会根据 user 对象的属性类型来推断。
Kotlin 怎么知道把哪两个属性分别赋值给 name、 pass 变量呢?其实 Kotlin 会将上面的赋值代码转换为如下两行:
var name = user.componentl()
var pass = user.component2()
从上面介绍可以看出,如果希望将对象解构给多个变量 ,那么必须为该对象的类定义 componentN() 方法。程序希望将对象解构给几个变量,就需要为该类定义几个 componentN() 方法,且该方法需要使用 operator 修饰。
下面程序示范了为 User 类定义 componentN() 方法,然后即可将该对象解构给 3 个变量。
class User(name: String, pass: String, age: Int) {
var name = name
var pass = pass
var age = age
// 定义 operator 修饰的 componentN 方法,用于解构
operator fun component1(): String {
return this.name
}
// 定义 operator 修饰的 componentN 方法,用于解构
operator fun component2(): String {
return this.pass
}
// 定义 operator 修饰的 componentN 方法,用于解构
operator fun component3(): Int {
return this.age
}
}
fun main(args: Array<String>) {
// 1、创建 User 对象
val user = User("赵雷", "123456", 19)
// 将 User 对象解构给 2 个变量
// ֻ只利用 user 对象的 component1() 和 component2 ()方法
var (name, pass: String) = user
println(name)
println(pass)
// 2、将 User 对象解构给 3 个变量
// ֻ利用 user 对象的 component1()、 component2() 和 component3() 方法
var (name2, pass2, age) = user
println(name2)
println(pass2)
println(age)
// 3、如果不想要前丽的某个属性,用“_”代替它
var (_, pass3, age2) = user
println(pass3)
println(age2)
}
上面程序中为 User 类定义了 3 个 operator 修饰的 compoentN() 方法,这就表明该类的实例最多可同时解构给 3 个变量。
接下来 main() 函数中将 user 对象解构给 2 个变量,此时将只解构 user 对象的前两个 componentN() 方法的返回值,这行代码还为 pass变量声明了类型: String,实际 上该类型是可推断出来的,因此完全可以不指定。
上面 2 中将 user 对象解构给 3 个变量,此时将会解构 user 对象的前 3 个 componentN() 方法的返回值。
在某些时候,程序希望解构对象后面几个 componentN()方法的返回值、忽略前面几个 componentN()方法的返回值,此时可通过下画线( _ )来占位。
上面 3 中希望解构 user对象的 component2()、 component3()两个方法的返回值,忽 略 component!()方法的返回值,因此程序在圆括号中的第一个位置处使用了下画线来代替。
2.4 数据类和返回多个值的函数
Kotlin 本身并不支持定义返回多个值的函数或方法,但通过上面所介绍的对象解构,我们 同样可让 Kotlin 函数返回多个值,本质是让 Kotlin 返回一个支持解构的对象。为了简化解构的实现, Kotlin 提供了一种特殊的类 :数据类,数据类专门用于封装数据。
数据类除使用 data 修饰之外,还要满足如下要求。
- 主构造器至少需要有一个参数。
- 主构造器的所有参数需要用 val 或 var 声明为属性。
- 数据类不能用 abstract、 open、 sealed 修饰,也不能定义成内部类。
- 在 Kotlin1.1 之前,数据类只能实现接口,现在数据类也可继承其他类。
定义数据类之后,系统自动为数据类生成如下内容。
- 生成 equals()、hashCode()方法。
- 自动重写 toString() 方法,返回形如 “User(name=John,age=42)” 的字符串。
- 为每个属性自动生成 operator 修饰的 componentN() 方法。
- 生成 copy() 方法, 用于完成对象复制。
下面程序将会先定义一个数据类,然后通过数据类来实现返回多个值的函数。
// 定义一个数据类
// 数据类会自动为每个属性定义对应的 componentN 方法
data class Result(val result: Int, val status: String)
fun factorial(n: Int): Result {
if (n == 1) {
return Result(1, "成功")
} else if (n > 1) {
return Result(factorial(n - 1).result * n, "成功")
} else {
return Result(-1, "参数必须大于0")
}
}
fun main(args: Array<String>) {
// 1、通过解构获取函数返回的两个值
var (rt, status) = factorial(6)
println(rt)
println(status)
// 2、
var (_, status2) = factorial(-6)
println(status2)
// 3、
var result = Result(2, "未知结果")
// 调用 copy ()方法完成复制
val oldRt = result.copy()
println(oldRt)
}
输出:
720
成功
参数必须大于0
Result(result=2, status=未知结果)
上面程序中定义了一个 factorial() 函数,该函数的返回值类型是 Result 数据类,该数据类会自动为它的 result、 status 两个属性生成 componentN() 方法,因此 Result对象支持解构。
上面程序的 main() 函数中的注释 1 将 factorial() 函数的执行结果解构rt、status 两个变量,这就相当于让该函数返回了两个值。
在注释 2 中,如果程序只需要解构 component2() 方法的返回值 ,则可用 “_” 忽略 component1() 方法的返回值。
在注释 3 中,系统为数据类生成的 copy() 方法可用于复制对象。
Kotlin 标准库提供了 Pair 和 Triple 两个数据类,正如它们的名字所暗示的,Pair 数据类可包含两个任意类型的属性;Triple 可包含三个任意类型的属性。在某些希望快速开发的场景下,我们也可直接使用 Kotlin 提供的 Pair 和 Triple 两个数据类。
2.5 在Lambda表达式中解构
Kotlin 允许对 Lambda 表达式使用解构 ,如果 Lambda 表达式的参数是支持解构的类型(如 Pair 或 Map.Entry 等,它们都具有 operator 修饰的 componentN()方法),那么即可通过将它们放在括号中引入多个新参数来代替单个参数。例如,如下两种写法是一样的 :
map.mapValues { entry -> "${entry.value}"}
// 使用解构,将 entry 解构成(key, value)
map mapValues { (key, value) -> "$value"}
请注意 Lambda 表达式包含两个参数和使用解构的区别:
{ a -> ... } // 一个参数
{ a, b -> ... } // 两个参数
{ (a, b)) -> ... } // 一个解构对
{ (a, b), c -> ... } // 一个解构对和第三个参数
可以看出, Lambda 表达式的多个参数是不需要使用圆括号的,只要看到在 Lambda 表达式的形参列表中出现圆括号,那就是使用解构。
如果希望只使用后面几个 componentN() 方法的返回值,则可使用下画线来代替。 例如,如下代码只使用 Map 的 value。
map.mapValues {(_, value) -> "$value!"}
3 属性和字段
属性是 Kotlin 的一个重要特色, Kotlin 的属性相当于 Java 的字段再加上 getter 和 setter 方法(只读属性没有 setter方法),而且开发者不需要自己实现 getter 和 setter 方法。
3.1 读写属性和只读属性
Kotlin 使用 val 定义只读属性,使用 var 定义读写属性 ,系统会为只读属性生成 getter 方法,会为读写属性生成 getter 和 setter 方法。
在定义 Kotlin 的普通属性时,需要程序员显式指定初始值,要么在定义时指定初始值,要么在构造器中指定初始值。
下面代码定义了一个 Address 类,并为该类定义了多个属性。
class Address {
var street: String = ""
var city = ""
var province = ""
var postCode: String? = null
}
在定义属性时,如果系统可根据属性初始值推断出属性的类型,那么程序就可以不显式指定属性的类型。
接下来程序即可使用点语法来操作属性。
fun main(args: Array<String>) {
var addr = Address()
// 通过点语法对属性赋值,实际就是调用 setter 方法
addr.street = "人民广场"
addr.city = "长春"
// 通过点语法访问属性,实际就是调用 getter 方法
println(addr.city)
println(addr.street)
}
需要指出的是,虽然 Kotlin 确实会为属性生成 getter、 setter 方法,但由于源程序中并未真正定义这些 getter、 setter 方法,因此 Kotlin 程序不允许直接调用 Address 对象的 getter、 setter 方法。
但如果使用 Java 程序来调用 Address 类,由于该 Address 类中各属性对应的属性都用 了 private 修饰,因此不能用点语法直接访问这些日eld,所以只能用 getter、setter 方法来访问属性。
在 Kotlin 类中定义属性后,被 Kotlin 程序使用时只能使用点语法访问属性; 被 Java 程序使用时只能通过 getter、setter 方法访问属性。
使用 val 声明的是只读属性,则只有 getter 方法,没有 setter 方法。只读属性的语法和读写属性的语法有两方面的不同:
- 只读属性用 val 定义,读写属性用 var 定义。
- 只读属性不允许有 setter 方法。
例如如下 Item类。
class Item(barCode: String, name: String, price: Double){
// 定义属性,使用主构造器的参数为它们分配初始值
val barCode = barCode
val name = name
val price = price
}
由于上面 Item 类定义了一个主构造器,因此系统不再为该类自动提供无参数的构造器。 此外,由于程序为 Item 类定义了 3 个只读属性,因此系统为它们生成 3 个 final 修饰的 getter 方法 。
Kotlin 程序使用 Item 类时,同样只能用点语法来获取其属性值;但如果是 Java 程序使用 Item 类,则需要使用 getter 方法来获取属性值。 如下程序。
fun main(args: Array<String>) {
// 调用有参数的构造器创建实例
var im = Item("001", "华为P30 Pro", 6999)
// 通过点语法访问对象的属性
println(im.barCode)
println(im.name)
println(im.price)
}
3.2 自定义 getter 和 setter
定义 getter 和 setter方法时无须使用 fun 关键字。下面程序定义了一个简单的 User 类,该类包括自定义的 getter 方法。
class User(first: String, last: String) {
var first: String = first
var last: String = last
val fullName: String
// 自定义 getter 方法
get() {
println("执行 fullName 的 getter 方法")
return "${first}.${last}"
}
}
上面方法定义了 first 和 last 两个属性, Kotlin 会为这两个属性分别生成一个 field、 getter 和 setter 方法。
程序中粗体字代码为 User 类定义了 fullName 属性,并为该属性重新定义了 getter 方法。 由于该属性是一个只读属性,因此系统不需要为它生成 setter 方法。
需要指出的是 ,由于 fullName 井不需要真正存储状态,它的返回值其实是通过 first 和 last 两个属性拼凑出来的, 因此 Kotlin 也不需要为其生成对应的 field。当 Kotlin 不需要为该属性生成对应的 field 时,也就不能为该属性指定初始值,所以上面程序没有为 fullName 属性指定初始值。
下面程序示范了如何使用该 User 类。
fun main(args: Array<String>) {
var user = User("詹姆斯", "哈登")
// 输出 user.fullName,实际上是调用其 getter 方法返回值
println(user.fullName)
}
输出:
执行 fullName 的 getter 方法
詹姆斯.哈登
从上面输出可以看出,程序访问 user.fullName 时实际上就是调用了它的印 fullName 的 getter 方法。
上面程序中 User 类的 getter 是一个只读属性,其实程序也可将其定义成一个读写属性。 如果如 fullName 是一个读写属性,那么程序既可重写它的 getter 方法,也可重写它的 setter 方法。
对于只读属性来说,由于该属性只有 getter 方法,因此只能重写 getter 方法; 对于读写属性来说,由于该属性既有 getter 方法,也有 setter 方法,因此可根据需要重写其中之一,也可同时重写它们。
class User(first: String, last: String) {
var first: String = first
var last: String = last
// 由于可通过 getter 方法推断出该属性的类型,因此可省略类型声明
var fullName
// 使用单表达式定义 getter 方法 的方法体
get() = "${first}.${last}"
set(value) {
println("ִ执行 fullName 的 setter 方法”")
// value 字符串中不包含“.”或包含几个“.”都不行
if ("." !in value || value.indexOf(".") != value.lastIndexOf(".")) {
println("您输入的 fullName 不合法")
} else {
var tokens = value.split(".")
first = tokens[0]
last = tokens[1]
}
}
}
fun main(args: Array<String>) {
var user = User("詹姆斯", "哈登")
// 使用点语法赋值,实际上是调用 setter 方法
user.fullName = "特雷西.麦克格雷迪"
println(user.first)
println(user.last)
}
输出:
ִ执行 fullName 的 setter 方法”
特雷西
麦克格雷迪
3.3 幕后字段
在 Kotlin 中定义一个普通属性时, Kotlin 会为该属性生成一个 field (字段)、 getter 和 setter 方法(只读属性没有 setter 方法)。 Kotlin 为该属性所生成的 field 就被 称为幕后字段。
如果 Kotlin 类的属性有幕后宇段,则 Kotlin 要求为该属性显式指 定初始值:要么在定义时指定,要么在构造器中指定
如果 Kotlin 类的属性没有幕后宇段, 则 Kotlin 不允许为该属性指定初始值(这是理所当然的 ,由于没有 field,即使指定了初始值 也没地方保存)
那么 Kotlin 何时会为属性生成幕后宇段呢?只要满足以下条件,系统就会为属性生成幕 后字段。
-
该属性使用 Kotlin 自动生成的 getter 和 setter 方法或其中之一。换句话说,对于只读属性,必须重写 getter 方法;对于读写属性,必须重写 getter、 setter 方法 : 否则总会为该属性生成幕后宇段。
-
重写 getter、 setter 方法时,使用 field 关键字显式引用了幕后字段。
通过上面描述可以发现, Kotlin 允许开发者在 getter 或 setter 方法中通过 field 引用系统自动生成的字段(幕后宇段)。
例如,有时候我们希望对用户设置的属性值进行控制 ,此时就可以重写 setter 方法,并在 setter 方法中加入自己的控制。例如如下代码。
class Person(name: String, age: Int) {
// 使用 private 修饰属性,将这些属性隐藏起来
var name = name
set(newName) {
// ִ执行合理性校验,要求用户名必须在 2-6 位之间
if (newName.length > 6 || newName.length < 2) {
println("”您设置的人名不符合要求”")
} else {
field = newName
}
}
var age = age
set(newAge) {
// ִ执行合理性校验,要求用户年龄必须在 0-100 之间
if (newAge > 100 || newAge < 0) {
println("您设置的年龄不合法”")
} else {
field = newAge
}
}
}
fun main(args: Array<String>) {
var p = Person("黄家驹", 31)
p.age = 120 // 赋值非法,赋值失败
println(p.age) // 输出29
p.age = 29 // 赋值合法,赋值成功
println(p.age) // 输出25
}
3.4 幕后属性
在个别情况下,开发者希望自己定义 field,并为该 field 提供 setter、 getter 方法,就像 Java 所使用的方法。此时可使用 Kotlin 的幕后属性。
幕后属性就是用 private 修饰的属性, Kotlin 不会为幕后属性生成任何 setter、 getter方法。 因此程序不能直接访问幕后属性,必须由开发者为幕后属性提供 setter、 getter方法 。
例如如下程序。
class BackingProperty(name: String) {
// 定义 private 修饰的属性,该属性是幕后属性
private var _name: String = name
var name
// 重写 getter 方法,返回幕后属性的值
get() = _name
set(newName) {
// ִ执行合理性校验,要求用户名必须在 2~6 位之间
if (newName.length > 6 || newName.length < 2) {
println("您设置的人名不符合要求")
} else {
// 对幕后属性赋值
_name = newName
}
}
}
fun main(args: Array<String>) {
var p = BackingProperty("黄贯中")
// 访问 p.name,实际上会转为访问幕后属性口 _name
println(p.name)
// 对 p.name 赋值,实际上会转为对幕后属性 name 赋值
p.name = "范.海伦"
println(p.name)
}
上面程序中定义了 一个 private 修饰的 _name 属性,该属性就是一个幕后属性, Kotlin 不会为该属性生成 getter、setter 方法,因此程序无法直接访问 Person 对象的 _name 属性。
接下来程序定义了 一个 name 属性,井重写了 name 属性的 getter、setter方法,重写的 setter、 getter 方法实际上访问的是 name 幕后属性。
上面这种方式就是 Java 的做法:先定义一个 private 修饰的字段,然后再为该字段定义 public 修饰的 setter、 getter 方法。
3.5 延迟初始化属性
Kotlin 提供了 lateinit 修饰符来解决属性的延迟初始化。使用 lateinit 修饰的属性,可以在定义该属性时和在构造器中都不指定初始值。
对 lateinit 修饰符有以下限制。
- lateinit 只能修饰在类体中声明的可变属性(使用 val 声明的属性不行,在主构造器中 声明的属性也不行)。
- lateinit 修饰的属性不能有自定义的 getter 或 setter 方法。
- lateinit 修饰的属性必须是非空类型。
- lateinit 修饰的属性不能是原生类型。
与 Java 不同的是, Kotlin 不会为属性执行默认初始化。因此,如果在 lateinit 属性赋初始值之前访问它,程序将会引发 “lateinit property name has not been initialized” 异常。
例如,如下程序定义了两个延迟初始化的属性。
import java.util.Date
class User {
// 延迟初始化属性
lateinit var name: String
lateinit var birth: Date
}
fun main(args: Array<String>) {
var user = User()
// println(user.name) // 引发异常
// println(user.birth) // 引发异常
user.name = "齐达内"
user.birth = Date()
println(user.name)
println(user.birth)
}
上面程序中定义了 name、 birth 两个 lateinit 属性,这样即可在定义该属性时和在构造器中都不为其指定初始值。 由于 User 的 name、 birth 两个属性都没有初始值,因此程序创建 User 对象后不能立即访 问它的 name、 birth 属性,否则将会引发异常,如上面程序中两行注释掉的代码所示。
3.6 内联属性
从 Kotlin 1.1 开始, inline 修饰符可修饰没有幕后字段的属性的 getter 或 setter 方法,既可单独修饰属性的 getter 或 setter 方法;也可修饰属性本身,这相当于同时修饰该属性的 getter 和 setter 方法。
对于使用 inline 修饰的 getter 和 setter 方法,程序在调用 getter 或 setter 方法时也会执行内联化。 如下程序示范了内联属性。
class Name(name: String, desc: String) {
var name = name;
var desc = desc;
}
class Product {
var productor: String? = null
// inline 修饰属性的 getter 方法,表明读取属性时会内联化
val proName: Name
inline get() = Name("倚天屠龙记", "倚天剑屠龙刀")
// inline 修饰属性的 setter 方法,表明设置属性时会内联化
var author: Name
get() = Name("金庸", "无")
inline set(v) {
this.productor = v.name
}
// inline 修饰属性本身,表明读取和设置属性时都会内联化
inline var pubHouse: Name
get() = Name("TVB", "无")
set(v) {
this.productor = v.name
}
}
上面程序为 Product 定义了 3 个属性 : proName、 author 和 pubHouse,这 3 个属性都是没有幕后宇段的属性,因此 Kotlin 允许将它们变成内联属性。
使用 inline 修饰了 proName 属性的 getter 方法,这意味着当程序读取该属性的值时会变成内联化。
使用 inline 修饰了 author 属性的 setter 方法,这意味着当程序设置该属性时 会变成内联化。
使用 inline 修饰了 pubHouse 属性本身,这意味着当程序读取和设置该属性时都会变成内联化。
4 隐藏和封装
封装是面向对象的三大特征之一(另外两个特征是继承和多态) ,指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在 对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的。
- 隐藏类的实现细节。
- 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问。
- 可进行数据检查,从而有利于保证对象信息的完整性。
- 便于修改,提高代码的可维护性。
为了实现良好的封装,需要从两个方面考虑。
- 将对象的属性和实现细节隐藏起来,不允许外部直接访问。
- 把方法暴露出来,让方法来控制对这些属性进行安全的访问和操作。
4.1 包和导包
Kotlin 的包与 Java 的包相同,既是逻辑上的一个程序单元,也是一个命名空间。如果希望把函数、类放在指定的包结构下,则应该在 Kotlin 源程序的第一个非注释行放置如下格式的代码:
package 包名
与 Java 的包机制 完全相同的是, Kotlin 的包也需要两个方面的保证:
- 在源程序中使用 package 语句指定包名。
- 必须将 class 文件放在对应的路径下。
为了使用其他包中的函数和类, Kotlin 同样使用 import 执行导入 。 Kotlin 的 import 语法和 Java类似,同样支持精确导入和通配符导入。
- 精确导入:
import foe.Bar // 导入 foo 包中的 Bar,以后使用 Bar 时无需包名
- 通配符导入
import foo.* //导入 foo 包中的所有成员,以后使用 foo 包中的所有成员时都无需包名
Kotlin 的 import 功能比较强大, 它相当于 Java 的 import 和 import static (静态导入)的合体,它不仅可以导入类,还可以导入如下内容。
- 顶层函数及属性。
- 在对象声明中声明的函数和属性。
- 枚举常量。
此外,如果需要在同一个源文件中导入不同包中的同名类(比如 java.util.Date 和 java.sql.Date),以前 Java 程序很难处理这种情况,通常有一个类总要使用全限定类名(放弃导包),但 Kotlin 提供了更简单的处理方式。看如下代码。
import java.util.Date
// 导入 java.sql.Date,并指定别名为 SDate
import java.sql.Date as SDate
fun main(args: Array<String>) {
// 使用 java.util.Date
var d = Date()
// 使用 java.sql.Date
var d2 = SDate(System.currentTimeMillis())
println(d)
println(d2)
}
从上面可以看出, Kotlin 的 import 语句支持 as 关键字,这样就可以为导入类指定别名, 从而解决了导入两个不同包中的同名类的问题。
4.2 Kotlin的默认导入
Kotlin 默认会导入如下包。
- kotlin.*
- kotlin.annotation.*
- kotlin.collections.*
- kotlin.comparisons.* (自 Kotlin 1.1 起)
- kotlin.io.*
- kotlin.ranges.*
- kotlin.sequences.*
- kotlin .text. *
此外,对于 JVM 平台,还会自动导入如下两个包。
- java.lang.*
- kotlin.jvm.*
对于 JavaScript平台 , 则额外导入如下包。
- kotlin.js.*
4.3 使用访问控制符
Kotlin 提供了 4 个访 问控制符 : private、 internal、 protected 和 public, 分别代表 4 个访问控制级别。 Kotlin 的 4 个访问控制符的意义如下。
- private:与 Java 的 private 类似, private 成员只能在该类的内部或文件的内部被访问。
- internal:internal 成员可以在该类的内部或文件的内部或者同一个模块内被访问。
- protected:protected 成员可以在该类的内部或文件的内部或者其子类中被访问。
- public:public 成员可以在任意地方被访问。
与 Java 不同的是 ,如果 Kotlin 没有显式指定修饰符的话 ,默认的访问控制修饰符是 public。 根据上面描述不难发现, Kotlin 的访问控制符与 Java 的区别有如下几点。
- Kotlin 取消了 Java 的默认访问权限(包访问权限),引入了 internal 访问控制符(模块访问权限)。
- Kotlin 取消了 protected 的包访问权限。
- Kotlin 的默认访问控制符是 public。
此处涉及一个问题:何为模块?模块是编译在一起的一套 Kotlin 文件,模块的存在形式 有如下几种。
- 一个 IntelliJ IDEA 模块。
- 一个 Maven 项目。
- 一个 Gradle 源集。
- 一次 的 Ant 任务执行所编译的一套文件。
5 深入构造器
5.1 主构造器和初始化块
Kotlin 类可以定义 0~1 个主构造器和 0~N 个次构造器。如果主构造器没有任何注解或可见性修饰符,则可以省略 constructor 关键字。
主构造器作为类头的一部分,可以声明形参,但它自己并没有执行体。那么主构造器的形 参有什么用呢?其作用主要有两点。
- 初始化块可以使用主构造器定义的形参。
- 在声明属性时可以使用主构造器定义的形参。
由此可见, Kotlin 的主构造器并不是传统意义上的构造器,它更像 Java 的初始化块,或者说是对初始化块的增强,Java 的初始化块不能传入参数; Kotlin 通过主构造器的设计,允许为初始化块传入参数。
初始化块的语法格式如下:
init {
// 初始化块中的可执行代码,可以使用主构造器定义的参数
...
}
初始化块中的代码可以使用主构造器定义的参数,也可以包含任何可执行语句,包括定义 局部变量、调用其他对象的方法,以及使用分支、循环语句等。
下面程序定义了一个 Person 类,它既包含了主构造器,也包含了初始化块。
class Person(name: String) {
// 下面定义一个初始化块
init {
var a = 6
if (a > 4) {
println("Person 初始化块:局部变量 a 的值大于 4")
}
println("Person 初始化块")
println("name 参数为:${name}")
}
// 定义第二个初始化块
init {
println("Person 的第二个初始化块")
}
}
fun main(args: Array<String>) {
Person("Beyond")
}
上面程序的 main() 函数只创建了一个 Person 对象,程序输出如下:
Person 初始化块:局部变量 a 的值大于 4
Person 初始化块
name 参数为:Beyond
Person 的第二个初始化块
从运行结果可以看出,当程序通过主构造器创建对象时,系统其实就是调用该类里定义的初始化块,如果一个类里定义了两个普通初始化块,则前面定义的初始化块先执行,后面定义的初始化块后执行。
和 Java 类似的是, Kotlin 也允许一个类里定义两个初始化块,但这没有任何意义。因为初始化块是在创建对象时隐式执行的,而且它们总是全部执行,因此完全可以把多个普通初始化块合并成一个初始化块,从而让程序更加简洁,可读性更强。
从上面结果不难看出,程序调用主构造器创建对象,实际上就是执行初始化块。由此可见, 主构造器的主要作用就是为初始化块定义参数,因此主构造器更像是初始化块的一部分。也可以说,初始化块就是主构造器的执行体 。
构造器最大的用处就是在创建对象时执行初始化,但由于初始化块就是主构造器的执行体, 因此,如果希望为对象的属性显式指定初始值,则也可以通过初始化块来指定。
如果程序员没有为 Kotlin 类提供任何构造器(主构造器和次构造器),则系统会为这个类 提供一个无参数的主构造器,这个构造器的执行体为空,不做任何事情。无论如何, 至少包含一个构造器。
运行上面程序,将看到输出 ConstructorTest 对象时,它的 name 和 count 属性都是调用主 构造器时传入的参数。
一旦提供了自定义的构造器,系统就不再提供默认的构造器,因此上面的 ConstructorTest 类不能再通过 ConstructorTest() 代码来创建实例,因为该类不再包含无参数的构造器。
如果希望该类保留无参数的构造器,或者希望有多个初始化过程,则可以为该类提供多个 构造器。如果一个类里提供了多个构造器,那么就形成了构造器重载。
5.2 次构造器和构造器重载
Kotlin 允许使用 constructor 关键宇定义 N 个次构造器,次构造器类似于 Java 传统的构造 器。
而 Kotlin 的主构造器其实属于初始化块(或者说,初始化块其实是主构造器的执行体), 因此 Kotlin 要求所有的次构造器必须委托调用主构造器,可以直接委托或通过别的次构造器 间接委托。所谓“委托”,其实就是要先调用主构造器(执行初始化块中的代码),然后才执行 次构造器代码。
同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。
程序可通过不同的构造器来创建对象,但不管使用哪个构造器,首先都要先调用主构造器(执行初始化块代码)。
为了让系统能区分不同的构造器,这些构造器的参数列表必须不同。下面程序示范了构造器重载,利用构造器重载就可以通过不同的构造器来创建对象。
class ConstructorOverload {
var name: String?
var count: Int
init {
println("初始化块!")
}
// 提供无参数的构造器
constructor() {
name = null
count = 0
}
constructor(name: String, count: Int) {
this.name = name
this.count = count
}
}
fun main(args: Array<String>) {
// 通过无参数的构造器创建 ConstructorOverload 对象
var oc1 = ConstructorOverload()
// 通过有参数的构造器创建 ConstructorOverload 对象
var oc2 = ConstructorOverload(
"凤凰传奇", 200
)
println("${oc1.name} ${oc1.count}")
println("${oc2.name} ${oc2.count}")
}
上面的 ConstructorOverload 类提供了两个重载的次构造器,但它们的形参列表不同。在调 用构造器时,系统将根据传入的实参列表来决定调用哪个构造器。 运行上面程序,可看到如下输出:
初始化块!
初始化块!
null 0
凤凰传奇 200
从上面的运行结果可以看出,不管调用哪个构造器创建对象,系统总会先执行初始化块。 也就是说,初始化块总会在所有次构造器之前执行。用 Kotlin 的专业术语来说,叫作:所有的次构造器都要委托调用初始化块。
从上面的运行结果可以看出,使用不同的构造器初始化出来的对象具有不同的状态。
上面的 ConstructorOverload 没有定义主构造器,因此次构造器不需要委托主构造器。下面再定义一个带主构造器的类。
// 定义主构造器
class User(name: String) {
var name: String
var age: Int
var info: String? = null
init {
println("User 的初始化块")
this.name = name
this.age = 0
}
// 委托给主构造器
constructor(name: String, age: Int) : this(name) { // 注释1
this.age = age
}
// 委托给(String, Int)构造器
constructor(name: String, age: Int, info: String) : this(name, age) { // 注释2
this.info = info
}
}
fun main(args: Array<String>) {
// 调用主构造器
var us1 = User("少年时代")
println("${us1.name} => ${us1.age} => ${us1.info}")
// 调用(String, Int)构造器
var us2 = User("T-ara", 21)
println("${us2.name} => ${us2.age} => ${us2.info}")
// 调用(String, Int, String)构造器
var us3 = User("BigBang", 20, "权志龙")
println("${us3.name} => ${us3.age} => ${us3.info}")
}
上面程序中 User 类定义了一个带 String 参数的主构造器,接下来使用 constructor 定义了两个次构造器,其中注释 1 定义的次构造器委托了主构造器;
注释 2 定义的次构造器委托了注释 1 代码定义的次构造器,这样就间接委托了主构造器。
从上面代码可以看出, Kotlin 使用“:this(参数)”的语法委托另 一个构造器,到底委托哪个构造器则取决于传入的参数。系统会根据传入的参数来推断委托了哪个构造器。 主函数分别使用了 3 个构造器来创建实例,运行程序可看到如下输出 :
User 的初始化块
少年时代 => 0 => null
User 的初始化块
T-ara => 21 => null
User 的初始化块
BigBang => 20 => 权志龙
从上面输出可以看到,当程序调用主构造器创建实例时,程序调用两个次构造器创建实例时,也会先执行初始化块,这是由于它们都委托了主构造器的缘故。
5.3 主构造器声明属性
Kotlin 允许在主构造器上声明属性,直接在参数之前使用 var 或 val 即可声明属性。使 用 var 声明的是读写属性,使用 val 声明的是只读属性。当程序调用这种方式声明的主构造器创建对象时,传给该构造器的参数将会赋值给对象的属性。
例如如下程序。
// 使用主构造器声明属性
class Item(val code: String, var price: Double) {
}
fun main(args: Array<String>) {
var im = Item("1234567", 6.7)
println(im.code) // 输出 1234567
println(im.price) // 输出 6.7
}
如果主构造器的所有参数都有默认值,程序能以构造参数的默认值来调用该构造器(即不需要为构造参数传入值),此时看上去就像调用无参数的构造器。例如如下程序。
// 使用主构造器声明属性
class Customer(
val name: String = "匿名",
var addr: String = "天河”"
) {
}
fun main(args: Array<String>) {
// 调用有参数的主构造器
var ct = Customer("孙悟空", "花果山")
println(ct.name) // 输出 孙悟空
println(ct.addr) // 输出 花果山
// 以构造参数的默认值调用构造器,看上去像调用无参数的构造器
var ctm = Customer()
println(ctm.name) // 输出 匿名
println(ctm.addr) // 输出 天河
}
6 类的继承
Kotlin 的继承同样是单继承: 每个子类最多只有一个直接父类。
6.1 继承的语法
Kotlin 的子类继承父类的语法格式如下:
修饰符 class SubClass : Superclass {
// 类定义部分
}
如果在定义一个 Kotlin 类时并未显式指定这个类的直接父类,则这个类默认扩展 Any 类。 因此, Any 类是所有类的父类,要么是其直接父类,要么是其间接父类。
需要说明的是,Any 类不是 java.lang.Object 类,Any 类只有 equals()、hashCode() 和 toString() 这 3 个方法。
还有一点需要说明的是, Kotlin 的类默认就有 final 修饰,因此 Kotlin 的类默认是不能派 生子类的。 为了让一个类能派生子类,需要使用 open 修饰该类。
下面分主构造器和次构造器进行详细说明。
- 子类的主构造器
如果子类定义了主构造器,由于主构造器属于类头部分,为了让主构造器能调用父类构造器,因此主构造器必须在继承父类的同时委托调用父类构造器。例如如下代码。
open class BaseClass {
var name: String
constructor(name: String) {
this.name = name
}
}
// 子类没有显式声明主构造器
// 子类默认有一个主构造器,因此要在声明继承时委托调用父类构造器
class SubClass1 : BaseClass("foo") {
}
// 子类显式声明主构造器
// 主构造楼必须在声明继承时委托调用父类构造器
class SubClass2(name: String) : BaseClass(name) {
}
上面程序为 BaseClass 派生了两个子类:SubClass1 和 SubClass2。
其中 SubClass1 没有显式声明主构造器,系统会为该类自动生成一个无参数的主构造器,因此程序在继承 BaseClass 时必须立即调用父类构造器。
其中 SubClass2 显式定义了一个带参数的主构造器,因此程序同样需要在继承 BaseClass 时必须立即调用父类构造器。
- 子类的次构造器 次构造器同样需要委托调用父类构造器。
如果子类定义了主构造器,由于子类的次构造器总会委托调用子类的主构造器(直接或间 接),而主构造器一定会委托调用父类构造器,因此子类的所有次构造器最终也调用了父类构 造器。
如果子类没有定义主构造器,则此时次构造器委托调用父类构造器可分为 3 种方式。
- 子类构造器显式使用:this(参数) 显式调用本类中重载的构造器,系统将根据 this(参数) 调用中传入的实参列表调用本类中的另一个构造器。调用本类中的另一个构造器最终还是要调用父类构造器。
- 子类构造器显式使用:super(参数) 委托调用父类构造器,系统将根据 super(参数) 调用中传入的实参列表调用父类对应的构造器。
- 子类构造器既没有: super(参数) 调用,也没有:this(参数)调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。Kotiin 的次构造器相当于 Java 的构造器。
如下代码示范了没有主构造器的子类的次构造器是如何调用父类构造器的。
open class Base {
constructor() {
println("Base 的无参数的构造器”")
}
constructor(name: String) {
println("Base 的带一个 String参数:${name}的构造器:”")
}
}
class Sub : Base {
// 构造器没有显式委托
// 因此该次构造器将会隐式委托调用父类无参数的构造器
constructor() {
println("Sub 的无参数的构造器")
}
// 构造器用 this(name) 显式委托本类中带 String 参数的构造器
constructor(name: String) : super(name) {
println("Sub 的 String 构造器, String 参数为:${name}")
}
// 构造器用 this(name) 显式委托本类中带 String 参数的构造器
constructor(name: String, age: Int) : this(name) {
println("Sub 的 String 构造器,Int 构造器, Int 参数为:${name}")
}
}
fun main(args: Array<String>) {
Sub()
Sub("Sub")
Sub("子类", 29)
}
输出:
Base的无参数的构造器”
Sub的无参数的构造器
Base的带一个String参数:Sub的构造器:”
Sub的String构造器,String参数为:Sub
Base的带一个String参数:子类的构造器:”
Sub的String构造器,String参数为:子类
Sub的String构造器,Int构造器,Int参数为:子类
上面的 Sub 类没有定义主构造器,类体中定义了 3 个次构造器。
第一个次构造器既没有 super(参数)委托,也没有(this)委托,因此该构造器会隐式委托调用父类无参数的构造器。
第二个次构造器使用:super(name) 委托调用, 其中 name 是一个 String 类型的参数,因此该构造器属于显式委托调用父类带一个 String 参数的构造器。
第三个次构造器使用:this(name) 委托调用,其中 name 是一个 String 类型的参数,因此该 构造器属于显式委托调用该类中带一个 String 参数的构造器,即调用前一个构造器;而前一个构造器委托调用了父类构造器,因此该次构造器最终也调用了父类构造器一次。
注意:Kotlin 与 Java 的设计相同:所有子类构造器必须调用父类构造器一次。
当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此, 在执行父类构造器时,系统会再次上溯执行其父类构造器 ...... 依此类推,创建任何 Kotlin 对象,最先执行的总是 Any 类的构造器。
6.2 重写父类的方法
子类继承父类,将可以获得父类的全部属性和方法。
下面程序示范了子类继承父类的特点。如下是 Fruit 类的代码。
open class Fruit(var weight: Double) {
fun info() {
println("我是一个水果!重${weight}g")
}
}
接下来定义该 Fruit 类的子类 Apple, 程序如下。
class Apple : Fruit(0.0)
fun main(args: Array<String>) {
// 创建 Apple 对象
var a = Apple()
// Apple 对象本身没有 weight 属性
// 因为 Apple 的父类有 weight 属性,所以也可以访问 Apple 对象的 weight 属性
a.weight = 56.0
// 调用 Apple 对象的 info ()方法
a.info()
}
上面的 Apple 类只是一个空类,但程序中创建了 Apple 对象之后,可以访问该 Apple 对象的 weight 属性和 info() 方法,这表明 Apple 对象也具有了 weight 属性和 info() 方法,这就是继承的作用。
子类继承了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,额外增 加新的属性和方法。但有一种情况例外:子类需要重写父类的方法。
例如,鸟类都包含了飞翔方法,其中驼鸟是一种特殊的鸟类,因此驼鸟应该是鸟的子类,它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合驼鸟,所以驼鸟需要重写鸟类的方法。
下面程序先定义了一个 Bird类。
open class Bird {
// Bird类的 fly()方法
open fun fly() {
println("我在天空里自由自在地飞翔...")
}
}
上面的 fly() 方法,该方法同样使用了 open 修饰符 ,因为 Kotlin 默认为所有方法添加 final 修饰符,阻止该方法被重写,添加 open 关键字用于阻止 Kotlin 自动添加 final 修饰符。
下面再定义一个 Ostrich 类, 这个类扩展了 Bird 类, 重写了 Bird 类的时 fly() 方法。
class Ostrich : Bird() {
// 重写 Bird 类的 fly ()方法
override fun fly() {
println("我只能在地上奔跑...")
}
}
fun main(args: Array<String>) {
// 创建Ostrich对象
var os = Ostrich()
// ִ执行Ostrich对象的 fly()方法,将输出”我只能在地上奔跑...”
os.fly()
}
运行上面程序,将看到运行 os.fly() 时执行的不再是 Bird 类的 fly() 方法,而是执行 Ostrich 类的 fly() 方法。
从可以看出,Kotlin 类重写父类的方法必须添加 override 修饰符, 就像 Java 的 @Override 注解,只不过 Java 的 @Override 是可选的,而 Kotlin 的 override 修饰符是强制的。
这种子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。 可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
方法的重写要遵循 “两同两小一大” 规则,“两同” 即方法名相同、形参列表相同;“两小” 指的是子类方法的返回值类型应比父类方法的返回值类型更小或相等,子类方法声明抛出的异 常类应比父类方法声明抛出的异常类更小或相等;“一大” 指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方 法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中被覆盖的方法,则可以使用 super 作为调用者来调用父类中被覆盖的方法。
如果父类方法具有 private 访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。
如果子类中定义了一个与父类 private 方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,那么这不是重写,只是在子类中重新定义了 一个新方法。例如,下面代码是完全正确的。
open class BaseClass {
// test() 方法具有 private 访问权限,子类不可访问该方法
private fun test() {}
}
class Subclass : BaseClass() {
// 此处并不是方法重写,所以不可以用 override 修饰
fun test() {}
}
6.3 重写父类的属性
重写父类的属性与重写父类的方法大致相似:父类被重写的属性必须使用 open 修饰,子 类重写的属性必须使用 override 修饰。此外,属性重写还有如下两个限制。
- 重写的子类属性的类型与父类属性的类型要兼容。
- 重写的子类属性要提供更大的访问权限。 此处包含两方面的含义 : 1、在访问权限方面,子类属性的访问权限应比父类属性的访问权限更大或相等。2、只读属性可被读写属性重写,但读写属性不能被只读属性重写。
下面程序给出了重写父类属性的示例。
open class Item {
open protected var price: Double = 10.9
open val name: String = ""
open var validDays: Int = 0
}
class Book : Item {
// 正确重写了父类属性,类型兼容,访问权限更大
override public var price: Double
// 正确重写了父类属性,读写属性重写只读属性
override var name = "图书"
// 重写错误 ,只读属性不能重写读写属性
open val validDays: Int = 2
constructor() {
price = 3.0
}
}
6.4 super限定
如果需要在子类方法中调用父类中被覆盖的方法或属性,则可使用 super 限定。例如,为 上面的 Ostrich 类添加一个方法,在这个方法中调用 Bird 类中被覆盖的的 fly() 方法。
fun callOverridedMethod(){
// 在子类方法中通过 super 显式调用父类中被覆盖 的方法
super.fly()
}
借助 callOverridedMethod() 方法,就可以让 Ostrich 对象既可以调用自己重写的 fly() 方法,也可以调用 Bird 类中被覆盖的 fly() 方法(调用 callOverridedMethod() 方法即可)。
super是 Kotlin 提供的一个关键字,用于限定该对象调用它从父类继承得到的属性或方法。
如果子类重写了父类的属性,那么子类中定义的方法直接访问该属性默认会访问到子类中定义的属性,无法访问到父类中被重写的属性。
在子类定义的方法中可以通过 super 来访问父类中被重写的属性,如下面代码所示。
open class BaseClass {
open var a: Int = 5
}
class SubClass : BaseClass() {
override var a: Int = 7
fun accessOwner() {
println(a)
}
fun accessBase() {
// 通过 super 限定访问从父类继承得到的 a 属性
println(super.a)
}
}
fun main(args: Array<String>) {
val sc = SubClass()
sc.accessOwner() // 输出7
sc.accessBase() // 输出5
}
上面程序的 BaseClass和 Subclass 中都定义了名为 a 的属性,则 SubClass 的 a 属性将会重 写 BaseClass 的 a 属性。
程序中 super.a 时,使用 super 限定访问该对象从父类继承得到的 a 属性,而不是在当前类中定义的 a 属性。
如果子类中没有包含和父类同名的属性 ,那么在子类方法中访问该属性时,则无须显式使用 super 作为调用者。
如果在某个方法中访问名为 a 的属性,但没有显式指定调用者,则系统查找 a 的顺序为:
- 查找该方法中是否有名为 a 的局部变量。
- 查找当前类中是否包含名为 a 的属性。
- 查找 a 的直接父类中是否包含名为 a 的属性,依次上溯查找 a 的所有父类,直到 Any 类,如果最终不能找到名为 a 的属性,则系统出现编译错误。
6.5 强制重写
如果子类从多个直接超类型(接口或类)继承了同名的成员,那么 Kotlin 要求子类必须 重写该成员。
如果需要在子类中使用 super 来引用超类型中的成员,则可使用尖括号加超类型名限定的 super 进行引用。
如下程序示范了编译器强制重写的方法。
open class Foo {
open fun test() {
println("Foo的test")
}
fun foo() {
println("foo")
}
}
interface Bar {
// 接口中成员默认是 open 的
fun test() {
println("Bar的test")
}
fun bar() {
println("bar")
}
}
class Wow : Foo(), Bar {
// 编译器要求必须重写 test()
override fun test() {
super<Foo>.test() // 调用父类 Foo 的 test()
super<Bar>.test() // 调用父接口 Bar 的 test()
}
}
fun main(args: Array<String>) {
var w = Wow()
w.test()
}
上面程序在 Foo 类中定义了一个可被重写的 test() 方法,在 Bar 接口中也定义了一个可被重写的 test() 方法。
当子类 Wow 同时继承 Foo、 Bar 时, 它会获得两个直接父类中定义的 test() 方法,此时编译器要求子类 Wow 必须重写 test()方法,如上面程序的 Wow 类中的 test()方法所示。
如果程序希望在子类中调用父类 Foo 的 test() 方法,则可通过 super<Foo>.test() 进行调用;
如果希望在子类中调用父接口 Bar 的 test() 方法,则可通过 super<Bar>.test()进行调用。
运行上面程序,可看到如下输出 :
Foo的test
Bar的test
7 多态
与Java类似, Kotlin 的变量也有两个类型: 一个是编译时类型,一个是运行时类型。
编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态。
7.1 多态性
open class BaseClass {
open var book = 6
fun base() {
println("父类的普通方法")
}
open fun test() {
println("父类的被覆盖的方法")
}
}
class SubClass : BaseClass() {
// 重写父类的属性
override var book = 60
// 重写父类的方法
override fun test() {
println("子类的覆盖父类的方法")
}
fun sub() {
println("”子类的普通方法")
}
}
fun main(args: Array<String>) {
// 下面编译时类型和运行时类型完全一样,因此不存在多态
var bc: BaseClass = BaseClass()
// 输出 6
println(bc.book)
// 下面两次调用将执行 BaseClass 的方法
bc.base()
bc.test()
// 下面编译时类型和运行时类型完全一样,因此不存在多态
var sc: SubClass = SubClass()
// 输出 60
println(sc.book)
// 下面调用将执行从父类继承的 base ()方法
sc.base()
// 下面调用将执行当前类的 test ()方法
sc.test()
// 下面编译时类型和运行时类型不一样,多态发生
var ploymophicBc: BaseClass = SubClass()
// 输出 60 ,表明访问的依然是子类对象的属性
println(ploymophicBc.book)
// 下面调用将执行从父类继承的 base ()方法
ploymophicBc.base()
// 下面调用将执行当前类的 test ()方法
ploymophicBc.test()
// 因为 ploymophicBc 的编译时类型是 BaseClass
// BaseClass 类没有提供 sub ()方法,所以下面代码编译时会山现错误
// ploymophicBc.sub()
}
7.2 使用is检查类型
为了保证类型转换不会出错, Kotlin 提供了类型检查运算符: is 和 !is。
is 运算符的前一个操作数通常是一个变量,后一个操作数通常是一个类(也可以是接口, 可以把接口理解成一种特殊的类),它用于判断前面的变量是否引用后面的类,或者其子类、 实现类的实例。如果是,则返回 true,否则返回 false。
!is 就是 is 运算符的反义词。
下面程序示范了 is运算符的用法。
import java.util.Date
// 定义一个接口
interface Testable {}
fun main(args: Array<String>) {
// 声明 hello 时使用 Any 类,则 hello 的编译时类型是 Any
// hello 变量的实际类型是 String
val hello: Any = "Hello"
println("字符息是否是String类的实例:${hello is String}") // 返回 true
// Date 与 Any 类存在继承关系, 可以进行 is 运算
println("字符串是否是 Date 类的实例:${hello is Date}") //返回 false
// String没有实现 TestProtocol 协议,所以返回 false
println("”字符串是否是 Testable 协议的实例 :${hello is Testable}") //返回 false
val a: String = "Hello"
// String 类与 Date 类没有继承关系,所以下面代码编译出现错误
println("字符串是否是 Date类的实例:${a is Date}")
}
此外, Kotlin的 is 和 !is 都非常智能,只要程序使用 is 和 !is 对变量进行了判断,系统就会自动将变量的类型转换为目标类型。例如如下代码。
fun main(args: Array<String>) {
var a: Any = "fkit"
// ֱ直接访问 a 的 length 属性,编译器报错
// 由于 a 的编译时类型是 Any,因此编译时它没有 length 属性
// println(a.length)
// 先判断 a 为 String, 在 if 条件体中 a 被自动转换为 String 类型
if (a is String) println(a.length)
test(a)
when (a) {
// 如果进入该分支, 则表明 a 是 String 类型,可调用 String 的方法
is String -> println(a.length)
// 如果进入该分支,则表明 a 是 Int 类型,可调用 Int 的方法
is Int -> println(a.toDouble())
}
// 能进入 && 之后的表达式,表明 a 是 String 类型
// 因此在 && 之后可调用 a 的 length 属性
if (a is String && a.length > 3) {
println("a 的长度大于 3")
}
foo(a)
}
fun test(x: Any) {
// 如果 x 不是 String,函数返回
if (x !is String) return
// 因此以下部分,x 会被自动转换为 String 类型
println("x 的长度为:${x.length}")
}
fun foo(x: Any) {
// 能进入 || 之后的表达式,表明 a 是 String 类型
// 因此在 || 之后可调用 a 的 length 属性
if (x !is String || x.length == 0)
return
// 以下部分,x 会被自动转换为 String 类型
println("x 的长度为:${x.length}")
}
输出:
4
x 的长度为:4
4
a 的长度大于 3
x 的长度为:4
7.3 使用as运算符转型
除使用 is 自动转型之外, Kotlin 也支持使用 as 运算符进行强制转型 。 Kotlin 提供了两个向下转型运算符。
- as:不安全的强制转型运算符,如果转型失败,程序将会引发 ClassCastException 异常。
- as?:安全的强制转型运算符,如果转型失败,程序不会引发异常,而是返回 null。
下面是进行强制类型转换的示范程序。下面程序详细说明了哪些情况可以进行类型转换, 哪些情况不可以进行类型转换。
fun main(args: Array<String>) {
val obj: Any = "Hello"
// obj 变量的编译时类型为 Any, Any 与 String 存在继承关系,可以进行转换
// 而且 obj 实际引用的实例是 String 类型,所以运行时也可通过
val objStr = obj as String
println(objStr)
// 定义一个 objPri 变量,编译时类型为 Any,实际类型为 Int
val objPri: Any = 5
// objPri 变量的编译时类型为 Any, objPri 的运行时类型为 Int
// Any 与 String 存在继承关系,可以进行转换,编译通过
////但 objPri 变量实际引用的实例是 Int 类型 ,所以转换失败
val str: String = objPri as String // 转换失败
val fkit = "apache.org"
val s: String = "Kotlin"
// s 变量的编译时类型为 String, s 的运行时类型为 String
// 但 String 与 Number 不存在继承关系,因此编译发出警告:转换不可能成功
val num: Number = s as Number // 警告:转换不可能成功
}
使用 as 执行的强制转型是不安全的转换,如果强制转型失败则会引发 ClassCastException 异常,此时可用 as? 执行安全的类型转换, 使用 as? 进行转换时,如果转换失败则会返回 null。
由于 as? 转换返回的是可空的值,因此程序需要对 as? 转换的结果进行 null 判断。例如如 下程序。
open class Fruit {
var name: String
var weight: Double
constructor(name: String, weight: Double) {
this.name = name
this.weight = weight
}
}
class Apple : Fruit {
var color: String
constructor(name: String, weight: Double, color: String) : super(name, weight) {
this.color = color
}
}
class Grape : Fruit {
var sugarRate: Double
constructor(name: String, weight: Double, sugarRate: Double) : super(name, weight) {
this.sugarRate = sugarRate
}
}
fun main(args: Array<String>) {
// 使用数组保存 4 个水果
var fruits = arrayOf(
Apple("红富士", 1.8, "粉色"),
Apple("花牛果", 2.3, "红色"),
Grape("巨峰", 1.4, 0.34),
Grape("加州提子", 2.2, 0.45)
)
// 由于 fruits 的类型是 Array<Fruit>,因此程序只知道 该数组元素是 Fruit
for (f in fruits) {
// 此处使用 as?强制转型,因此 ap 的类型是 Apple?
var ap = f as? Apple
// 由于 ap 的类型是 ap?,因此程序使用?.语法来访问它的属性
println("${ap?.name}苹果的颜色是:${ap?.color}")
}
}
输出:
红富士苹果的颜色是:粉色
花牛果苹果的颜色是:红色
null苹果的颜色是:null
null苹果的颜色是:null