类与对象二
接口
Kotlin 的接口与 Java 8 类似,既包含抽象方法的声明,也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。
使用关键字 interface 来定义接口
interface MyInterface {
fun bar()
fun foo() {
// 可选的方法体
}
}
一个类或者对象可以实现一个或多个接口。
class Child : MyInterface {
override fun bar() {
// 方法体
}
}
接口中的属性
你可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们。
interface MyInterface {
val prop: Int // 抽象的
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
接口中的属性
接口可以定义成抽象的,也可定义成非抽象的,就是提供自定义的get,set方法的属性。
对于非抽象的属性,var属性,必须提供同时提供自定义的set和get,val提供get方法。原因,在接口中声明的属性不能有幕后字段(backing field),幕后字段:如果属性至少一个访问器使用默认实现,或者自定义访问器通过 field 引用幕后字段,将会为该属性生成一个幕后字段。
interface MyInterface {
val prop: Int // 抽象的
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
接口继承
一个接口可以从其他接口派生,从而既提供基类型成员的实现也声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现:
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName"
}
data class Employee(
// 不必实现“name”
override val firstName: String,
override val lastName: String,
val position: Position
) : Person
解决覆盖冲突
实现多个接口时,可能会遇到同一方法继承多个实现的问题。例如
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
override fun bar() {
super<B>.bar()
}
}
对于C 必须实现A的bar,对于D必须实现A 的bar()
上例中,接口 A 和 B 都定义了方法 foo() 和 bar()。 两者都实现了 foo(), 但是只有 B 实现了 bar() (bar() 在 A 中没有标记为抽象, 因为没有方法体时默认为抽象)。因为 C 是一个实现了 A 的具体类,所以必须要重写 bar() 并实现这个抽象方法。
然而,如果我们从 A 和 B 派生 D,我们需要实现我们从多个接口继承的所有方法,并指明 D 应该如何实现它们。这一规则既适用于继承单个实现(bar())的方法也适用于继承多个实现(foo())的方法。
可见性
类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。 (getter 总是与属性有着相同的可见性。) 在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal 和 public。 如果没有显式指定修饰符的话,默认可见性是 public。
包
函数、属性和类、对象和接口可以在顶层声明,即直接在包内:
// 文件名:example.kt
package foo
fun baz() { ... }
class Bar { ... }
如果你不指定任何可见性修饰符,默认为 public,这意味着你的声明将随处可见;
如果你声明为 private,它只会在声明它的文件内可见;
如果你声明为 internal,它会在相同模块内随处可见;
protected 不适用于顶层声明。
注意:要使用另一包中可见的顶层声明,仍需将其导入进来。
例如:
// 文件名:example.kt
package foo
private fun foo() { …… } // 在 example.kt 内可见
public var bar: Int = 5 // 该属性随处可见
private set // setter 只在 example.kt 内可见
internal val baz = 6 // 相同模块内可见
类和接口
对于类内部声明的成员:
private 意味着只在这个类内部(包含其所有成员)可见;
protected—— 和 private一样 + 在子类中可见。
internal —— 能见到类声明的 本模块内 的任何客户端都可见其 internal 成员;
public —— 能见到类声明的任何客户端都可见其 public 成员。
注意 对于Java用户:Kotlin 中外部类不能访问内部类的 private 成员。
如果你覆盖一个 protected 成员并且没有显式指定其可见性,该成员还会是 protected 可见性。例子:
open class Outer {
private val a = 1
protected open val b = 2
internal val c = 3
val d = 4 // 默认 public
protected class Nested {
public val e: Int = 5
}
}
class Subclass : Outer() {
// a 不可见
// b、c、d 可见
// Nested 和 e 可见
override val b = 5 // “b”为 protected
}
class Unrelated(o: Outer) {
// o.a、o.b 不可见
// o.c 和 o.d 可见(相同模块)
// Outer.Nested 不可见,Nested::e 也不可见
}
构造函数
要指定一个类的的主构造函数的可见性,使用以下语法(注意你需要添加一个显式 constructor 关键字):
class C private constructor(a: Int) { …… }
这里的构造函数是私有的。默认情况下,所有构造函数都是 public,这实际上等于类可见的地方它就可见(即 一个 internal 类的构造函数只能在相同模块内可见).
局部声明
局部变量、函数和类不能有可见性修饰符。
模块
可见性修饰符 internal 意味着该成员只在相同模块内可见。更具体地说, 一个模块是编译在一起的一套 Kotlin 文件:
一个 IntelliJ IDEA 模块;
一个 Maven 项目;
一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明);
一次 Ant 任务执行所编译的一套文件。
嵌套类与内部类
和java不同,写在类内部的如果不加inner关键字,只是嵌套类,加了inner类才是内部类
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
val demo = Outer.Nested().foo() // == 2
内部类
类可以标记为 inner 以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用:
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() // == 1
This 表达式
为了表示当前的 接收者 我们使用 this 表达式:
在类的成员中,this 指的是该类的当前对象。
在扩展函数或者带有接收者的函数字面值中, this 表示在点左侧传递的 接收者 参数。
如果 this 没有限定符,它指的是最内层的包含它的作用域。要引用其他作用域中的 this,请使用 标签限定符:
限定的 this
要访问来自外部作用域的this(一个类 或者扩展函数, 或者带标签的带有接收者的函数字面值)我们使用this@label,其中 @label 是一个代指 this 来源的标签:
class A { // 隐式标签 @A
inner class B { // 隐式标签 @B
fun Int.foo() { // 隐式标签 @foo
val a = this@A // A 的 this
val b = this@B // B 的 this
val c = this // foo() 的接收者,一个 Int
val c1 = this@foo // foo() 的接收者,一个 Int
val funLit = lambda@ fun String.() {
val d = this // funLit 的接收者
}
val funLit2 = { s: String ->
// foo() 的接收者,因为它包含的 lambda 表达式
// 没有任何接收者
val d1 = this
}
}
}
}
匿名内部类
使用对象表达式创建匿名内部类实例:
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { …… }
override fun mouseEntered(e: MouseEvent) { …… }
})
如果对象是函数式 Java 接口(即具有单个抽象方法的 Java 接口)的实例, 你可以使用带接口类型前缀的lambda表达式创建它:
val listener = ActionListener { println("clicked") }
数据类
我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data:
data class User(val name: String, val age: Int)
1、编译器自动从主构造函数中声明的所有属性导出以下成员:
— equals()/hashCode() 对;
— toString() 格式是 “User(name=John, age=42)”;
— componentN() 函数 按声明顺序对应于所有属性;
— copy() 函数(见下文)。
2、为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:
— 主构造函数需要至少有一个参数;
— 主构造函数的所有参数需要标记为 val 或 var;
— 数据类不能是抽象、开放、密封或者内部的;
— (在1.1之前)数据类只能实现接口。
— 成员生成遵循关于成员继承的这些规则:
3、如果在数据类体中有显式实现 equals()、 hashCode() 或者 toString(),或者这些函数在父类中有 final 实现,那么不会生成这些函数,而会使用现有函数;
4、如果超类型具有 open 的 componentN() 函数并且返回兼容的类型, 那么会为数据类生成相应的函数,并覆盖超类的实现。如果超类型的这些函数由于签名不兼容或者是 final 而导致无法覆盖,那么会报错;
从一个已具 copy(……) 函数且签名匹配的类型派生一个数据类在 Kotlin 1.2 中已弃用,并且会在 Kotlin 1.3 中禁用。
不允许为 componentN() 以及 copy() 函数提供显式实现。
自 1.1 起,数据类可以扩展其他类(示例请参见密封类)。
5、在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值。 (参见构造函数)。
data class User(val name: String = "", val age: Int = 0)
6、在类体中声明的属性
请注意,对于那些自动生成的函数,编译器只使用在主构造函数内部定义的属性。如需在生成的实现中排出一个属性,请将其声明在类体中:
data class Person(val name: String) {
var age: Int = 0
}
在 toString()、 equals()、 hashCode() 以及 copy() 的实现中只会用到 name 属性,并且只有一个 component 函数 component1()。虽然两个 Person 对象可以有不同的年龄,但它们会视为相等。
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
Target platform: JVMRunning on kotlin v. 1.3.21
复制
在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 copy() 函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
这让我们可以写:
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
数据类与解构声明
为数据类生成的 Component 函数 使它们可在解构声明中使用:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // 输出 "Jane, 35 years of age"
密封类
定义
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
特点
1、密封类需要加sealed 修饰符
2、密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。(在 Kotlin 1.1 之前, 该规则更加严格:子类必须嵌套在密封类声明的内部)
3、一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员。
4、构造函数只能是private的
5、扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
好处
使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用。
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}
枚举类
枚举类的最基本的用法是实现类型安全的枚举:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
每个枚举常量都是一个对象。枚举常量用逗号分隔。
初始化
因为每一个枚举都是枚举类的实例,所以他们可以是这样初始化过的:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
匿名类
枚举常量也可以声明自己的匿名类:
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
及相应的方法、以及覆盖基类的方法。注意,如果枚举类定义任何成员,要使用分号将成员定义中的枚举常量定义分隔开,就像在 Java 中一样。
枚举条目不能包含内部类以外的嵌套类型(已在 Kotlin 1.2 中弃用)。
在枚举类中实现接口
一个枚举类可以实现接口(但不能从类继承),可以为所有条目提供统一的接口成员实现,也可以在相应匿名类中为每个条目提供各自的实现。只需将接口添加到枚举类声明中即可,如下所示:
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};
override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
使用枚举常量
就像在 Java 中一样,Kotlin 中的枚举类也有合成方法允许列出定义的枚举常量以及通过名称获取枚举常量。这些方法的签名如下(假设枚举类的名称是 EnumClass):
EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>
如果指定的名称与类中定义的任何枚举常量均不匹配,valueOf() 方法将抛出 IllegalArgumentException 异常。
自 Kotlin 1.1 起,可以使用 enumValues() 与 enumValueOf() 函数以泛型的方式访问枚举类中的常量 :
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}
printAllValues<RGB>() // 输出 RED, GREEN, BLUE
每个枚举常量都具有在枚举类声明中获取其名称与位置的属性:
val name: String
val ordinal: Int
枚举常量还实现了 Comparable 接口, 其中自然顺序是它们在枚举类中定义的顺序。
内联类
内联类仅在 Kotlin 1.3 之后版本可用,目前还是实验性的。关于详情请参见下文
有时候,业务逻辑需要围绕某种类型创建包装器。然而,由于额外的堆内存分配问题,它会引入运行时的性能开销。此外,如果被包装的类型是原生类型,性能的损失是很糟糕的,因为原生类型通常在运行时就进行了大量优化,然而他们的包装器却没有得到任何特殊的处理。
为了解决这类问题,Kotlin 引入了一种被称为 内联类 的特殊类,它通过在类的前面定义一个 inline 修饰符来声明:
inline class Password(val value: String)
内联类必须含有唯一的一个属性在主构造函数中初始化。在运行时,将使用这个唯一属性来表示内联类的实例(关于运行时的内部表达请参阅下文):
// 不存在 'Password' 类的真实实例对象
// 在运行时,'securePassword' 仅仅包含 'String'
val securePassword = Password("Don't try this in production")
这就是内联类的主要特性,它灵感来源于 “inline” 这个名称:类的数据被 “内联”到该类使用的地方(类似于内联函数中的代码被内联到该函数调用的地方)。
成员
内联类支持普通类中的一些功能。特别是,内联类可以声明属性与函数:
inline class Name(val s: String) {
val length: Int
get() = s.length
fun greet() {
println("Hello, $s")
}
}
fun main() {
val name = Name("Kotlin")
name.greet() // `greet` 方法会作为一个静态方法被调用
println(name.length) // 属性的 get 方法会作为一个静态方法被调用
}
然而,内联类的成员也有一些限制:
内联类不能含有 init 代码块
内联类不能含有幕后字段
因此,内联类只能含有简单的计算属性(不能含有延迟初始化/委托属性)
继承
内联类允许去继承接口
interface Printable {
fun prettyPrint(): String
}
inline class Name(val s: String) : Printable {
override fun prettyPrint(): String = "Let's $s!"
}
fun main() {
val name = Name("Kotlin")
println(name.prettyPrint()) // 仍然会作为一个静态方法被调用
}
禁止内联类参与到类的继承关系结构中。这就意味着内联类不能继承其他的类而且必须是 final。
表示方式
在生成的代码中,Kotlin 编译器为每个内联类保留一个包装器。内联类的实例可以在运行时表示为包装器或者基础类型。这就类似于 Int 可以表示为原生类型 int 或者包装器 Integer。
为了生成性能最优的代码,Kotlin 编译更倾向于使用基础类型而不是包装器。 然而,有时候使用包装器是必要的。一般来说,只要将内联类用作另一种类型,它们就会被装箱。
interface I
inline class Foo(val i: Int) : I
fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}
fun <T> id(x: T): T = x
fun main() {
val f = Foo(42)
asInline(f) // 拆箱操作: 用作 Foo 本身
asGeneric(f) // 装箱操作: 用作泛型类型 T
asInterface(f) // 装箱操作: 用作类型 I
asNullable(f) // 装箱操作: 用作不同于 Foo 的可空类型 Foo?
// 在下面这里例子中,'f' 首先会被装箱(当它作为参数传递给 'id' 函数时)然后又被拆箱(当它从'id'函数中被返回时)
// 最后, 'c' 中就包含了被拆箱后的内部表达(也就是 '42'), 和 'f' 一样
val c = id(f)
}
因为内联类既可以表示为基础类型有可以表示为包装器,引用相等对于内联类而言毫无意义,因此这也是被禁止的。
名字修饰
由于内联类被编译为其基础类型,因此可能会导致各种模糊的错误,例如意想不到的平台签名冲突:
inline class UInt(val x: Int)
// 在 JVM 平台上被表示为'public final void compute(int x)'
fun compute(x: Int) { }
// 同理,在 JVM 平台上也被表示为'public final void compute(int x)'!
fun compute(x: UInt) { }
为了缓解这种问题,一般会通过在函数名后面拼接一些稳定的哈希码来重命名函数。 因此,fun compute(x: UInt) 将会被表示为 public final void compute-(int x),以此来解决冲突的问题。
请注意在 Java 中 - 是一个 无效的 符号,也就是说在 Java 中不能调用使用内联类作为形参的函数。
内联类与类型别名
初看起来,内联类似乎与类型别名非常相似。实际上,两者似乎都引入了一种新的类型,并且都在运行时表示为基础类型。
然而,关键的区别在于类型别名与其基础类型(以及具有相同基础类型的其他类型别名)是 赋值兼容 的,而内联类却不是这样。
换句话说,内联类引入了一个真实的新类型,与类型别名正好相反,类型别名仅仅是为现有的类型取了个新的替代名称(别名):
typealias NameTypeAlias = String
inline class NameInlineClass(val s: String)
fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}
fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""
acceptString(nameAlias) // 正确: 传递别名类型的实参替代函数中基础类型的形参
acceptString(nameInlineClass) // 错误: 不能传递内联类的实参替代函数中基础类型的形参
// And vice versa:
acceptNameTypeAlias(string) // 正确: 传递基础类型的实参替代函数中别名类型的形参
acceptNameInlineClass(string) // 错误: 不能传递基础类型的实参替代函数中内联类类型的形参
}
内联类的实验性状态
内联类的设计目前是实验性的,这就是说此特性是正在 快速变化的,并且不保证其兼容性。在 Kotlin 1.3+ 中使用内联类时,将会得到一个警告,来表明此特性还是实验性的。
要想移除警告,你必须通过对 kotlinc 指定 -XXLanguage:+InlineClasses参数来选择使用该实验性的特性。
在 Gradle 中启用内联类:
compileKotlin {
kotlinOptions.freeCompilerArgs += ["-XXLanguage:+InlineClasses"]
}
关于详细信息,请参见编译器选项。关于多平台项目的设置,请参见使用 Gradle 构建多平台项目章节。
在 Maven 中启用内联类
<configuration>
<args>
<arg>-XXLanguage:+InlineClasses</arg>
</args>
</configuration>
关于详细信息,请参见指定编译器选项。