0%

Kotlin 实战读书笔记(二)

前言

《 Kotlin 实战 》读书笔记(5~6章 & 附录及其他)。

《 Kotlin 实战 》读书笔记

5 Lambda 编程

Lambda,可以作为函数参数的代码块

1
val sum = {x: Int, y: Int -> x + y}

集合的函数式 API

  • fliter 过滤 (保留符合 predicate=true 的值)
  • map 变换
  • all,any,count,find
  • groupBy 列表按 lambda 表达式提供的字段key 转换为 Map<key,List>
  • flatMap 先对集合中的每个元素做 map 操作,然后把列表合并和一个
  • flatten 直接平铺(合并)
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
fun main() {
val origin = listOf("a", "b", "c", "X", "H", "d", "efg", "hi", "mike")

val filter = origin.filter { it.length > 1 }
println(filter)

val map = origin.map { it.toUpperCase() }
println(map)

val all = origin.filter { it.length == 1 }.all { it in "a".."z" }
// 所有单个字母是否属于小写字母
println(all)

val any = origin.any { it in "a".."z" }
// 至少有一个匹配 lambda 表达式
println(any)

val count = origin.count { it !in "a".."z" }
// 综上,all 和 any 在逻辑上是互非的操作,可以按照理解选择
println(count)

val find = origin.find { it in "e".."z" }
// 返回第一个找到的元素,还有 fiirstOrNull
println(find)

val groupBy = origin.groupBy { it.first() }
println(groupBy)

val flatMap = origin.flatMap { it.toList()}
println(flatMap)

}

output

1
2
3
4
5
6
7
8
[efg, hi, mike]
[A, B, C, X, H, D, EFG, HI, MIKE]
false
true
2
efg
{a=[a], b=[b], c=[c], X=[X], H=[H], d=[d], e=[efg], h=[hi], m=[mike]}
[a, b, c, X, H, d, e, f, g, h, i, m, i, k, e]

合理使用搭配操作符,避免出现相同操作被执行多次的情况,或者优先筛选,避免无谓的迭代查找

惰性集合操作

以上 API 操作,每一个操作符在中间会生成一系列的列表,即生成了许多中间的暂时对象,当列表数量巨大时,产生的开销也是巨大的,因此需要优化

1
2
3
4
5
6
7
8
9
fun userSequence() {
val origin = listOf("a", "b", "c", "X", "H", "d", "efg", "hi", "mike")

val result = origin.asSequence()
.filter { it in "c".."h" }
.map { "<".plus(it).plus(">") }
.toList()
println(result)
}

output

1
[<c>, <d>, <efg>]

只有在执行最后一个操作符(比如上面的 toList())时才会进行所有运算。 其实和 Java8 中 Stream 是类似的。

函数式接口

SAM

lambda 表达式转换成函数式接口
1
val run = Runnable { println("just run") }

带接收者的 lambda :with & apply

with

方法原型

1
2
3
4
5
6
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
apply
1
2
3
4
5
6
7
8
  @kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

首先从函数原型可以看出,二者都是对接收者对象进行了 lambda 表达式的操作。with 返回的是lambda 表达式结果, apply 返回的永远都是接收者对象本身。结合下面的例子:

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
val result = with(StringBuilder()) {
append("a")
append("b")
append("c")
append("d")
append("e")
toString()
1
}
println(result)
println(result.javaClass)

val output = StringBuilder().apply {
append("a")
append("b")
append("c")
append("d")
append("e")
toString()
}
println(output.javaClass)
1
2
3
1
int
class java.lang.StringBuilder

结果和方法定义也是如出一辙。因此这就是二者的区别。apply 本质上是一个扩展函数。带接收者的 lambda 是构建 DSL 的好工具。

6 Kotlin 的类型系统

Elvis 运算符 “?:”

1
2
3
fun foo(s: String?) {
val length : Int = s?.length ?: 0
}

!! 非空断言

1
2
3
4
5
fun foo(s: String?) {
val result = s!!
println(result.length)
}
foo(null)

此时,在 s!! 的时候立即会发生 npe,而不是在使用的时候。

对于控制还可以使用 let 操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用

val param: String? = ""
param?.let {

println(it)
}
// 函数原型

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

可以看到 let 函数和之前的 apply 函数有些相似,都是扩展函数,但是 let 返回的是 lamdba 执行的结果,而 apply 返回的永远都是接收者对象;再有和 let 和 with 也是不一样的,虽然都是返回 block 执行结果,但是with 的默认参数是 this 因此可以直接调用接收者方法,而 let 默认参数是 it ;必须用 it 才能调用接收者的方法。

lateinit

个人经验,lateinit 比较坑,尽量不用吧

1
2
3
4
5
private lateinit var mm :Any
....
if(::mm.isInitialized) {
// 最安全的使用场景
}

类型参数的可空

在 Kotlin 中所有泛型类和泛型函数的类似参数默认都是可空的。

1
2
3
fun <T> printHash(t: T) {
println(t?.hashCode())
}

类型参数推导出的类型是 Any?,因此 T 虽然声明非空,但是其实是可空的。为了避免这种情况,可以这样声明:

1
2
3
fun <T :Any> printHash(t: T) {
println(t?.hashCode())
}

可控性和 Java

  • 包含注解

    首先,如果 Java 代码写的比较规范,那么代码中

    1
    2
    @Nullable + Type == Type?
    @Notable + Type == Type

    如果 Type 没有注解,那么在 Kotlin 中会被当做平台类型

  • 平台类型

    不知道可空性信息的类型,即使用者既可以当做空类型,也可以当做非空类型。进行操作是,自己决定是否需要判空。

    常见的错误类型信息

    1
    Type mismatch: inferred type is String,but Int was expected

    在用 Kotlin 继承实现 Java 的类时,一定要确定好参数的可控性,不要随意删掉参数后的 ?。Kotlin 编译其会为你声明的每一个非空的参数生成非空断言,如果 java 代码传递了一个 null,断言就会触发,即便你从来都没用过这个参数

    基本数据类型和其他类型

  • Kotlin 不区分基本数据类型和包装类型

  • Any,Unit & Nothing

    • Any 所表达的含义类似于 Java 中的 Object。但是依旧要区分 Any 和 Any?。但是 Any 并不是 Object,没有继承关系。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public open class Any {

    public open operator fun equals(other: Any?): Boolean

    public open fun hashCode(): Int


    public open fun toString(): String
    }

    并没有 Object 的 wait 及 notify 等方法。

    • Unit 表达 Java 中 void 的含义。大部分情况作为函数返回值使用,没有什么特别的。**在 Kotlin 中,Unit 是一个完备的类型,可以作为参数类型(就是泛型尖括号里内容),但是 void 是不行的。而且他有唯一的值,这个值也是 Unit,在返回为 Unit 的函数中会被隐式的返回。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface Processor<T> {
    fun proc(): T
    }

    class NoResultProcessor : Processor<Unit> {
    override fun proc() {
    // 底层帮你显示的返回了这句,不用自己写
    return Unit
    }
    }

    这个特性其实非常好,类似 Callable 和 Runnable 其实并不需要重复定义了(当然只是对纯粹使用 Kotlin 的实现来说),比较在 Java 中为了表达返返回的泛型为空这件事代码得这么写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private static void simpleCallable() {

    Callable call = new Callable<Void>() {

    @Override
    public Void call() throws Exception {
    // 必须显示的写 return 语句
    return null;
    }
    };
    }
    • Nothing

    这个函数永不返回,一般用在抛出异常等场景。

    集合和数组

  • 集合

    Kotlin 中结合区分为可变集合不可变集合。

    集合类型 只读 可变
    List listOf mutableListOf,arrayListOf
    Set setOf mutableSetOf,hashSetOf,linkedSetOf,sortedSetOf
    Map mapOf mutableMapOf,hashMapOf,linkedMapOf,sortedMapOf

    在 Kotlin 中使用 Java 中定义的方法是,对于方法中集合类型,需要结合实际业务决定集合的可空性、可变性等因素。

  • 数组

  • 引用类型数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 引用类型的数组声明及创建方式
    val strings: Array<String> = arrayOf("a", "b", "c")
    println(strings.joinToString())
    val peoples: Array<People?> = arrayOfNulls<People>(5)
    println(peoples.joinToString())
    val people :Array<People> = Array(3) {
    People("mike $it","${it*it}".toInt())
    }
    println(people.joinToString())
    1
    2
    3
    a, b, c
    null, null, null, null, null
    People(name=mike 0, age=0), People(name=mike 1, age=1), People(name=mike 2, age=4)

    引用类型,有 3 中方法可以创建数组,可以满足日常需求了。

  • 基础类型数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     // 基本数据类型的数组

    val ints: Array<Int> = arrayOf(1, 2, 3, 4)
    println(ints.javaClass)
    println(ints.joinToString())

    // 基础数据类型数组创建方式 1
    val ints0 = IntArray(5)
    println(ints0.javaClass)
    println(ints0.joinToString())

    // 基础数据类型数组创建方式 2
    val ints1 = intArrayOf(0, 1, 2, 4)
    println(ints1.joinToString())

    // 基础数据类型数组创建方式 3
    val ints2 = IntArray(4) { it * it }
    println(ints2.joinToString())

    // [Integer] 转换为 [int]
    val ints3: IntArray = arrayOf(1, 2, 3, 4).toIntArray()
    println(ints3.joinToString())
    1
    2
    3
    4
    5
    6
    7
    class [Ljava.lang.Integer;
    1, 2, 3, 4
    class [I
    0, 0, 0, 0, 0
    0, 1, 2, 4
    0, 1, 4, 9
    1, 2, 3, 4

    IntArray 对应于 int[],而 Array 对应于 Integer[],因此如果需要使用数组,应该经量选择 IntArray,ByteArray,CharArray 这些真正的基础类型。当然,也可以通过装箱类型的数组做转换。

  • 数组遍历

    1
    2
    3
    4
    // 带下标的遍历方式
    for (i in ints2.indices) {
    print(i)
    }

其他

在这本书里始终没有提到,但是有时候又会用到的东西。

位操作符

1
2
3
4
5
6
7
shl(bits) – 左移位 (Java’s <<)
shr(bits) – 右移位 (Java’s >>)
ushr(bits) – 无符号右移位 (Java’s >>>)
and(bits) – 与
or(bits) – 或
xor(bits) – 异或
inv() – 反向

附录

  • 文档相关

类似于 Java 的 Javadoc ,Kotlin 有 KDoc。 有 Java 相比有部分差异。Kotlin 的文档生成工具叫做
Dokka

加个鸡腿呗.