构造函数
主构造函数
在Kotlin中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。
1 | class Person constructor(firstName: String) { /*……*/ } |
如果主构造函数没有任何注解或者可见性修饰符,可以省略constructor关键字:
1 | class Person(firstName: String) { /*……*/ } |
主构造函数不能包含任何的代码。初始化的代码可以放到以init关键字作为前缀的初始化块(initializer blocks)中。在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起。主构造的参数可以在初始化块中使用,也可以在类体内声明的属性初始化器中使用:
1 | class Customer(name: String) { |
事实上,声明属性以及从主构造函数初始化属性,Kotlin有简洁的语法:
1 | class Person(val firstName: String, val lastName: String, var age: Int) { /*……*/ } |
次构造函数
类也可以声明前缀有constructor的次构造函数:
1 | class Person { |
如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数,可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用this关键字即可:
1 | class Person(val name: String) { |
初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。
如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是public。
在JVM上,如果主构造函数的所有的参数都有默认值,编译器会生成一个额外的无参构造函数,它将使用默认值。
继承
继承
在Kotlin中所有类都有一个共同的超类Any,这对于没有超类型声明的类是默认超类。Any有三个方法:equals()、hashCode()与toString()。因此,为所有Kotlin类都定义了这些方法。如果派生类有一个主构造函数,其基类型可以(并且必须)用基类的主构造函数参数初始化:
1 | class Derived(p: Int) : Base(p) |
如果派生类没有主构造函数,那么每个次构造函数必须使用super关键字初始化其基类型,或委托给另一个构造函数做到这一点。在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
1 | class MyView : View { |
覆盖方法/属性
Kotlin对于可覆盖的成员(open)以及覆盖后的成员需要显式修饰符:
1 | open class Shape { |
标记为override的成员本身是开放的,也就是说,它可以在子类中覆盖。可以使用final关键字禁止再次覆盖。
属性覆盖与方法覆盖类似:在超类中声明然后在派生类中重新声明的属性必须以override开头,并且它们必须具有兼容的类型。每个声明的属性可以由具有初始化器的属性或者具有get方法的属性覆盖。可以用一个var属性覆盖一个val属性,但反之则不行,因为一个val属性本质上声明了一个get方法,而将其覆盖为var只是在子类中额外声明一个set方法。
可以在主构造函数中使用override关键字作为属性声明的一部分。
调用超类实现
派生类中的代码可以使用super关键字调用其超类的函数与属性访问器的实现:
1 | open class Rectangle { |
在一个内部类中访问外部类的超类,可以通过由外部类名限定的super关键字来实现:super@Outer:
1 | class FilledRectangle: Rectangle() { |
覆盖规则
在Kotlin中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现,它必须覆盖这个成员并提供其自己的实现。为了表示采用从哪个超类型继承的实现,应该使用由尖括号中超类型名限定的super,如super<Base>
:
1 | open class Rectangle { |
抽象类
类以及其中的某些成员可以声明为abstract,抽象成员在本类中可以不用实现,且不需要用open标注一个抽象类或者函数。可以用一个抽象成员覆盖一个非抽象的开放成员:
1 | open class Polygon { |
属性
Getters与Setters
声明一个属性的完整语法是:
1 | var <propertyName>[: <PropertyType>] [= <property_initializer>] |
- 读一个属性的实质就是执行了属性的读访问器getter;
- 写一个属性的实质就是执行了属性的写访问器setter;
- getter访问器的可见性修饰需要与属性的可见性一致。
一个只读属性的语法和一个可变的属性的语法有两方面的不同:
- 只读属性的用val开始代替var;
- 只读属性不允许setter。
如果定义了一个自定义的getter,那么每次访问该属性时都会调用它;如果定义了一个自定义的setter,那么每次给属性赋值时都会调用它:
1 | var stringRepresentation: String |
如果需要改变一个访问器的可见性或者对其注解,但是不需要改变默认的实现,你可以定义访问器而不定义其实现:
1 | var setterVisibility: String = "abc" |
幕后字段
如下代码会崩溃:
1 | class Person { |
将Person类转为Java类:
1 | public final class Person { |
由此引出了Kotlin的幕后字段。在Kotlin中,如果属性至少一个访问器使用默认实现,那么Kotlin会自动提供幕后字段,用关键字field表示,幕后字段主要用于自定义getter和setter中,并且只能在getter和setter中访问。因此上述赋值应该改成如下:
1 | class Person { |
满足下面条件之一的属性拥有幕后字段:
- 使用默认getter/setter的属性,一定有幕后字段。对于var属性来说,只要getter/setter中有一个使用默认实现,就会生成幕后字段;
- 在自定义getter/setter中使用了field的属性。
没有幕后字段的例子:
1 | class NoField { |
幕后属性
幕后属性:对外表现为只读,对内表现为可读可写。
1 | private var _table: Map<String, Int>? = null |
将_table属性声明为private,因此外部是不能访问的,内部可以访问,外部访问通过table属性,而table属性的值取决于_table,这里_table就是幕后属性。
Collection中有个size字段,size对外是只读的,size的值的改变根据集合的元素的变换而改变,这是在集合内部进行的,这用幕后属性来实现非常方便。
常量
val的值并不是不可能变化的,如下:
1 | val currentTimeMillis: Long |
我们每次访问currentTimeMillis得到的值是变化的,因而val不是常量。想要实现真正的常量方法有两种,一种是const,另一个使用@JvmField注解。
已知值的属性可以使用const修饰符标记为编译期常量。这些属性需要满足以下要求:
- 位于顶层(不被任何类/接口等包含)或者是object声明或companion object的一个成员;
- 以String或原生类型值初始化;
- 没有自定义getter。
@JvmField注解:
- 在val常量前面增加一个@JvmField就可以将它变成常量;
- 其内部作用是抑制编译器生成相应的getter方法;
- 是用该注解修饰后则无法重写val的get方法。
1 | val NAME = "89757 |
lateinit
一般地,属性声明为非空类型必须在构造函数中初始化,可以用lateinit修饰符标记该属性使其可以延迟初始化,在初始化前访问一个lateinit属性会抛出异常:
1 | public class MyTest { |
接口
定义接口
Kotlin的接口可以既包含抽象方法的声明也包含实现,与抽象类不同的是,接口无法保存状态,它可以有属性但必须声明为抽象或提供访问器实现。使用关键字interface来定义接口:
1 | interface MyInterface { |
接口属性
可以在接口中定义属性,在接口中声明的属性要么是抽象的,要么提供访问器的实现。在接口中声明的属性不能有幕后字段,因此接口中声明的访问器不能引用它们。
1 | interface MyInterface { |
接口继承
一个接口可以从其他接口派生,实现这样接口的类只需定义所缺少的实现:
1 | interface Named { |
覆盖冲突
实现多个接口时,可能会遇到同一方法继承多个实现的问题:
1 | interface A { |
可见性修饰符
Kotlin中有四个可见性修饰符:private、protected、internal和public,如果没有显式指定修饰符的话,默认可见性是public。
对于类内部声明的成员:
- private:只在这个类内部(包含其所有成员)可见;
- protected:在子类中可见,覆盖一个protected成员并且没有显式指定其可见性,该成员还会是protected可见性;
- internal:能见到类声明的本模块内的任何客户端都可见其internal成员;
- public:能见到类声明的任何客户端都可见其public成员。
可见性修饰符internal意味着该成员只在相同模块内可见。更具体地说,一个模块是编译在一起的一套Kotlin文件:
- 一个 IntelliJ IDEA 模块;
- 一个 Maven 项目;
- 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明);
- 一次
<kotlinc> Ant
任务执行所编译的一套文件。
嵌套类
类可以嵌套在其他类中:
1 | class Outer { |
内部类
Kotlin 的内部类默认为静态内部类,添加 inner 标记后变为非静态内部类,能够访问外部类的成员,内部类会带有一个对外部类的对象的引用:
1 | class Outer { |
匿名内部类
使用对象表达式创建匿名内部类实例:
1 | window.addMouseListener(object : MouseAdapter() { |
对于JVM平台,如果对象是函数式Java接口(即具有单个抽象方法的Java接口)的实例,可以使用带接口类型前缀的lambda表达式创建它:
1 | val listener = ActionListener { println("clicked") } |
枚举类
枚举类的最基本的用法是实现类型安全的枚举:
1 | enum class Direction { |
每个枚举常量都是一个对象,所以可以这样初始化:
1 | enum class Color(val rgb: Int) { |
枚举常量还可以声明其带有相应方法以及覆盖了基类方法的匿名类。
1 | enum class ProtocolState { |
一个枚举类可以实现接口(但不能从类继承),可以为所有条目提供统一的接口成员实现,也可以在相应匿名类中为每个条目提供各自的实现。只需将接口添加到枚举类声明中即可,如下所示:
1 | enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator { |
Kotlin中的枚举类也有合成方法允许列出定义的枚举常量以及通过名称获取枚举常量,这些方法的签名如下(假设枚举类的名称是 EnumClass):
1 | EnumClass.valueOf(value: String): EnumClass |
如果指定的名称与类中定义的任何枚举常量均不匹配,valueOf()方法将抛出IllegalArgumentException异常。
可以使用 enumValues<T>()
与 enumValueOf<T>()
函数以泛型的方式访问枚举类中的常量 :
1 | enum class RGB { RED, GREEN, BLUE } |
每个枚举常量都具有在枚举类声明中获取其名称与位置的属性,枚举常量还实现了Comparable接口,其中自然顺序是它们在枚举类中定义的顺序。
数据data类
1 | data class User(val name: String, val age: Int) |
在上面的数据类中,它只用来保存数据,编译器自动从主构造函数中声明的所有属性导出以下成员:
equals()/hashCode()
toString()
格式是"User(name=John, age=42)"
componentN()
函数按声明顺序对应于所有属性copy()
函数
如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值。
密封sealed类
密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。
一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员。密封类不允许有非-private 构造函数(其构造函数默认为 private)。扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。
1 | sealed class Result |
网络请求的结果一般只有两种类型,即 Success 或 Exception, 此时使用密封类能够很好地定义这种场景。
1 | fun response(result: Result): String{ |
使用密封类的话时 when 表达式可以覆盖所有情况,不需要再添加 else 语句。
对象表达式
要创建一个继承自某个(或某些)类型的匿名类的对象,我们会这么写:
1 | window.addMouseListener(object : MouseAdapter() { |
如果超类型有一个构造函数,则必须传递适当的构造函数参数给它,多个超类型可以由跟在冒号后面的逗号分隔的列表指定:
1 | open class A(x: Int) { |
如果我们只需要一个对象而已,并不需要特殊超类型,那么我们可以简单地写:
1 | fun foo() { |
匿名对象可以用作只在本地和私有作用域中声明的类型,如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是Any,在匿名对象中添加的成员将无法访问。
1 | class C { |
对象声明
单例模式在一些场景中很有用,而Kotlin使单例声明变得很容易:
1 | object DataProviderManager { |
这称为对象声明,就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。对象声明的初始化过程是线程安全的。如需引用该对象,我们直接使用其名称即可,这些对象可以有超类型。
注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。
伴生对象
类内部的对象声明可以用companion关键字标记,称为伴生对象,该伴生对象的成员可通过只使用类名作为限定符来调用:
1 | class MyClass { |
可以省略伴生对象的名称,在这种情况下将使用名称Companion:
1 | class MyClass { |
其自身所用的类的名称可用作对该类的伴生对象(无论是否命名)的引用:
1 | class MyClass1 { |
请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且还可以实现接口:
1 | interface Factory<T> { |
在JVM平台,使用@JvmStatic
注解可以将伴生对象的成员生成为真正的静态方法和字段。
类型别名
类型别名不会引入新类型,它们等效于相应的底层类型。
1 | // 缩减集合类型 |
init/constructor/companion执行顺序
看一段代码:
1 | class Main() { |
伴生对象中的代码在类加载时就会执行,即代码中出现 Main 时便会顺序执行伴生对象代码,当执行到 instance 赋值时会实例化 Main 类。将上面 instance 改成懒加载:
1 | // ... |
当代码中出现 Main 时便会顺序执行伴生对象代码,由于 instance 是懒加载,即调用它时才会初始化 Main。
非懒加载时查看反编译成 Java 后的代码:
1 | public final class Main { |
可以看到 init 代码块的代码会顺序插入主构造函数中。