第四章主要讲了,类和接口,非默认属性的和构造方法。数据类。类委托和使用 object
关键字。
定义类继承接口
Kotlin 中的接口
普通的接口定义和 Java 一样,与 Java 不一样的是,override
成了一个关键字,强制要求加上,否则将无法编译。
1 |
|
输出:
I was clicked
不同与 Java ,Kotlin 可以在接口中定义一个带方法体的方法。
1 | interface Clickable { |
输出:
I’m clickable
定义另一个实现同样方法的接口,同时让类实现这两个接口。
1 | interface Focusable { |
会出现编译不过,因为编译器强制要求你提供你自己的实现。但同时也可以通过 super<>
去指定调用哪个父类。
例如:
1 | class Button : Clickable, Focusable { |
原理,其实 Kotlin 是以 Java 6 设计的。其并不支持接口中的默认方法。因此它会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类结合体。当子类没有实现的时候,其实 Kotlin 在编译的时候帮你实现了,里面调用的就是静态的类的静态方法。可以参考 :面向对象:抽象类和接口
open、final 和 abstract 修饰符:默认为 final
在 《Effective Java》中建议“要么为继承做好设计并记录文档,要么禁止这么做。” 所以 Kotlin
中采用了同样的哲学思想,类和方法默认都是 final
的。
代码如下:
1 | open class RichButton : Clickable { //这个类是 open 的,其他类可以继承它 |
open 类和智能转换,类默认为 final 带来的一个重要好处就是这使得在大量场景中的智能转化成为可能。
抽象类,基本上和 Java 差不多。代码如下:
1 | abstract class Animated { //抽象类,不能创建实例 |
可见性修饰符:默认为 public
总的来说 Kotlin 和 Java 的修饰符类似,同样可以使用 public
、protected
和 private
修饰符。不同的一点是默认的可见性不一样。Kotlin 如果省略掉修饰符,那么声明的就是public
的。
Java 默认的可见性 – 包私有。在 Kotlin 中并没有使用。Kotlin 只把包作为在命名空间里组织代码的一种方式使用,并没有将其作用可见性控制。
作为替代方案,Kotlin 提供了一个新的修饰符。internal
,表示只在模块内部可见
例子如下:
1 |
|
一个通用的规则: 类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性。所以上述代码,有两种解决方案,一是改变基础类为 public
或把函数改成与之对应的internal
修饰符(因为默认是 public
)
和 Java 还有一个区别是 protected
成员只在和类和它的子类可见。而 Java 是在同一个包内,都能访问一个 protected
成员。
Kotlin 可见性修饰符表:
修饰符 | 类成员 | 顶层声明 |
---|---|---|
public(默认) | 所有地方可见 | 所有地方可见 |
internal | 模块中可见 | 模块中可见 |
protected | 子类中可见 | – |
private | 类中可见 | 文件中可见 |
内部类和嵌套类 : 默认是嵌套类
和 Java 一样,都是能在类中嵌套内部类。而区别是 Kotlin 的嵌套类不能访问外部类的实例,除非做出了特别的要求。
1 | // Kotlin |
上述的内部类 ButtonState
,在序列会出现异常。因为内部类隐式的持有宿主类的引用。而宿主类并没有现实序列化接口,所以会出现异常。所以修复这个问题,需要将内部类 ButtonState
声明为 static
在 Kotlin 中,默认行为和 Java 刚好相反。
1 | class Button : View { |
如果要把变成一个内部类持有外部类的引用的话,需要使用inner
修饰符。同时引用实例的语法也和 Java 不同。需要使用 this@Other
从 Inner
去访问 Outher
类。
1 | class Outer { |
密封类: 定义受限的类继承结构
下面例子中,接口的 Expr
的两个子类分别表示数字的Num
,以及表示两个表达式之和的 Sun
用when
表达式处理所有可能固然方便,但是必须提供一个分支来处理没有任何其他分支匹配的情况。
1 |
|
上述的代码,编译器会强制检查默认选项。在这个例子中,不能返回一个有意义的值。就会直接抛出异常。当我们扩展Expr
的子类的时候。忘了改上述代码。就会出现异常报错。编译器不能的检查的出这个潜在的 BUG。
Kotlin 为了这个问题提供了解决方案: sealed
类。为父类添加一个 sealed
修饰符。对可能创建的子类做出严格的显示。所有的直接子类都必须嵌套在父类中。
代码如下:
1 |
|
这样,在添加多一个嵌套类的时候,when 这里就会编译报错。需要你实现分支。同时注意,sealed
修饰符隐含着这个类是一个 open
类。不再需要显示的添加。
声明一个带非默认构造方法或者属性的类
在 Java 中,一个类可以声明一个或多个构造方法。Kotlin 也是类是的。只是做了点修改,区分了主构造方法(通常是主要而简洁的初始化类的方法)和从构造方法(在类内部声明)。
初始化类:主构造方法和初始化语句块
声明一个简单的类
1 | class User(val nickname: String) |
括号围起来的语句块就叫做主构造方法。它表明了构造函数的参数。以及定义参数的初始化的属性。
这是一种简写。明确的代码如下:
1 | class User constructor(_nickname: String) { //带一个参数的主构造函数 |
上述代码有这么两个关键字。
constructor
关键字用来开始一个主构造方法或从构造方法的声明。init
关键字着用来引入一个初始化语句块。包含了在类被创建时执行的代码,并会与主构造方法一起使用。
如果主构造方法没有注解或可见性修饰符,同样也可以去掉 constructor
关键字。
_nickname
用来消除歧义。同时也可以用this
来消除歧义。
如果类具有父类。主构造方法同样需要初始化父类。可以通过在基类列表的父类引用中提供父类构造方法的参数的方式来做到这一点。
1 | class TwitterUser(_nickname: String) : User(_nickname) |
如果确保类不被其他代码实例化。必须把构造方法标记为 private
1 | class Secretive private constructor() //这个类有一个 private |
构造方法:用不同的方式来初始化父类
例如 Android 的 View 。多个构造函数来创建 View
代码如下:
1 | open class View { |
如果想要扩展这个类,可以声明同样的构造方法。
1 | class MyButton : View { |
和 Java 一样。也可以使用 this
关键字。从一个构造方法中调用你自己类的另一个构造方法。如下:
1 | class MyButton : View { |
实现在接口中声明的属性
在 Kotlin 接口可以包含抽象属性的声明。如下:
1 | interface User { |
可以以不同的方式去实现接口属性。
1 | class Privateuser(override val nickname: String) : User //主构造方法属性 |
除了抽象属性声明外。接口还可以包含具有 getter
和 setter
的属性。
1 |
|
通过 getter 或 setter 访问支持字段
结合存储的属性和具有自定义访问器在每次访问时计算值的属性。需要支持这个情况,需要能够从属性的访问器中访问它的支持字段。
例子如下:
1 | class User(val name: String) { |
输出
Address was changed for Alice:
“unspecified” -> “Elsenheimerstrasse 47,80687 Muenchen”.
在 setter 的函数中,使用了特殊的标识符 field
来访问支持字段的值。
修改访问器的可见性。
访问器的可见性默认与属性的可见性相同。但可以通过修饰符方式来修改它。
代码如下:
1 | class LengthCounter { |
输出:
3
编译器生成的方法:数据类和类委托
Java 平台定义了一些需要在许多类中呈现的方法,并通过是以一种机械的方式实现。如equals、hashCode及 toString
通用对象方法
先简单的创建一个客户类
1 | class Client(val name: String, val postalCode: Int) |
字符串表示:toString
重写 toString
方法。
1 | class Client(val name: String, val postalCode: Int) { |
输出:
Client(name=xiaowu,postalCode111)
对象相等性:equals
重写 equals
方法。
1 | override fun equals(other: Any?): Boolean { //"Any" 是 java.lang.Object 的模拟。Kotlin 中所有类的父类 |
输出:
true
true
Hash 容器: hashCode
hashCode 通常和 equals 一起重写。假设创建了有一个元素的set
。如果传入相同数据的Clint
期望检查是true
但是实际上返回的是 false
1 |
|
输出:
false
添加上 hashCode 的实现
1 | class Client(val name: String, val postalCode: Int) { |
这时再调用上述processed.contains
方法。返回的就是 true
了。
数据类: 自动生成通用方法的实现
如果想要的类是一个方便的数据容器,你需要重新写toString,equals,hashCode
方法。虽然说 IDE 能帮助生成这些代码,但是也是重复的工作量,Kotlin 添加了data
修饰符。通过这个修饰符。编译的时候就自动帮你生成那些需要重写的方法。
代码如下:
1 | data class Client(val name: String, val postalCode: Int) |
再调用toString,equals,hashCode
这些方法。效果和自己重写的一模一样。
数据类和不可变性:copy() 方法
虽然数据类,没有强制用val
,同样也可以用var
但是强烈建议用val
。因为用var
的话。当实例被加入到 HashMap 或类似容器的键。作为键的对象加入容器后被修改。这时候容器可能会进入一种无效的状态。
为了使用不可变对象的数据类变得更容易,Kotlin 编译器为它们生成了一个方法;一个允许 copy 类的实例方法,并在 copy 的同时修改某些属性值。创建副本通常是修改实例的好选择:副本有着单独的生命周期而且不会影响代码中引用原始的位置。
手动实现 copy 方法后看起来的样子。
1 | class Client(val name: String, val postalCode: Int) { |
输出
Client(name=Bob, postalCode=222)
类委托类:使用”by”关键字
当我们需要向一些类添加一些行为,即便它并没有被设计为可扩展的。一个常用的方式以装饰器模式。这个模式的本质是创建一个新类,实现与原始类一样的接口。并将原来的类实例作为一个字段保存。与原始类拥有同样行为的方法不用被修改,只需要转发到原始类的实例。
这个方式的一个缺点就是需要相当多的模板代码。
1 |
|
kotlin 将委托作为一个语言级别的功能做了头等支持。无论什么时候实现一个接口,你都可以使用 by
关键字将接口的实现委托到另一个对象。
下面用这种方法重写上述代码
1 |
|
类中所有方法实现都消失了,编译器会生成它们。因为代码中没有太多有意思的内容,所以当编译器能够自动为你做同样的事情时候,没必要手写这些代码。
可以部分不使用委托,提供一个不同的实现。
1 | class CountingSet<T>( |
输出
3 objects were added,2 remain
object
关键字:将声明一个类与创建一个实例结合起来。
核心理念。这个关键字定义一个类并同事创建一个实例(对象)。下面是一些使用场景
- 对象声明是定义单例的一种方式。
- 伴生对象可以持有工厂方法和其他与这个类相关,但在调用时并不依赖类实例对象。他们成员可以通过类名来访问。
- 对象表达式用替代 Java 的匿名内部类。
对象声明:创建单例易如反掌
在 Java 常用单例模式,定义一个使用 private 构造方法并用静态字段来持有这个类仅有的实例。而 Kotlin 通过使用对象声明功能来实现这个。
例如一个忽略大小写的文件路径比较器。
1 | object CaseInsensititiveFileComparator : Comparator<File> { |
输出:
0
[/a, /Z]
同样可以在类中声明对象。这样的对象同样只有一个单例实例:它在每个容器类的实例并不具有不同的实例。
例如嵌套类实现 Comparator
1 | data class Person(val name: String) { |
输出:
[Person(name=Alice), Person(name=Bob)]
Kotlin 中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都 INSTANCE
代码如下
1 | /* Java */ |
伴生对象:工厂方法和静态成员的地盘
一般来说,可以用顶层函数来做为静态工具方法。但是它并不能访问到类的 private
成员。因此需要在没有类实例的情况下访问类内部的函数。可以将其写成那个类中的对象声明成员。这种函数的一个例子就是工厂方法。
在类中定义的对象之一可以使用一个特殊关键字来标记 companion
代码如下:
1 | class A { |
输出
companion object call
定义一个拥有多个构造方法的类
1 | class User { |
使用工厂方法来替代从构造方法
1 |
|
输出:
bob
作为普通对象使用伴生对象
声明一个命名伴生对象。例如命名为 Loader
代码如下:
1 | class Person(val name: String) { |
这样就可以通过 Loader.
也可以直接通过方法名。
在伴生对象实现接口
伴生对象也能实现接口。代码如下:
1 | interface JSONFactory<T> { |
Kotlin 的伴生对象和静态成员。
类的伴生对象会同样被编译成常规对象:类中的一个引用了它的实例的静态字段。如果这个伴生对象没有给命名,在 Java 中它可以通过 Companion
引用来访问。
1 | Person.Companion.fromJSON("..."); |
伴生对象扩展
1 |
|
对象表达式:改变写法的匿名内部类
object 不仅仅用来声明单例的对象。还能用来声明匿名对象。
用匿名对象来实现事件监听
1 | window.addMouseListener(object : MouseAdapter() { |
如果需要给对象分配一个名字。可以将其存储到一个变量中
1 | val listener = object : MouseAdapter() { |
和 Java 匿名内部类只能扩展一个类或实现一个接口不同。Kotlin 的匿名对象可以实现多个接口或者不实现接口。
与 Java 匿名类一样。在对象表达式中的代码可以访问创建它的函数中的变量。但与 Java 不同。访问并没有被限制在 final 变量,还可以在对象表达式中国年修改变量值。
如下:
1 | fun countClick(window: Window) { |
总结
- Kotlin 的接口与 Java 的相识,但可以包含默认实现。
- 所有的声明默认都是 final 和 public 的。
- 要想使得声明不是 final 的,标记为 open
- internal 声明在同一模块可见
- 嵌套类默认不是内部类,使用 inner 关键字来存储外部类的引用
- sealed 类(密封类)的子类只能嵌套在自身的声明中(1.1 允许将子类放置在同一文件的任意地方)
- 初始化语句块(init)和从构造方法(constructor)为初始化类实例提供了灵活性
- 使用 field 标识符在访问器方法体中引用属性的支持字段
- 数据类提供了编译器生成的 equals、hashCode、toString、copy和其他方法
- 类委托帮助避免在代码中出现许多相似的委托方法
- 对象声明(object)是 Kotlin 中定义单例类的方法。
- 伴生对象 companion(与包级别函数和属性一起)替代了 Java 静态方法和字段的定义
- 伴生对象和其他对象一样,可以实现接口,也可以拥有扩展函数和属性
- 对象表达式是 Kotlin 中针对 Java 匿名内部类的替代品,并增加了诸如实现多个接口的能力和修改在创建对象的作用域中定义的变量的能力等功能