1. 类与继承
1.1 类的用法
与java一样使用class声明类,后面可以跟着大括号:
class Invoice { /*……*/ }
class Empty
1.2 构造函数
kotlin可以有一个构造函数以及多个次构造函数。主构造函数是类头的一部分,跟在类头后面:
class Person constructor(firstName: String) { /*……*/ }
如果主构造函数没有任何注解或者可见性修饰符,那么可以省略constructor。
class Person(firstName: String) { /*……*/ }
主构造函数不能包含任何的代码,初始化的代码可以放到Init初始化代码块中。主构造函数的参数可以在属性的初始化器中使用。初始化是按类体中出现的顺序执行:
class 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}")
}
}
如果没有定义主构造函数,类照样会定义一个无参构造函数。也会去调用Init初始化模块:
fun main() {
InitOrderDemo()
}
class InitOrderDemo{
init {
println("== init ==")
}
}
//打印== init ==
1.3 次构造函数
次构造函数是在函数体中使用constructor构造一个构造函数。如果当前有定义主构造函数,那么次构造函数必须委托给主构造函数:
class Person {
var children: MutableList<Person> = mutableListOf<>()
constructor(parent: Person) {
parent.children.add(this)
}
}
class Person(val name: String) {
var children: MutableList<Person> = mutableListOf<>()
constructor(name: String, parent: Person) : this(name) {//有构造主构造函数,则次构造函数必须调用this
parent.children.add(this)
}
}
1.4 创造类的实例
类可以主构造函数或者次构造函数构建对象。对于定义了主构造函数的类,那么无参的默认构造函数就不能使用了。
fun main() {
InitOrderDemo("Hello","parent")
InitOrderDemo("Hello")
InitOrderDemo() //这一行会报错:none of the following functions can be called with the arguments supplied
}
class InitOrderDemo(name: String) {
init {
println("== initializer block == ")
}
constructor(name: String, parent: String):this(name){
println("== constructor ==")
}
}
1.5 类成员
类可以包含:
-
构造函数与初始化块
-
函数
-
属性
-
嵌套类与内部类
-
对象声明
1.6 继承
首先,所有的类都继承自一个共同的超类。他提供了三个基本的函数:equals()
、 hashCode()
与 toString()
。
其次,和java不一样。kotlin类默认是final修饰的,也就是说如果想要开放继承那么就需要open修饰。
最后,对于子类的构造函数,分为两种情况。
- 子类有主构造函数
如果子类有主构造函数, 则基类必须在主构造函数中立即初始化。
open class Person(var name : String, var age : Int){// 基类
}
class Student(name : String, age : Int, var no : String, var score : Int) : Person(name, age) {
}
- 子类没有主构造函数
open class Person(name:String){
init{
println("父类主构造函数:姓名:${name}")
}
}
class Student:Person{
constructor(name:String,age:Int):super(name){//使用 super 关键字初始化其基类型
println("次构造函数1:学生名: ${name},年龄:${age}")
}
constructor(name:String,age:Int,no:String):this(name,age){//委托给另一个构造函数
println("次构造函数2:学生名: ${name},年龄:${age},学号:${no}")
}
}
fun main(args: Array<String>) {
Student("Runoob", 18, "S12345")
println("*************************")
Student("Runoob", 18)
}
如果子类没有主构造函数,那么他可以调用父类的构造函数或者委托给其他的次构造函数。
1.6 覆盖方法
和java一样,在重写的函数前面要加上override关键字。但是被重写的函数必须是open修饰的。
open class Shape {
open fun draw() {
println("Shape::draw")
}
fun fill() {
println("Shape::fill")
}
}
class Circle() : Shape() {
override fun draw() {
println("Circle::draw")
}
override fun fill() {//由于fill并不是open修饰的。error: 'fill' in 'Shape' is final and cannot be overridden
println("Circle::fill")
}
}
1.7 覆盖属性
覆盖属性和覆盖方法在操作上差不多。主要是val和var在覆盖上有差别:
open class Shape {
open var value1: Int = 0
open val value2: Int = 0
}
class Rectangle : Shape() {
override val value1 = 4//val覆盖var是不可以的
override var value2 = 4//var覆盖val是可以的
}
1.8 派生类初始化顺序
这个比较容易理解。显示初始化基类的,再去初始化派生类。
open class Base() {
init { println("Initializing Base") }
}
class Derived() : Base() {
init { println("Initializing Derived") }
}
fun main(args: Array<String>) {
Derived()
}
1.9 调用超类实现
跟java差不多。有一个区别就是kotlin的可以使用@来使用外部类的函数:super@Outer
:
class 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()
}
}
}
1.10 覆盖规则
与java的重写比较大的差别是他可以通过super< >来指定要调用的方法所在的类
open class Rectangle {
open fun draw() {
println("Rectangle|draw")
}
}
interface Polygon {
fun draw() {
println("Polygon|draw")
} // 接口成员默认就是“open”的
}
class Square() : Rectangle(), Polygon {
override fun draw() {
super<Rectangle>.draw() // 调用 Rectangle.draw()
super<Polygon>.draw() // 调用 Polygon.draw()
}
}
fun main(args: Array<String>) {
Square().draw()
}
2. 属性与字段
kotlin中的关键字可以用var声明为可变的,也可以用val声明为不可变的。
2.1 Getters 与 Setters
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
getter和setter是可选的。val的时候是属于只读属性,是不允许setter。而property_initializer初始化器是必须实现的。
假如说只是想改变下getter或setter的可见性修饰或者注解, 你可以定义访问器而不定义其实现:
var setterVisibility: String = "abc"
private set // 此 setter 是私有的并且有默认实现
var setterWithAnnotation: Any? = null
@Inject set // 用 Inject 注解此 setter
2.2 幕后字段
在kotlin中如果一个属性需要一个幕后字段的时候,kotlin会自动提供。这个幕后字段可以使用field
标识符在访问器中引用:
var counter = 0 // 注意:这个初始器直接为幕后字段赋值
set(value) {
if (value >= 0) field = value//field自能在属性的访问器中使用
}
2.3 幕后属性
如果觉得幕后字段的field不可控,那么可以自己定义一个属性用来充当field。
private 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")
}
2.4 编译期常量
首先注意有一点val并不是kotlin的常量。比如说下面一个例子:
val currentTimeMillis: Long get() {return System.currentTimeMillis()}
这个val只能说明属性是只读的,而读出来的数据可能是变化的。kotlin提供了const 定义一个编译期常量。他需要满足一下要求:
-
位于顶层或者是 object 声明 或 companion object 的一个成员
-
以 String 或原生类型值初始化
-
没有自定义 getter
2.5 延迟初始化属性与变量
如果在定义一个变量的时候不想马上初始化变量或者属性,那么可以使用lateinit来修饰,以便稍后赋值。该修饰符只能用于类体中或者顶层属性与局部变量。
需要注意的是,在初始化之前使用该修饰符修饰的变量或抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。所以个人感觉还是尽量少用。
如果检测一个 lateinit var 是否已初始化,可以调用属性的.isInitialized来判断:
if (foo::bar.isInitialized) {
println(foo.bar)
}
3. 接口
kotlin的接口既可以包含方法的声明也可以包含方法的实现。但是不可以保存属性状态。
3.1 接口的使用
一个类或者对象可以实现一个或多个接口:
interface MyInterface {
val name: String//属性必须是抽象的
val name_2: String
get() = "foo"//提供访问器的实现
fun bar()
fun foo() {
println("MyInterface::foo")
}
}
class Child : MyInterface {
override val name_2 = "get name_2"
override val name: String get() = "get name"
override fun bar() {
// 方法体
println("Child::bar")
}
}
fun main(args: Array<String>) {
Child().bar()
Child().foo()
}
3.2 解决覆盖冲突
如果碰到多个接口的方法名是同一个,那么可以用super<类名>来分别调用:
interface A {
fun foo() { print("A") }
}
interface B {
fun foo() { print("B") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
}
4. 可见性修饰符
类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。在kotlin中有四中修饰符:private
、 protected
、 internal
和 public
。more的话是public。
和java不同的是多了个internal
,它是针对模块范围的。所谓的模块就是对应AndroidStudio工程中的module了。
4.1 包
所谓的包,也就是对应在顶层中声明。
-
public:意味着你的声明将随处可见
-
private:作用域只会在文件内
-
internal:作用域只会在相同模块内
-
protected:对此不适用
4.2 类和接口
和4.1包的区别:一个是protect,类和接口是可以用protect修饰,就是说对类或接口的子类可见。另外就是添加了类的限制:
-
private
意味着只在这个类内部(包含其所有成员)可见; -
internal
—— 能见到类声明的 本模块内 的任何客户端都可见其internal
成员; -
public
—— 能见到类声明的任何客户端都可见其public
成员。
5. 数据类
kotlin专门有个数据类来存放数据,着极大方便的有大量的数据对象的场景。kotlin需要在数据类前面添加data关键字修饰。
编译器自动从主构造函数中声明的所有属性导出以下成员:
equals()
/hashCode()
对;toString()
格式是"User(name=John, age=42)"
;componentN()
函数按声明顺序对应于所有属性;copy()
函数(见下文)。
为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:
-
主构造函数需要至少有一个参数;
-
主构造函数的所有参数需要标记为
val
或var
; -
数据类不能是抽象、开放、密封或者内部的;
如果生成类需要用到无参构造函数,那么需要给参数指定默认值:
data class User(val name: String = "", val age: Int = 0)
5.1 在类体中声明的属性
对于kotlin自动识别的函数,比如说toString()、
equals()、
hashCode()以及
copy()只会用到参数中定义的变量。显然两个数据类对象对比的时候也只会对比主构造函数中定义的参数:
data class Person(val name: String) {
var age: Int = 0
}
fun main(args: Array<String>) {
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println(person1 == person2)
}
5.2 复制
复制的过程中也可以随便改变其中的值
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
5.3 数据类与解构声明
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // 输出 "Jane, 35 years of age"
6. 密封类
密封类用sealed修饰。他自身是抽象的,并且可以有抽象成员。
密封类只能有private构造函数。
这边重点提一下和枚举类的却别:每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
}
fun main(args: Array<String>) {
println(eval(Sum(Const(1.0),Const(2.3))))
}
其中Sum可以包含其他状态的实例,这是枚举做不到的。
7.嵌套类与内部类
首先介绍下嵌套类。调用的方式是:外部类.内部类.方法:
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
fun main() {
val demo = Outer.Nested().foo() // == 2
}
可以看到内部类的方法是可以静态调用的。这点很奇怪,反编译看看。
public final class Outer {
private final int bar = 1;
@Metadata(
...
)
public static final class Nested {
public final int foo() {
return 2;
}
}
}
反编译出来的结果可以看到Nested其实是内部静态类,难怪可以直接调用foo
方法。
其次是内部类。内部类会带有一个对外部类的对象的引用。
最后说下匿名内部类,匿名内部类其实其实是用对象表达式来创建的。下面将会介绍。
8. 枚举类
8.1 初始化
每一个枚举是枚举类的实例,所以他们可以通过方法来初始化:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
8.2 匿名类
枚举常量还可以声明其带有相应方法以及覆盖了基类方法的匿名类。
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
8.3 在枚举类中实现接口
枚举类实现接口,只要将接口实现放在枚举声明后即可。可以为接口提供统一实现,也可以在匿名类中分别做实现。
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};
override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
如果使用枚举常量,kotlin
提供了 enumValues<T>()
与 enumValueOf<T>()
函数以泛型的方式访问枚举类中的常量 :
9. 对象表达式与对象声明
如果想要对某个类对象做轻微的改动,那么可以用对象表达式。
对象表达式可以有多个超类,他们用逗号隔开。如果是某个类对象,那么构造函数需要传进适当的参数:
open class A(x: Int) {
public open val y: Int = x
}
interface B { /*……*/ }
val ab: A = object : A(1), B {
override val y = 15
}
如果没有超类的话,如果只是想定义一个对象而已。那么可以:
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}
如果作为返回值的时候,可分为两种情况:
class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}
// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
}
}
对象表达式中的代码可以访问来自包含它的作用域的变量。
9.1 对象声明
对象声明即在 object 关键字后跟一个名称,就像变量声明一样。他是线程安全的并且在首次访问时进行。所以他经常用来作为单例模式来使用,这些对象同时也是可以有超类。
9.2 伴生对象
伴生对象使用companion关键字来标记,伴生对象的名字也是可以省略的。如下所示:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
val instance = MyClass.create()//伴生对象有名称的情况
class MyClass {
companion object { }
}
val x = MyClass.Companion//伴生对象没有名称的情况
顾名思义,其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象 (无论是否具名)的引用:
class MyClass1 {
companion object Named { }
}
val x = MyClass1
class MyClass2 {
companion object { }
}
val y = MyClass2
伴生对象 还可以实现接口。
9.3 对象表达式和对象声明之间的语义差异
对象表达式和对象声明之间有一个重要的语义差别:
- 对象表达式是在使用他们的地方立即执行(及初始化)的;
- 对象声明是在第一次被访问到时延迟初始化的;
- 伴生对象的初始化是在相应的类被加载(解析)时,也可以理解为和类绑定在一起。
10. 类型别名
如果你觉得集合类型比较冗长、如果你觉得函数写起来不方便、如果你觉得内部类写起来太复杂。那么在kotlin
中你可以给这些复杂的结构起个别名。
typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate<T> = (T) -> Boolean
class A {
inner class Inner
}
class B {
inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner
typealias Predicate<T> = (T) -> Boolean
fun foo(p: Predicate<Int>) = p(42)
11. 委托
所谓的委托就是某一个对象委托给另一个类中,那么该类对象就有了被委托对象的功能。
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()//Derived拥有了被委托对象BaseImpl的接口功能
}
如果委托对象实现了和被委托对象同样的函数,那么优先使用委托对象的实现:
interface Base {
fun printMessage()
}
class BaseImpl(val x: Int) : Base {
override fun printMessage() { print(x) }
}
class Derived(b: Base) : Base by b {
override fun printMessage() { print("abc") }
}
fun main() {
val b = BaseImpl(10)
Derived(b).printMessage()//会打印出"abc",也就是说最终调用的是委托对象Derived的实现
}
如果委托对象实现了和被委托对象同样的成员,那么被委托对象只会调用自己定义的成员。
12. 委托属性
委托就相当于代理。不同的代理处理不同的场景,有以下三种场景:
-
延迟属性(lazy properties): 其值只在首次访问时计算;
-
可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
-
把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
委托的语法是: val/var <属性名>: <类型> by <表达式>
。在 by 后面的表达式是该 委托, 因为属性对应的 get()
(与 set()
)会被委托给它的 getValue()
与 setValue()
方法。 属性的委托不必实现任何的接口,但是需要提供一个 getValue()
函数(与 setValue()
——对于 var 属性)。 例如:
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
val e = Example()
println(e.p)
12.1 延迟属性Lazy
lazy的话,就是第一次是会执行 lambda 表达式并记录结果.接下来的话是直接使用结果:
val lazyValue: String by lazy() {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)//执行打印computed
println(lazyValue)//没有执行打印computed
}
如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION
作为参数传递给 lazy()
函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用 LazyThreadSafetyMode.NONE
模式:它不会有任何线程安全的保证以及相关的开销。
12.2 可观察属性 Observable
这个也就是说属性的改变会被实时回调执行,这个在监控数据变化的时候极其有用:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "first"//执行<no name> -> first
user.name = "second"//first -> second
}
12.3 把属性储存在映射中
先写下传统的Map用法:
val map = mapOf(
"name" to "John Doe",
"age" to 25
)
val name = map.get("name")
val age = map.get("age")
这样子获取name或者age会不会觉得要定义多次字段很麻烦,kotlin
提供了Map委托:
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
//再也不用定义name和name了,是不是感觉很方便
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
1.1开始可以将局部变量声明为委托属性.
12.4 属性委托要求
对于一个只读属性(即 val 声明的),委托必须提供一个操作符函数 getValue()
,该函数具有以下参数:
thisRef
—— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。property
—— 必须是类型KProperty<*>
或其超类型。
getValue()
必须返回与属性相同的类型(或其子类型)。
举个例子:
fun main(args: Array<String>) {
println(Owner().valResource)
}
open class SuperOwner{
}
class Owner():SuperOwner(){
val valResource: Int by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: SuperOwner, property: KProperty<*>): Int {//这边的SuperOwner换成Owner同样是可以的.
return 1
}
}
对于一个可变属性(即 var 声明的),委托必须额外提供一个操作符函数 setValue()
, 该函数具有以下参数:
-
thisRef
—— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。 -
property
—— 必须是类型KProperty<*>
或其超类型。 -
value
— 必须与属性类型相同(或者是其超类型)。
12.5 提供委托
如果想扩展属性委托的话是这样子做的:
// 检测属性名称而不使用“provideDelegate”功能
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}
fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// 创建委托
}
注意到如果想做校验checkProperty
的话,那么就必须额外传入字符串.那么有没有什么方法可以拦截属性呢?
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// 创建委托
return ResourceDelegate()
}
private fun checkProperty(thisRef: MyUI, name: String) { …… }
}
相对的他对变量属性 KProperty<*>
进行了自动获取,而不用手动传入.方便很多