继承
在 Kotlin 中所有类都有一个共同的超类Any
,这对于没有超类型声明的类是默认超类,Any与java中的Object不同,它只有equals()
、hashCode()
和toString()
三个方法。
要声明一个显式的超类型,我们把类型放到类头的冒号之后:
1 | open class Base(p: Int) |
基类在声明时必须要有open修饰,表示该类可以被继承。同时如果类有主构造函数,那么其基类必须用主构造函数参数实例化,如果没有主构造函数,那么每个次构造函数必须用super关键字实例化基类。如:
1 | class MyView : View { |
覆盖方法和属性
Kotlin 需要显式 标注可覆盖的成员(我们称之为开放)和覆盖后的成员:
1 | open class Base { |
基类可以覆盖的方法属性必须有open
,子类覆盖的方法属性必须有override
。同时你也可以用一个var
属性覆盖一个val
属性,但反之则不行。这是允许的,因为一个val
属性本质上声明了一个getter
方法,而将其覆盖为var
只是在子类中额外声明一个setter
方法。
抽象类
类和其中的某些成员可以声明为abstract
。抽象成员在本类中可以不用实现。需要注意的是,我们并不需要用open
标注一个抽象类或者函数,因为抽象类本来就是让人继承的。
1 | open class Base { |
接口
Kotlin的接口与Java 8类似,既包含抽象方法的声明,也可以包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。
1 | interface MyInterface { |
实现接口:
1 | class Child : MyInterface { |
与抽象类一样,接口中无需open
修饰,因为本来就是让其他类去实现的。
接口中属性的使用:
1 | interface MyInterface { |
可见性修饰符
在 Kotlin 中有这四个可见性修饰符:private
、protected
、internal
和public
。如果没有显式指定修饰符的话,默认可见性是public
。
private
、protected
和public
的作用于在java中基本一样,internal
表示它会在相同模块内随处可见。那什么是相同的模块呢?
具体地说, 一个模块是编译在一起的一套 Kotlin 文件,比如一个 IntelliJ IDEA 模块;一个 Maven 或者 Gradle 项目或一次 <kotlinc> Ant 任务执行所编译的一套文件。
扩展
在Kotlin中能够扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式,就叫做扩展
。Kotlin
支持扩展函数
和扩展属性
。
声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀,如:
1 | class Expr |
这里就扩展了Expr
类。
扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如:
1 | open class C |
这个例子会输出 “c”,因为调用的扩展函数只取决于参数 c 的声明类型,该类型是 C 类。
同时,当扩展函数和成员函数相同时,调用会调用成员函数,除非两者出现重载。
和函数类似,Kotlin 支持扩展属性:
1 | val <T> List<T>.lastIndex: Int |
扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。
如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数和属性:
1 | class MyClass { |
调用时:MyClass.foo()
在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个隐式接收者
—— 其中的对象成员可以无需通过限定符访问。扩展声明所在的类的实例称为分发接收者
(声明拓展的部分),扩展方法调用所在的接收者类型的实例称为扩展接收者
(拓展部分)。
1 | class D { |
对于分发接收者和扩展接收者的成员名字冲突的情况,扩展接收者优先。
解构声明
Kotlin可以把一个变量解构成多个变量,如:
1 | var a=A() |
一个解构声明会被编译成以下代码:
1 | val x = person.component1() |
所以在A中需要添加如下代码:
1 | operator fun component1(): Any? { |
请注意,componentN() 函数需要用 operator 关键字标记,以允许在解构声明中使用它们,同时数据类自动声明 componentN() 函数。
集合
Kotlin 区分可变集合和不可变集合(lists、sets、maps 等)。
Kotlin 的List<out T>
类型是一个提供只读操作如size
、get
等的接口,要想编辑需要使用MutableList<T>
。同样的这也适用于Set<out T>
/MutableSet<T>
及Map<K, out V>
/MutableMap<K, V>
。
1 | val numbers: MutableList<Int> = mutableListOf(1, 2, 3) |
Kotlin 没有专门的语法结构创建 list 或 set。 要用标准库的方法,如 listOf()、 mutableListOf()、 setOf()、 mutableSetOf()。 在非性能关键代码中创建 map 可以用一个简单的惯用法来完成:mapOf(a to b, c to d)。
区间
区间表达式由具有操作符形式 … 的rangeTo
函数辅以 in 和 !in 形成。
如:
1 | if (i in 1..10) { // 等同于 1 <= i && i <= 10 |
这里需要注意的是1..10
是不能倒过来的,即如果写成10..1
,那么就表明是 10 <= i && i <= 1,这样程序是不会执行的。
所以如果要使用倒序,则需要使用downTo
函数:
1 | for (i in 10 downTo 1) print(i) |
除此以外,还有几个函数也非常有用:
1 | for (i in 1..10 step 2) print(i)//输出 13579 |
step
函数表示执行的步长度。step 2
说明每部执行2个数(即执行一步,跳一步)。
1 | for (i in 1 until 10) { // i in [1, 10) 排除了 10 |
until
函数表示不包含某数。
1 | for (i in (1..10).reversed()){//10,9,8,7,6,5,4,3,2,1 |
reversed
能返回函数返回数据反转后的数列。
类型的检查与转换
我们可以使用is
操作符或其否定形式!is
来检查对象是否是给定类型。
1 | if (obj is String) { |
在多数情况下,不需要使用显式转换操作符,因为编译器会跟踪is
和!is
检查,并在需要时自动插入转换:
1 | fun demo(x: Any) { |
这中智能转换也一样适用于when表达式。
当变量不能保证检查和使用时不可变时,不会进行智能转换,所以智能转换的使用范围:
val 局部变量——总是可以;
val 属性——如果属性是 private 或 internal,或者该检查在声明属性的同一模块中执行。智能转换不适用于 open 的属性或者具有自定义 getter 的属性;
var 局部变量——如果变量在检查和使用之间没有修改、并且没有在会修改它的 lambda 中捕获;
var 属性——决不可能(因为该变量可以随时被其他代码修改)。
Kotlin中的可以使用操作符as
对类型进行强制转换:
1 | val x: String = y as String |
当y为null时,上述代码会报错,所以我们必须在在转换左右都有可空类型:
1 | val x: String? = y as String? |
this作用域
要访问来自外部作用域的this,我们使用this@label,其中 @label 是一个代指 this 来源的标签:
1 | class A { // 隐式标签 @A |
空安全
在Kotlin
中,类型系统会区分一个引用是否可以容纳null
:
1 | var a: String = "abc" |
当类型允许为空,但依旧被调用时:
1 | var b: String? = "abc" |
这种情况时会编译错误,这时,我们可以对它进行空判断:
1 | var b: String? = "abc" |
除此以外,我们还可以使用安全调用符,?.
1 | val l = b?.length |
它表示当b为null时,返回null,不为null时,返回b.length。
除此之外,我们还可以使用Elvis
操作符,写作?:
:
1 | val l = b?.length ?: -1 |
它表示当左边的表达式非空时执行左边的表达式,为空时执行右边的表达式。
第三中方式是使用!! 操作符:
1 | val l = b!!.length |
当b为空时,程序会抛出NPE
异常。