内容:
- 类与继承
- 属性与字段
- 接口
一、类
使用关键字 class
声明类。
类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号。
1 | //类名:Person; |
1、构造函数:
主构造函数:
constructor
可省略,但村长注解(如@Inject
)或可见性修饰符(如public
)时,不可省略;只可有一个。不能包含任何代码1
class Customer public @Inject constructor(name: String) { /*……*/ }
主构造函数中属性声明:可变的(
var
)或只读的(val
)皆可1
2//简洁语法,参数初始化
class Person(val firstName: String, val lastName: String, var age: Int) { /*……*/ }初始化块:
init
关键字,初始化的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println) //①
init {
println("First initializer block that prints ${name}") //②
}
val secondProperty = "Second property: ${name.length}".also(::println) //③
init {
println("Second initializer block that prints ${name.length}") //④
}
}
fun main() {
InitOrderDemo("hello")
}
在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起。执行顺序为①②③④
- 次构造函数:可声明多个
1
2
3
4
5
6
7
8
9
10
11class Person(val name: String) {
var children: MutableList<Person> = mutableListOf<Person>();
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
println("Constructor block") //②
}
init {
println("Init block") //①
}
}
委托到同一个类的另一个构造函数用 this 关键字。
初始化块中的代码实际上会成为主构造函数的一部分。 委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块,执行顺序为①②
非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public
。可声明为私有构造函数:
1 | class DontCreateMe private constructor () { /*……*/ } |
注:在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成 一个额外的无参构造函数,它将使用默认值。这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。
1 | class Customer(val customerName: String = "") |
类的实例:如函数一样直接调用,无需
new
关键字类成员:
构造函数与初始化块
函数
属性
嵌套类与内部类
对象声明
2、继承
2.1、超类Any
在 Kotlin 中所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类:
1
class Example // 从 Any 隐式继承
如需声明一个显式的超类型,请在类头中把超类型放到冒号之后:
1
2open class Base(p: Int)
class Derived(p: Int) : Base(p)构造函数继承:
- 如果派生类有一个主构造函数,其基类型可以(并且必须) 用基类的主构造函数参数就地初始化。
- 如果派生类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
1
2
3
4class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
2.2、覆盖成员
覆盖方法:
- 可覆盖的成员用
open
修改(称之为开放),不写则子类不允许定义同名函数。final
类不可用open
- 覆盖后的成员用
override
,不写则报错 - 禁止覆盖用
final
1
2
3
4
5
6
7
8
9
10
11
12open class Shape {
open fun draw() { /*……*/ }
fun fill() { /*……*/ }
}
class Circle() : Shape() {
override fun draw() { /*……*/ }
}
open class Rectangle() : Shape() {
final override fun draw() { /*……*/ }
}
- 可覆盖的成员用
覆盖属性:
- 属性覆盖与方法覆盖类似
- 在超类中声明然后在派生类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。
- 可以用一个
var
属性覆盖一个val
属性,但反之则不行。
因为一个val
属性本质上声明了一个get
方法, 而将其覆盖为var
只是在子类中额外声明一个set
方法。(val
修饰的没有set
方法,var
有)1
2
3
4
5
6
7
8
9interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // 总是有 4 个顶点
class Polygon : Shape {
override var vertexCount: Int = 0 // 以后可以设置为任何数
}
2.3、派生类初始化顺序
1 | open class Base(val name: String) { |
1 | Constructing Derived("hello", "world") |
Tip: 设计一个基类时,应该避免在构造函数、属性初始化器以及 init 块中使用 open 成员。
2.4、调用超类实现
super
关键字调用其超类的函数与属性1
2
3
4
5
6
7
8
9
10
11
12
13open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}
val fillColor: String get() = super.borderColor
}内部类中访问外部类的超类,可以通过由外部类名限定的 super 关键字来实现:
super@Outer
:1
2
3
4
5
6
7
8
9
10
11
12
13class FilledRectangle: Rectangle() {
fun draw() { /* …… */ }
val borderColor: String get() = "black"
inner class Filler {
fun fill() { /* …… */ }
fun drawAndFill() {
super@FilledRectangle.draw() // 调用 Rectangle 的 draw() 实现
fill()
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 所实现的 borderColor 的 get()
}
}
}
2.5、覆盖规则
1 | open class Rectangle { |
3、抽象类
- 关键词:
abstract
- 无需
open
来标注一个抽象类或函数 - 抽象成员覆盖一个非抽象的开放成员
1 | open class Polygon { |
二、属性与字段
1、属性
- 声明:
var
(可变) 或val
(不可变,只读) - 引用:名称引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
}
fun copyAddress(address: Address): Address {
val result = Address() // Kotlin 中没有“new”关键字
result.name = address.name // 将调用访问器
result.street = address.street
// ……
return result
}
2、Getters 与 Setters
声明属性完整语法:
1
2
3var <propertyName>[: <PropertyType>] [= <property_initializer>]
Tip: 其初始器(initializer)、getter 和 setter 都是可选的
自定义的访问器
1
2
3
4
5
6
7
8
9
10//Boolean可省略:
//val isEmpty get() = this.size == 0 // 具有类型 Boolean
val isEmpty: Boolean //无幕后字段
get() = this.size == 0
var stringRepresentation: String
get() = this.toString()
set(value) { //value可以任意名称
setDataFromString(value) // 解析字符串并赋值给其他属性
}如果你需要改变一个访问器的可见性或者对其注解,但是不需要改变默认的实现, 你可以定义访问器而不定义其实现:
1
2
3
4
5var setterVisibility: String = "abc"
private set // 此 setter 是私有的并且有默认实现
var setterWithAnnotation: Any? = null
set // 用 Inject 注解此 setter幕后字段
- 在 Kotlin 类中不能直接声明字段。
field
标识符在访问器中引用幕后字段1
2
3
4var counter = 0 // 注意:这个初始器直接为幕后字段赋值
set(value) {
if (value >= 0) field = value
}
幕后属性
1
2
3
4
5
6
7
8private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // 类型参数已推断出
}
return _table ?: throw AssertionError("Set to null by another thread")
}
3、编译期常量
- 修饰符:
const
,修饰已知值的属性,为编译期常量 - 要求:
- 位于顶层或者是 object 声明 或 companion object 的一个成员
- 以
String
或原生类型值初始化 - 没有自定义 getter
4、延迟初始化属性与变量
属性声明为非空类型必须在构造函数中初始化。属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检测。
- 修饰符:
lateinit
只能用于在类体中的属性
不是在主构造函数中声明的 var 属性1
2
3
4
5
6
7
8
9
10
11public class MyTest {
lateinit var subject: TestSubject
fun setup() {
subject = TestSubject()
}
fun test() {
subject.method() // 直接解引用
}
}
5、委托属性
通过使用委托属性实现为库
三、接口 interface
1、定义
- 接口可以既包含抽象方法的声明也包含实现。
- 与抽象类不同的是,接口无法保存状态。
- 接口可以有属性但必须声明为抽象或提供访问器实现。
- 一个类或者对象可以实现一个或多个接口。
1
2
3
4
5
6
7
8
9
10
11
12
13interface MyInterface {
fun bar()
fun foo() {
// 可选的方法体
}
}
//实现
class Child : MyInterface {
override fun bar() {
// 方法体
}
}
2、属性
- 接口中声明的属性可以是抽象的
- 或接口中声明的属性提供访问器的实现。
在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们。1
2
3
4
5
6
7
8
9
10
11
12
13
14interface MyInterface {
val prop: Int // 抽象的
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
3、接口继承
一个接口可以从其他接口派生,从而既提供基类型成员的实现也声明新的函数与属性。
1 | interface Named { |
4、覆盖冲突
实现多个接口时,可能会遇到同一方法继承多个实现的问题。这时,就需要指明如何去实现的多个接口中的相同函数。
1 | interface A { |
上例中,接口 A 和 B 都定义了方法 foo() 和 bar()。 两者都实现了 foo(), 但是只有 B 实现了 bar() (bar() 在 A 中没有标记为抽象, 因为没有方法体时默认为抽象)。因为 C 是一个实现了 A 的具体类,所以必须要重写 bar() 并实现这个抽象方法。
然而,如果我们从 A 和 B 派生 D,我们需要实现我们从多个接口继承的所有方法,并指明 D 应该如何实现它们。这一规则既适用于继承单个实现(bar())的方法也适用于继承多个实现(foo())的方法。
四、可见性修饰符
1、修饰符:
- public:声明随处可见,默认可见性
- private:私有,当前作用域可见
- internal:模块可见
- protected:私有+子类可见
类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。
2、修饰符应用
2.1、包 (package
) 内
1 | // 文件名:example.kt |
注:要使用另一包中可见的顶层声明,仍需将其导入进来。
2.2、类和接口
1 | open class Outer { |
2.3、构造函数
指定类的主构造函数的可见性,需要添加一个显式 constructor
关键字
1 | class C private constructor(a: Int) { …… } |
这里的构造函数是私有的。默认情况下,所有构造函数都是 public,这实际上等于类可见的地方它就可见(即 一个 internal 类的构造函数只能在相同模块内可见).
2.4、局部声明
局部变量、函数和类不能有可见性修饰符。(???)
2.5、模块
可见性修饰符 internal 意味着该成员只在相同模块内可见。更具体地说, 一个模块是编译在一起的一套 Kotlin 文件:
- 一个 IntelliJ IDEA 模块;
- 一个 Maven 项目;
- 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明);
- 一次
<kotlinc>
Ant 任务执行所编译的一套文件。