0%

Kotlin 实战读书笔记(三)

前言

《 Kotlin 实战 》读书笔记。

第二部分 拥抱 Kotlin

7 运算符重载及其他约定

约定

在 Kotlin 中,某些功能和特定的函数命名相关。比如在类中定义了一个名为 plus 的特殊方法,那么按照约定,就可以在该类的实例上使用 + 运算符。

重载算数运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}

/**
* 运算符不要求两个数是相同类型,按实际需求出发
*/
operator fun Point.times(scale: Float): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
}

fun main() {
val p1 = Point(1, 2)
val p2 = Point(3, 4)
println(p1 + p2)
}

使用 operator 定义特定名称的方法(当然也可以是扩展函数),就可以按照普通的算数运算符使用,比如这里的 plus == + 。具体可以使用的方法名称如下表

表达式 函数名
a*b times
a/b div
a%b mod
a+b plus
a-b minus

类似的可以有 minusAssign(重载复合运算符)、一元运算符、比较运算符、排序运算符等。

集合与区间约定
  • 下标运算符
1
2
3
4
5
6
7
operator fun Point.get(index:Int) : Int {
return when (index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException()
}
}

也可以自定 in,rangTo,interator等。

解构声明和组件函数
  • 解构声明
1
2
3
4
val p1 = Point(1, 2)
val (x,y) = p1
println(x)
println(y)
1
2
1
2

val (x,y) 称为解构声明,在初始化括号中的变量时,实际上是在调用名为 componentN 的函数,N为声明中变量的位置。对于数据类 data class 编译器为每个在柱构造方法中声明的属性生成了一个 componentN 函数。为了实现同样的功能,你也可以自定义这样的函数。
其实这个功能类似于 Pair / Triple 类的定义。

委托属性
1
2
3
4
5
class C {
var prop : Type by MyDelegate
}

val c = C()

MyDelegate 的实例会被保存在一个隐藏的属性中,通过这个隐藏属性的 setValue 和 getValue 实现。

  • by lazy
1
2
3
val value: Int by lazy {
8 * 8
}

value 只有在被第一次使用的的时候才会完成初始化

8 lambda 作为形参和返回值

声明高阶函数

高阶函数:用另一个函数作为参数或返回值的函数,在 Kotlin 中可以使用 lambda 或函数引用来表示。

  • 函数类型

变量的类型

1
2
3
4
5
6
7
8
9
10
11
val any = Any()
val x = 4
val X = Integer(4)
val sum = { x: Int, y: Int -> x + y }
val enter = { println() }

println(any.javaClass.typeName)
println(x.javaClass.typeName)
println(X.javaClass.typeName)
println(sum.javaClass.typeName)
println(enter.javaClass.typeName)

output

1
2
3
4
5
java.lang.Object
int
java.lang.Integer
com.fun.HighOrderFunKt$main$sum$1
com.fun.HighOrderFunKt$main$enter$1

可以看到 lambda 表达式引用的真实类型就是内部声明的lambda 表达式生成的类。

  • 声明 lambda 表达式(函数)作为参数的函数
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
37
38
39
40
41
/**
* 高阶函数有返回值
*/
fun highOrderNotNull(x: Int, block: () -> Float): Double {
if (x > 0) {
return block().toDouble()
} else {
return 0.0
}
}

/**
* 高阶函数,无返回值,block 只是完成了一个 功能
*/
fun highOrder(x: Int, block: () -> Unit) {
println(x)
block()
}

/**
* 高阶函数,无返回值,block接收参数完成功能
*/
fun highOrderParam(x: Int, y: Int, block: (Int, Int) -> Unit): Int {
block(x, y)
return x
}

/**
* 高阶函数,接收参数,完成特定的 block 并将返回结果作为整个函数的结果返回
*/
fun highOrderParams(age: Int, name: String, boy: Boolean,
block: (age: Int, name: String, boy: Boolean) -> String): String {
return block(age, name, boy)
}

/**
* 高阶函数,为 String 定义一个扩展函数,实现 exchange 工,具体实现由 block 提供
*/
fun String.exchange(block: (String) -> String): String {
return block(this)
}

在 函数类型 block 变量声明时,其内部的方法参数声明类型即可,不需要名称,因为这个参数实际上是调用方的参数而已。 但是声明了也有好处,在 IDE 中可以提升代码的可读性。

在 Kotlin 中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val m = highOrderParam(10, 100) { a, b ->
run {
println("a * b is ${a * b}")
}
}

val origin = "asdfjhjkl"
val upper = origin.exchange {
it.toUpperCase()
}
val number = origin.exchange {
it.reversed()
}
println(upper)
println(number)

在 Java 中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
public static void main(String[] args) {

HighOrderFunKt.highOrderParam(1, 2, new Function2<Integer, Integer, Unit>() {
@Override
public Unit invoke(Integer integer, Integer integer2) {
System.out.println(integer);
System.out.println(integer2);
return Unit.INSTANCE;
}
});

HighOrderFunKt.highOrderParams(18, "mike", true, new Function3<Integer, String, Boolean, String>() {
@Override
public String invoke(Integer integer, String s, Boolean aBoolean) {
return integer + s + aBoolean;
}
});
}
}

可以看到实际上函数类型被声明为普通的接口,一个函数类型的变量是 FunctionN 接口的一个实例。因此,在 Java 中调用带有函数类型的 Kotlin 方法时,创建 FunctionN 接口的实例即可。但是要注意的是,对于返回类型为 Unit 的方法,要显示的返回 Unit.INSTANCE。

当然,在 Java8 中可以直接使用 lambda,因此以上代码可以简化为

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {

HighOrderFunKt.highOrderParam(1, 2, (integer, integer2) -> {
System.out.println(integer);
System.out.println(integer2);
return Unit.INSTANCE;
});

HighOrderFunKt.highOrderParams(18, "mike", true,
(integer, s, aBoolean) -> integer + s + aBoolean);
}
}

当然,函数类型的参数也可以有默认值,这和普通函数的定义是一样的。同时可以就是否为null进行定义,也可以就函数类型本身是否可以为null 进行定义。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
fun highOrderWithDefault(x: Int = 0, block: (Int) -> Unit) {
block(x)
}

fun highOrderWithNullParam(input: String? = null, block: (String?) -> Unit) {
// 函数类型的参数可空,调用时需要注意
block(input)
}

fun highOrderWithNull(input: String, block: () -> String?): String {
// 整个函数本身需要返回非空 String,但是由于函数参数返回了可空 String,因此
// 这里需要单独处理
return block() ?: input
}

/**
* 提供了参数类型有默认实现的高阶函数,调用时如果没有提供 lambda,将以默认方式实现
*/
fun highOrderDefault(input: String, block: ((String) -> String) = { "null" }): String {
return block.invoke(input)
}

/**
* 提供了参数类型可空的高阶函数,调用时如果提供了空 ,用默认实现
*/
fun highOrderNull(input: String, block: ((String) -> String)? = {"null"}): String {
return block?.invoke(input) ?: "null"
}

fun useHighOrder() {
highOrderWithDefault {
// 调用含有默认参数的,不用传参
println(it)
}
highOrderWithNullParam("a") {
// 参数可空,需要判空
it?.apply { }
}

// lambda 的返回值,可为null,需要显示写明
highOrderWithNull("a") {
null
}

highOrderDefault("a") {
// 函数参数不可空时,默认实现似乎有问题
""
}

highOrderNull("a")
}
  • 声明 lambda 表达式(函数)作为返回值的函数

这个功能感觉有点炫技,相同的功能完全可以用正常的代码实现。这里简单举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 摄氏与华氏 计算公式
*
* 华氏度 = 32+ 摄氏度 × 1.8
* 摄氏度 = (华氏度 - 32) ÷ 1.8
*/
fun highOrderReturnFun(type: String): (Int) -> Float {
val base = 32
val factor = 1.8f
when (type) {
"F" -> return { input -> base + input * factor }
"C" -> return { input -> (input - base) / factor }
else -> return { input -> base + input * factor }
}
}

// invoke

var calc = highOrderReturnFun("F")
println("38 ℃ ≈ ${calc(38)}")
calc = highOrderReturnFun("C")
println("100 ℉ ≈ ${calc(100)}")

output

1
2
38 ℃ ≈ 100.4
100 ℉ ≈ 37.77778

这里定义了一个函数,根据highOrderReturnFun函数的参数,分别返回了不同的函数。坦白说,相同的功能完全可以用有阅读性的代码实现。所以,返回函数的函数,个人感觉很鸡肋。如果要用或阅读到了此类函数,两个关键点

- 函数返回值,即冒号后面,是一个 lambda 表达式
- 函数 return 结果必然在一个大括号里,因为要返回一个 lambda 表达式。
内联函数

lambda 表达式会被正常的编译成匿名类。因此,每调用一次lambda表达式,一个额外的类会被创建。并且如果lambda表达式捕获了某个变量,每次调用还会创建一个新的变量。 这会带来额外的性能消耗。

可以使用 inline 修饰函数,实现 lambda 表达式被内联的效果,但是其实是有限制的。比如以函数作为参数的时候,内联效果并不是完整的,同时有变量捕获的情况时,也是无法使用的。

9 泛型

Kotlin 泛型的大部分使用规则和 Java 是类似的。同时提供了更多增强性的功能。

泛型允许我们定义带类型形参的类型。

1
fun <T> List<T>.slice(indices: IntRange): List<T>

T 就是类型形参,T 的默认类型是 Any?。 及可空的任意类型,这是需要注意的一点。

泛型参数在运行是会被擦除,因此你在一般情况下无论是 Java 还是 Kotlin 中,你无法感知你的列表是一个字符串列表还是一个 People 的列表。

在 Kotlin 中可以使用 inline 函数,声明带实化类型参数的函数。

1
2
3
4
inline fun <reified T> isA(value:Any) = value is T

println(isA<String>("aaa"))
println(isA<String>(111))

out

1
2
true
false

可以看到,在运行是我们是可以感知到泛型实际的类型的。

用实化类型参数还可以替代类引用

1
2
3
4
5
6
7
8
9
inline fun <reified T:Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}

final_one.setOnClickListener {
// startActivity(Intent(context, FinalActivity::class.java))
context?.startActivity<FinalActivity>()
}

可以看到,使用实化参数之后,FinalActivity 的类型在运行是得以保留,因此可以通过扩展函数获取到他对应的 Java Class 的类型来构建 Intent 。

10 注解与反射

注解定义与使用
1
annotation class Test(val path: String, val level: Int, val since: SinceKotlin)

在 kotlin 中定义注解。比 Java 中要简介一些。val 比不可少,同时也可以在注解中使用其注解比如这里的 SinceKotlin 作为参数)

1
2
@Test("/user/fly/documents", level, SinceKotlin("1.3.0"))
val dir = "home"

这里需要注意的是,注解参数的值是编译期决定的,因此在使用时,要么直接使用字面量,要么比如使用 const val 定义的值,val 也不行,比如这里的 level 必须在类的最外层定义
const val level = 100 ,要么直接使用 100。

在 Kotlin 中可以直接在表达式上面使用注解,比如@Suppress 这种,范围就可以进一步的缩小,不必为此单独写一个方法。同时 Kotlin 中注解默认的 @Retention 是 Runtime ,而在 Java 中则是 Class。

更多细节可以参考 JKid 这个 Kotlin 实现的 JSON 序列化和反序列化的库。

反射

在 Kotlin 中使用反射,可以用两种 API

  • java.lang.reflect 中 java 的 API(因为 kotlin 代码只是编译成了普通的 Java 字节码,这个是完全兼容的)
  • kotlin.reflect 中 kotlin 的 API 。有意思的是,使用这个库的 API 做反射,并不限定有 kotlin 类,可以访问任何 JVM 语言写成的类。

在 Android 中,kotlin 反射的库并没有被默认依赖,如果需要使用时,要单独添加依赖 org.jetbrains.kotlin:kotlin-reflect

加个鸡腿呗.