¶主要内容
- 类和接口
- 有用的属性和构造器
- data class
- 类传递
- 使用
object
关键字
¶Defining class hierarchies
本小节对比Java讨论Kotlin的类层级设计。并介绍新的修改器sealed
。
¶Interfaces in Kotlin
Kotlin的接口类似Java8:包含抽象方法的定义,以及非抽象方法的定义(Java8中接口的default
方法),但不能包含任何状态。
Kotlin中的接口使用interface
关键定义。
1 | interface Clickable { |
方法声明部分为抽象方法,这为click
。继承自接口的所有非抽象类都需要提供接口抽象方法的实现。
1 | class Button: Clickable { |
Kotlin使用冒号(:)后跟接口的方式来代替Java中extends
和implements
关键字的写法形式。Java中,接口可以多重实现(implements),但类仅能extends
一个父类,这是面向对象设计的多重继承的“菱形问题”,可以搜索Scala是如何解决“菱形问题”问题的。
Kotlin中对Java语法重写后,对于很多包含注解的部分,尽量变成了使用关键字代替,毕竟在当代编程语言中,“注解”被诟病太多。
override
修改器,对应于Java中的@Override
注解,用于标注方法和属性重载于父类或接口。和Java不同的是,using the override modifier is mandatory
,所以如果不写override
关键字会导致编译器报错。
接口可以包含默认的实现。和Java8不同的是,接口的默认方法需要带default
关键字,Kotlin不需要任何标注:仅需要提供一个方法体。
1 | interface Clickable { |
如果实现该接口,需要提供对click
的实现即可, 或者重写showOff
方法,或者保留默认不写。
假设现在有另一个接口定义了showOff
方法的实现如下部分:
1 | interface Focusable { |
如果要同时继承这两个接口会怎样?并且它们都包含了default
实现;最终选哪个?最终谁都不选,实际上同时继承这两个接口会得到一个显式的编译错误:
1 | The class 'Button' must |
Kotlin编译器会强制你对这种特定的方法要在具体类内实现,
1 | class Button: Clickable, Fucusable { |
现在,你只需要调用其中一个继承的实现即可,写为:
1 | override fun showOff() = super<Clickable>.showOff() |
下面是具体的main方法实现:
1 | fun main(args: Array<String>) { |
NOTE:相对于Scala的多重继承问题,Scala采用
with
关键字的深度优先的便利方法。也就是说,最后一个接口的方法将被采用。Scala写法如下:
1 class Button() extends Clickable with Fucusable根据深度遍历机制,最终只会调用
Fucusable
特质的showOff
方法。Scala 3 的写法与Kotlin类似。
Implementing interfaces with method bodies in Java
Kotlin 1.0 开始被设计面向Java6,接口不支持
default
方法。因此,它融合了每个接口的default
方法和常规接口方法,以及一个类包含方法体的静态方法。基于此,如果你需要在Java类实现这样一个接口,你必须定义对应于Kotlin方法体内的所有方法实现。
下面看看如何重载基础类内定义成员。
¶Open, final, and abstract modifiers: final by default
如你所认知,Java允许你创建任何类的子类,以及重载任何方法,除非它被显式标记了final
关键字。通常这很实用,也是问题所在。
所谓的 脆弱基类(fragile base class) 问题发生在:一个基类的修改会引起子类不正确的行为,因为基类的代表改变后不在适配子类的设定。如果子类不提供额外的规则(譬如重载方法),子类的方法重载会给基类的实现带来风险。因为编写基类的作者不可能分析所有的子类,基类的“脆弱(fragile)”由此而来。
为了规避这类问题,Java编程的著名书籍 Effective Java 的作者Joshua Boch(Addison-Wesley, 2008),推荐你“design and document for inheritance or else prohibit it.” 大意就是所有类和方法没有指定要从基类进行重载的,都应该显式地标记上final
。
Kotlin遵循同样的哲学(philosophy)。然而Java的类或方法默认是open
的,Kotlin默认则是final
。
如果你希望子类的创建,你需要用open
修改器标记。额外地,你需要给需要被重载的属性或方法添加open
修改器。
1 | open class RichButton: Clickable { // 此类是open的:可以继承 |
需要注意的是,如果你重载了基类或接口的成员,重载的成员默认变为了open
。所以,如果你想要子类的override实现不被修改,需要显式地标记上final
。
1 | open class RichButton: Clickable { |
Open classes and smart casts
类默认为
final
的重大益处在于在大多数情况下可以智能转换。前面的章节描述到只有变量能够智能转换,因为它的类型不会改变。但对于一个类,以为着智能转换仅可被用于一个类属性(property)作为一个val
不包含自定义accessor的情况。这种要求意味着属性必须是final
的,否则子类可以重载它的属性并自定义accessor,打破了智能转换的关键要求。因为属性是默认final
修饰的,你可以对大部分属性进行智能转换。
在Kotlin中,和Java一样可以声明一个类为abstract
的,该类不能被实例化。以及一个抽象类通常包含抽象成员,必须由子类重载。抽象成员总是open的,所以不需要显式地指定open
修改器。
1 | abstract class Animated { // 该类是abstract的:不能创建它的一个实例 |
下表列出了Kotlin的访问 修改器(access modifiers)。接口总是open
的,所以接口不会用到final
、open
或abstract
这些关键字。
Modifier | Corresponding member | Comments |
---|---|---|
final |
Can’t be overridden | Used by default for class members |
open |
Can be overridden | Should be specified explicitly |
abstract |
Must be overridden | Can be used only in abstract classes; abstract members can’t have an implementation |
override |
Overrides a member in a superclass or interface | Overridden member is open by default, if not marked final |
¶Visibility modifiers: public by default
可见性修改器控制了声明的访问。严格控制一个类的实现的可见性,可以确保对其的修改不会打破其它类的依赖风险。
基本上,Kotlin的可见性修改器类似于Java。包含有public
、protected
和private
。但默认的修改器不同:如果省略可见性修改器,Kotlin默认是public
的。
Java的默认可见性是package-private
的,这个在Kotlin中不存在。Kotlin的包的作用仅仅用于组织代码的命名空间;所以不用它做可见性控制。
作为替代,Kotlin提供了一个新的可见性修改器,internal
,表示“visible inside a module”。module 的概念指Kotlin编译文件的集合。它可能是一个IntelliJ IDEA模块,一个Eclipse工程,Maven或Gradle工程,或一系列Ant调用的编译任务。
internal
可见性的优势在于提供了对模块的真实实现的封装。对于Java,模块的封装是很容易被打破的,因为外部的类只需要使用一个同名的包通过继承的方式就可以修改package-private
的声明了。
Kotlin可见性的另外一点不同在于,对于top-level的声明总是private
的,不管是类、函数还是属性。这种声明的可见性作用于它所在的文件。这种方式可以用于隐藏实现细节。下面是可见性修改器列表。
Modifier | Class member | Top-level declaration |
---|---|---|
public(default) |
Visible everywhere | Visible everywhere |
internal |
Visible in a module | Visible in a module |
protected |
Visible in subclasses | |
private |
Visible in a class | Visible in a file |
下面例子中。函数giveSpeech
函数的每行都尝试违反可见性规则。会导致编译错误:
1 | internal open class TalkativeButton: Focusable { |
Kotlin禁止你引用less-visible类型TalkativeButton
。因为internal
是模块可见的,只能在模块内访问,以及内部成员的声明不可见;要解决这类问题,要么将扩展函数(extension function)修改问internal
的,要么去掉internal open
使其是public
的。
值得注意的是,Kotlin的protected
行为和Java有些许差别。在Java中,你可以在同一个包访问protected
声明,但Kotlin不允许这样。在Kotlin中,protected
的声明仅仅在自身或子类可见。所以一个类的扩展函数无法访问private
或protected
的声明成员。
Kotlin’s visibility modifiers and Java
Kotlin中的关键字
public
、protected
和private
是编译为Java字节码的保留字。你可以使用这些Kotlin声明为Java对应的可见性实现。唯一例外的是private
类:在底层它会被编译为package-private
的声明(因为Java没有private
类这种用法)。但对于
internal
修改器呢?Java中并没有与之对应的。包私有可见性是一个完全不同的东西:Kotlin中的模块通常包含好几个包,不同模块又包含同一个包的声明。这样一个internal
实际上对应了字节码的public
。实际上,编译后的
internal
的成员在类中是被损坏的(mangled)。技术层面上,你可以在Java中调用Kotlin的internal
成员,但代码看起来非常丑陋。
另外一个不同之处在于Kotlin的外部类看不到它内部(或者说内嵌)类的成员。
¶Inner and nested classes: nested by default
和Java一样,在Kotlin中你可以在一个类内声明另外一个类。这样做通常用于封装helper型对的类或放置无关紧要的代码在更容易找到的地方。但不同的是Kotlin的内嵌类不能被外部类实例访问,除非你指定要这样做。下面是一个例子。
1 | interface State: Serializable |
相应Java版的实现如下:
1 | public class Button implements View { |
代码的问题在于,你会得到一个java.io.NotSerializableException: Button
。Button不能被序列化:Button
没有序列化,但它对ButtonState
的引用会打破序列化。
为了修复该问题,你需要声明ButtonState
为static
的。但在Kotlin中,这种行为是正向的。
1 | class Button: View { |
由于Kotlin中不再包含有static
和new
这类关键字,不需要显式声明为static
内嵌类。如果需要允许外部类引用它的内嵌类,需要显式使用inner
修改器指定。
下表描述了Java和Kotlin内嵌类的不同之处。
Class A declared within another class B | in Java | in Kotlin |
---|---|---|
Nested class (does’t store a reference to an outer class) | static class A | class A |
inner class (stores a reference to an outer class) | class A | inner class A |
内嵌类(inner class)引用外部类(outer class)的方式也与Java不同。你需要通过this@Outer
来访问Outer
类。
1 | class Outer { |
¶Sealed classes: defining restricted class hierarchies
沿用前面的例子:
1 | interface Expr |
当你使用when
结构,Kotlin编译器强制你检查默认值。
总是要处理默认分支并不算方便。另外,当你添加新的子类时,编译器并不能探测到改变。如果你往家添加新的分支,默认分支会被选择,这可能会导致难以捉摸的bug。
Kotlin提供了一种解决方案:sealed
类。
1 | sealed class Expr { // Mark a base class as sealed... |
当你使用when
表达式处理sealed
类内的所有子类时,你不需要提供默认分支。注意到sealed
修改了暗含了类是open
的;你不需要再显式添加。
sealed
类的行为如下图:
当你在sealed
类中使用when
并添加一个新的子类时,when
表达式会返回一个值错误,表示代码必须更改。
在底层,Expr
类有一个private
构造器,它仅能在类内部调用。你不能声明一个sealed
接口,为什么?如果可以,Kotlin编译器不能保证接口在Java代码的实现。
对于接口的继承使用冒号的形式:
1 | class Num(val value: Int): Expr() |
¶Declaring a class with nontrivial constructors or properties
Ktolin 对于构造器和属性的初始化使用了一种initializer
块的形式。
¶Initializing classes: primary constructor and initializer blocks
一般地,带参数的类声明部分称为 第一构造器(primary constructor)。它有两个作用:指定构造器的参数并初始化定义参数属性。让我们解包看看它的显式代码写的是什么:
1 | class User constructor(_nickname: String) { // 带参数的第一构造器 |
这里的例子有两个Kotlin关键字:constructor
和init
。constructo
关键字开始于第一构造器或第二构造器。init
关键字引入了initializer
块。该块内的代码会在类被创建后执行初始化,并被确定为第一构造器一起使用。因为第一构造器包含约束语法,它自己不包含代码的初始化;如有必要,你可以在同一个类声明几个初始化块。
构造器参数_nickname
的下划线部分是用于区分属性名和构造器参数名。你可以使用this
关键字来规避使用下划线,和Java的写法类似:this.nickname = nickname
。
实际上,构造器constructor
关键字开始可以省略的,
1 | class User(_nickname: String) { // Primary constructor with one parameter |
最直接的方式是使用val
关键字来声明属性定义:
1 | class User(val nickname: String) // "val" means the corresponding proerty is generated for the constructor parameter. |
你可以定义默认的构造参数属性值:
1 | class User(val nickname: String, val isSubscribed: Boolean = true) // Provides a default value for the constructor parameter |
如果子类需要继承,子类需要显式初始化父类构造器参数:
1 | open class User(val nickname: String) {...} |
如果不声明任何构造器,会生成一个默认构造器:
1 | open class Button // The default constructor without arguments is generated. |
带有默认构造器的子类需要显式调用,即使它没有任何构造器参数:
1 | class RadioButton: Button() |
如果你希望子类不要作任何初始化,使用声明为private
的。
1 | class Secretive private constructor() {} // 该类有一个private constructor. |
¶Secondary constructors: initializing the superclass in different ways
通常Kotlin中不会有多个构造器的惯例。相比Java来说构造器的重载在Kotlin中用默认参数代替了。但仍然会有多个构造器的需求。
1 | open class View { |
该类没有声明第一构造器,而是声明了两个 第二构造器(secondary constructor)。第二构造器的引入使用constructor
关键字。
如果需要扩展该类,你需要声明同样的构造器:
1 | class MyButton: View { |
下图是子类构造器的传递性。
和Java一样,你也可以在自己的类中调用其它构造器,使用this()
关键字。
1 | class MyButton: View { |
this()
调用的是本类,super()
调用的是父类。
¶Implementing properties declared in interfaces
在Kotlin中,接口可以包含有抽象户型的定义。
1 | interface User { |
这意味着该接口的子类需要包含有nickname
的属性定义。以及接口只包含定义不包含值,不会存储任何属性值。
下面是实现接口属性,和自定义getter的对比。
1 | class PrivateUser(override val nickname: String): User // primary constructor property |
¶Accessing a backing field from a getter or setter
目前有两类属性:存值属性、访问器属性。下面例子将两种属性组合起来使用,将一个属性存储为值,访问或修改是添加额外计算逻辑。
下面定义一个可变属性并在setter access部分执行额外的代码。
1 | class User(val name: String) { |
你可以通过底层的setter由表达式user.address = "new value"
修改属性。这里的setter被重新定义了,额外的逻辑将被执行。
在setter的body部分,你使用了特定的标识符 field
来访问它的字段。在一个getter中,仅可以读;而在setter中,即可以读也可以写。
注意,你仅能重定义可变属性的一个accessor。示例中的getter是琐碎的、它是个read only字段值,不需要redefine。
默认地,属性的getter和setter部分由编译器生成,不需要显式定义。如果提供自定义的accessor,必须使用field
关键修饰才生效。
¶Changing accessor visibility
accessor的可见性默认等同于property所定义的可见性。但可以修改。只需要在get
或set
关键字前面使用可见性修改器(visibility modifier)修饰即可。
1 | class LengthCounter { |
属性部分的修改器为public
。为了不要其它类修改属性值counter
,将其更改为private
。
1 | >>> val lengthCounter= LengthCounter() |
More about properties later
lateinit
作用于一个non-null property,表示该属性在constructor调用后才被初始化。Lazy initialized properties
@JvmField
注解const
¶Compiler-generated methods: data classes and class delegation
¶Universal object methods
Universal object methods 指的是toString
、equals
和 hashCode
。通常用于debug。下面是重写的示例:
1 | class Client(val name: String, val postalCode: Int) { |
== for equality
在Java中,你可以使用
==
来比较原生类型和引用类型。当作用于原生类型时,Java的==
比较值;当作用于引用类型时则比较引用类型。所以在Java中需要特别关注一下equals
的实现。在kotlin中,
==
为比较对象的默认方式:equals
重写的类的实例;比较引用类型。
¶Data classes: autogenerated implementations of universal methods
如果希望类可以方便地保持数据,你需要override这些方法:toString
、equals
和hashCode
。Kotlin 提供了data
modifier来自动生成这些方法。
1 | data class Client(val name: String, val postalCode: Int) |
该类会重写所有的Java Object类的方法:
equals
用于实例比较hashCode
hash-based 容器的keystoString
字符串表述
equals
和 hashCode
会计算所有在第一构造函数中声明的属性。equals
会检测所有属性的equality;类似地,hashCode
会整合所有属性的hash codes。
DATA CLASSES AND IMMUTABILITY: THE COPY() METHOD
当使用data class
时,推荐声明实例为immutable
的,因为它更多是作为read-only属性的一种存储手段。另外,编译data class
时会额外生成一个copy
方法,用于对其实例的修改操作。因此,data class
实际上等效于如下的写法:
1 | class Client(val name: String, val postalCode: Int) { |
¶Class delegation: using the “by” keyword
传递的Java类要实现Class delegation 有一个著名的设计模式 Decorator 。这种模式的本质就是设计一个新的类,旧的类作为该类的成员变量进行访问,从而达到不需要修改原有类来扩展新的方法实现。最常见的就是代理模式。
这种模式被广泛应用于一些框架如Spring中。这会产生很多样本代码。譬如如下代码实例:
1 | class DelegatingCollection<T> : Collection<T> { |
kotlin 支持first-class 的委派形式,上述代码可以重写为:
1 | class DelegatingCollection<T>(innerList: Collection<T> = ArrayList()) : Collection<T> by innerList |
编译器会帮你自动生成 delegating 的代码,你需要做的,就是对你感兴趣的方法进行override即可。譬如实现自定义的集合:
1 | class CountingSet<T>(private val innerSet: MutableCollection<T> = HashSet()) : |
¶The “object” keyword: declaring a class and creating an instance, combined
Kotlin提供了object
关键字,有不同的使用场景:
Object declaration
,作为单例定义。Companion objects
,伴生对象。包含该类的工厂方法和其它方法。这些方法不需要创建实例也可以被调用。通过类名访问。Object expression
,相当于Java的匿名类, 一般用于第一构造函数的初始化。
¶Object declarations: singetons made easy
单例直接用object
修饰,
1 | object Payroll { |
object的声明可以继承类和接口实现。
1 | object CaseInsensitiveFileComparator : Comparator<File> { |
¶Companion objects: a place for factory methods and static members
伴生对象在类内使用companion object
声明,基本语法如下:
1 | class A { |
伴生对象可以访问该类的所有private
成员,包括private
构造器,是典型的Factory pattern。
下面是半生对象声明工厂方法的一个示例:
1 | class Individual { |
Secondary constructor可以被替换为companion object作为工厂方法实现:
1 | class Individual private constructor(val nickname: String) { |
¶Companion objects as regular objects
半生对象跟类名不同名是将作为常规对象,实际上会被编译为Java的静态对象的一个实例。
1 | interface Person(val name: String) { |
也可以实现接口类型的伴生对象。
1 | interface JSONFactory<T> { |
COMPANION-OBJECT EXTENSIONS
伴生对象也可以实现扩展方法(极度不推荐这样做!!),语法和扩展类、扩展属性一致。
1 | // business logic module |
¶Object expressions: anonymous inner classes rephrased
匿名对象实际上向导那个要Java内匿名类的实例而已。写法如下:
1 | window.addMouseListener( |
这种语法和对象的声明一样,处理省略了对象名。该对象表达式声明一个类被创建该类的一个实例,但不指派类或实例的名字。典型地,它是不需要的,因为仅在方法调用中作为参数。当然,你可以把它提取出来:
1 | val listener = object: MouseAdapter() { |
不同的是,Java的匿名类仅可以继承一个类或接口;Kotlin可以实现继承多个接口或不需要接口。
NOTE:和对象声明不同,匿名对象不是单例的。每次一个对象表达式被执行,相应地创建该实例对象。
因为匿名对象的创建属于表达式,因此可以修改外部对象实例。
1 | fun countClicks(window: Window) { |
¶Summary
- Kotlin的接口和Java类似,但可以包含默认的实现和属性。
- 默认,所有的声明都是
final
的、public
的。 - 要令一个声明non-
final
,标记为open
。 internal
为module可见的。- 内嵌类(Nested classes)默认不是内部类。使用关键字
inner
来存储它的外部类的引用。 sealed
类的子类仅内嵌在它声明的地方(同一个文件)。- initializer blocks 和 secondary constructors 为类实例的创建提供了灵活性。
- 可以使用
field
标识引用accessor 体内的属性字段。 data class
提供了编译的equals
、hashCode
、toString
、copy
和其它方法。- 类的委派避免了类似委派方法的样板代码。
- kotlin中类的声明作为单例。
- companion objects代替了Java的static块中的字段定义。
- companion objects可以实现接口,也可以包含扩展函数和属性。
- kotlin中的对象表达式可以代替Java的匿名内部类,并且可以实现多个接口、修改声明所在地方的变量,因为它属于表达式,每次执行将创建新的实例。