Kotlin——基本概念

委托

将自己的事情交给别人做,自己只要关注这件事情处理的记过就好了,这就是委托的过程。

委托属性

属性委托通常是为了解决以下场景的问题

  • 延迟属性(lazy property),其值只在首次访问时计算(首次访问时分配内存)
  • 可观察属性(observable property),监听器或收到有关此属性变更的通知
  • 把多个属性存储在一个映射(map)中,而不是每个存在单独的字段中

为了解决以上几种场景,Kotlin支持属性的委托,基本格式如下:

val/var 属性名:属性类型 by 表达式。在 by 后面的表达式是该 委托, 因为属性对应的 get()(与 set())会被委托给它的 getValue()setValue() 方法。 属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数(与 setValue()——对于 var 属性)。

委托实现的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import kotlin.reflect.KProperty

class Example {
var p: String by Delegate()
}


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")
}

}

fun main() {
val e = Example()
println(e.p)
e.p = "Randy"
}

属性委托的要求:

  1. thisRef类型必须与属性所有者类型相同(或者是其超类型)(对于扩展属性的委托,必须与扩展属性所在类类型相同,或者是其超类型)
  2. property类型必须是KProperty类型或其超类型
  3. value类型必须与属性类型相同或是其超类型
  4. getValue必须返回与属性类型的同的类型或其子类型

注意:lambda表达式返回值总是返回函数体内部最后一行表达式的值

延迟委托 lazy

使用示例:var propertyName by lazy lambda

lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

lateinit 和 by lazy比较 (结果由 chatGPT提供)

1
2
3
4
5
6
7
8
9
10
11
lateinit 和 by lazy 都是延迟初始化属性的方式,但它们有以下区别:

lateinit 只能用于 var 变量,而 by lazy 只能用于 val 变量。

lateinit 只适用于可空类型,而 by lazy 可以适用于任何类型。

lateinit 只是延迟初始化,而 by lazy 是惰性初始化,只有在第一次访问属性时才会初始化。

lateinit 变量必须在使用前进行初始化,否则会抛出异常;而 by lazy 变量则只有在第一次访问时才会进行初始化,如果不访问就不会初始化。

因此,如果您需要在声明时初始化变量,就应该使用 lateinit;如果您需要在第一次访问时初始化变量,就应该使用 by lazy。

可观察属性 Observable

主要实现是在Delegates.observable()中,例如:

1
2
var test0: String by Delegates.observable("Randy") { property, oldValue, newValue -> println("${property.name}:$oldValue->$newValue") }
test0 = "Jack"

输出:

1
test0:Randy->Jack

还可以通过Delegate.vetoable来防止属性被更改,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var test: String by Delegates.vetoable("ss") { property: KProperty<*>, oldValue: String, newValue: String ->
ss(
oldValue,
newValue
)
}
println(test)
test = "Jack"
println(test)


fun ss(oldValue: String, newValue: String): Boolean {
return true;
}

当ss函数返回true是,表示允许修改test属性,反之则不允许修改

委托给另一个属性

从1.4起,支持将一个属性的getter和setter方法委托给另一个属性处理,这种委托对顶层和类的属性(成员属性或扩展属性)都可用。通常这种委托方式在 向后兼容 处理是会非常有用。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var topPro3: String = "ss"

class MyClass(var pro1: String, anotherClass: AnotherClass) {
/**
* 将属性委托给自身的另一个属性
*/
var deprecatePro1: String by this::pro1
/**
* 将deprecatePro3委托给灵位一个类 AnotherClass的aPro4属性
*/
var deprecatePro3: String by anotherClass::aPro4

}

class AnotherClass(var aPro4: String)

/**
* 扩展属性委托给顶层属性
*/
var MyClass.deprecateTopPro3: String by ::topPro3

将属性存储在映射中

利用映射来存储 属性的值,非常适用于JSON解析这种场景

1
2
3
4
5
6
7
8
9
10
class TestUser(var map: Map<String, Any?>) {
val name: String by map
val age: Int by map

}

class TestUser2(var map: MutableMap<String, Any?>) {
var name: String by map// name 是读写的,所以使用的 map 必须支持 读写
var age: Int by map// age 是读写的 所以使用的 map 必须支持 读写
}

局部属性委托

1
2
3
4
5
6
7
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)

if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}

上面的代码将局部属性 memoizedFoo 委托给了 computeFoo这个 lambda表达式,这样做的好处是 如果 if 条件不满足,memoizeFoo 根本就不会初始化

委托实现

委托实现就是通过使用组合的方式来代替通过继承的方式来进行一些业务逻辑的视线。

为什么要这样做呢?继承的缺点:

  1. 无法通过继承的方式,重用多个类的代码,(当然可以通过接口实现)
  2. 通过继承,子类必须无条件就收父类的内容,没得选
  3. 父类继承而来的实现是静态的,不能动态改变,虽然可以通过重写做一些改善,但还不够灵活

所以,后面就提出用组合的方式来取代通过继承来实现代码的复用。

闲话不多扯了,来看看Kotlin 中类委托实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
}

同上上面这种写法,Kotlin编译后会自动生成 Base中定义的方法,同时对象b会存储在 Derived中,看看编译后 decompile来的Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface Base {
void print();
}

public final class BaseImpl implements Base {
private final int x;

public void print() {
int var1 = this.x;
boolean var2 = false;
System.out.print(var1);
}

public final int getX() {
return this.x;
}

public BaseImpl(int x) {
this.x = x;
}
}

public final class Derived implements Base {
// $FF: synthetic field
private final Base $$delegate_0;

public Derived(@NotNull Base b) {
Intrinsics.checkNotNullParameter(b, "b");
super();
this.$$delegate_0 = b;
}

public void print() {
this.$$delegate_0.print();
}
}

可以看到,Derived 中最终处理 print的都是 $$delegate_0这个属性,这个属性就是通过by 指定的由Kotlin自动生成的。

属性

幕后字段

在Kotlin中, 如果属性至少一个访问器使用默认实现,那么Kotlin会自动提供幕后字段,用关键字field表示,幕后字段主要用于自定义getter和setter中,并且只能在getter 和setter中访问。

幕后属性

对外表现为只读,对内表现为可读可写,我们将这个属性成为幕后属性

参考文章:什么是幕后字段和幕后属性

SAM接口

只有一个单一抽象方法(但可以有多个非抽象的方法)的接口,这种借口可以直接用lambda函数来实现,从而使代码简洁

扩展

Kotlin 能够对一个类或接口扩展新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做扩展的特殊声明完成。

扩展函数

声明一个扩展函数需用一个接收者类型也就是被扩展的类型来作为他的前缀。 下面代码为 MutableList<Int> 添加一个swap 函数:

1
2
3
4
5
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}

扩展是静态的解析的

扩展不能真正的修改他们所扩展的类。通过定义一个扩展,并没有在一个类中插入新成员, 只不过是可以通过该类型的变量用点表达式去调用这个新函数。

扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 这个例子说明扩展式静态的,s 传入的是Rectangle类型,但定义时用的时Shape类型,所以调用扩展方法时会采用Shape类型的扩展方法
*/
fun main2() {
open class Shape
class Rectangle : Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
println(s.getName())
}

printClassName(Rectangle())
}

可空接收者

可以为可空的接收者定义扩展,这样的扩展可以在对象变量上调用(即使为空也可以调用),例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main3() {
var name:String? = null

println(name.toString())
name = "RandyZhang"

println(name.toString())
}

fun Any?.toString() :String {
if (this == null) {
return "null"
}
return toString()
}

输出结果:

1
2
3
4
null
RandyZhang

Process finished with exit code 0

扩展属性

可以为List扩展一个 lastIndex 属性,用来表示最后一个元素的下标

1
2
3
4
5
6
7
8
9
val <T> List<T>.lastIndex: Int
get() = size - 1
fun main4() {
var list = arrayListOf(1, 3, 5)
var list1 = arrayListOf<Int>()

println("lastIndexOf list: ${list.lastIndex}")
println("lastIndexOf list1: ${list1.lastIndex}")
}

注意:

  1. 扩展属性不能有初始化器
  2. 要注意扩展属性定义的位置(大多数情况下都直接定义在包里)

kotlin单例实现

  1. 对象声明

    1
    2
    3
    4
    5
    6
    object Singleton {
        val property: String = "I am a singleton"
        fun method() {
            println("Method of singleton")
        }
    }
  2. 伴生对象

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass {
    companion object {
    val property: String = "I am a singleton"
    fun method() {
    println("Method of singleton")
    }
    }
    }

伴生对象的扩展

可以直接用伴生对象所在的类的类名访问伴生对象的扩展方法和扩展属性

1
2
3
4
5
6
7
8
9
10
11
class MyCompanion {
companion object
}

fun MyCompanion.Companion.printCompanion() {
println("companion")
}

fun main5() {
MyCompanion.printCompanion()
}
  • 扩展声明为成员

    这个好像比较复杂

数据类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 数据类
* 1. 主构造函数至少要有一个var或val修饰的属性
* 2. 会利用主构造函数里面的属性 自动生成对应的 hashCode、equals、和toString方法
* 3. 会按照中构造函数中声明的顺序,生成相应的considerN方法
* 4. 支持通过copy来产生对象,并对对象进行修改
* 5. 不能为considerN、copy提供显示的实现
* 6. 数据类不能是抽象、开放、密封或内部的
*/
data class User(var id: Int, var name: String) {
var state: String? = null
}

生成的.class文件如下:

1
2
3
4
5
6
7
8
9
10
11
public final data class User public constructor(id: kotlin.Int, name: kotlin.String) {
public final var id: kotlin.Int /* compiled code */

public final var name: kotlin.String /* compiled code */

public final var state: kotlin.String? /* compiled code */

public final operator fun component1(): kotlin.Int { /* compiled code */ }

public final operator fun component2(): kotlin.String { /* compiled code */ }
}

密封类

1
sealed class Error

密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

密封类作为when的判断条件时,是不需要再最后写else判断的,因为密封类的值集合是受限的

嵌套类

普通类中定义的一个无特殊修饰关键字的类就成为嵌套类、嵌套接口

1
2
3
4
5
6
7
8
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}

val demo = Outer.Nested().foo() // == 2

嵌套接口

1
2
3
4
5
6
7
8
9
interface OuterInterface {
class InnerClass
interface InnerInterface
}

class OuterClass {
class InnerClass
interface InnerInterface
}

内部类

类内部定义一个用 inner关键字修饰的类,这个inner关键字修饰的类就成为内部类,同Java一样,内部类会持有外部类的引用

1
2
3
4
5
6
7
8
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}

val demo = Outer().Inner().foo() // == 1

匿名内部类

匿名内部类通常用对象表达式表示

1
2
3
4
5
6
window.addMouseListener(object : MouseAdapter() {

override fun mouseClicked(e: MouseEvent) { …… }

override fun mouseEntered(e: MouseEvent) { …… }
})

作用域函数

  • let,扩展函数,调用let中使用的函数参数,并将调用let的对象作为参数,然后返回函数参数的结果
  • with,非扩展函数,将上下文对象作为参数传递到lambda表达式中,在lambda表达式中可以同伙this来访问这个上下文对象
  • run(非扩展形式的),就是开辟一个临时作用域来运行一段代码,并将运行代码的结果返回
  • run(扩展形式的),将上下文对象传递到lambda表达式中,在lambda表达式中可以通过this来访问这个对象,同时将lambda的结果返回
  • apply,扩展函数,对上下文对象应用一些列操作,并将对象本身返回
  • also,扩展函数,可以理解为在做某个操作时顺带做下某事,上下文对象会作为参数用(it)表示,传递到lambda中,并最终在lambda执行后返回上下文对象本身

takeIftakeUnless

作为作用域函数的补充,这两个函数配合作用域函数使得可以作用域函数调用链中进行值的检查与筛选

  • takeIf,如果满足给定的条件,就返回对象本身,否则返回null

  • takeUnless,和takeIf相反,如果不满足,就返回对象本身,否则返回null

其他

相等判断

  • 构成相等使用 == 运算符,并调用 equals() 来确定两个实例是否相等。
  • 引用相等使用 === 运算符,以检查两个引用是否指向同一对象。
  • ?,左边调用对象如果为空,则返回null,不为空正常执行(空安全调用)
  • !!,左边对象如果为空,则抛出NPE,否则正常执行(确定不为空)
  • ?:,elvis 表达式,例子

    1
    2
    3
    var firstName : String?
    ...
    println(firstName?:"Unknown")// firstName 不为空则输出firstName,为空则输出Unknown

默认参数于具名参数

假如定义了以下数据类:

1
data class User(var firstName: String? = null, var lastName: String?)

那么,主构造函数中的 firstName参数就是默认参数,具体使用例子如下:

1
2
3
var user1 = User("randy")// 这种写法是错误,由于默认参数将居于未设默认值的参数之前,使用时必须
// 指定参数的名字
var user2 = User(lastName = "randy")