0%

Kotlin 实战读书笔记(一)

《Kotlin 实战》读书笔记 (1~4 章)

第一部分 Kotlin 简介

1 Kotlin : 定义和目的

针对 Java 平台的静态类型语言。

Kotlin Playground
编译 Kotlin
1
2
3
4

kotlinc <source file or dir> -include-runtime -d xxx.jar
java xxx.jar

2 Kotlin 基础

关于语句和表达式

在 Kotlin中,if 是表达式,而不是语句。语句和表达式的区别在于,表达式有值,并且能作为另一个表达式的一部分使用;而语句总是包围着它的代码块中的顶层元素,并且没有自己的值。

在 Java中,所有的控制结构都是语句。而在 Kotlin中,除了循环(for、do和 dowhile)以外大多数控制结构都是表达式。这种结合控制结构和其他表达式的能力让你可以简明扼要地表示许多常见的模式,稍后你会在本书中看到这些内容。

另一方面,Java中的赋值操作是表达式,在 Kotlin中反而变成了语句。这有助于避免比较和赋值之间的混淆,而这种混淆是常见的错误来源。

if 是表达式的意义

1
fun max(a: Int,b: Int) = if (a > b) a else b

但这在 java 中是不可以的,因为 if 是语句

类和属性
  • 自定义访问器

    1
    2
    3
    4
    class Rectangle(val h:Int,val w:Int) {
    val isSquare : Boolean
    get() = h == w
    }
  • 枚举的定义,唯一的分号 ;

    1
    2
    3
    4
    5
    enum class Color(val r:Int) {
    BLACK(0),WHITE(1); // 此处必须有分号

    fun xxx(){}
    }
when & for
  • when

    可以使用枚举,此时 when 是一个语句,可以直接作为返回值

    1
    2
    3
    4
    5
    fun getXXX(color:Color) = 
    when(color) {
    Color.WHITE -> "xx"
    Color.BLACK -> "oo"
    }

    和 Java 中 switch 的限制不同, when 可以使用任意对象,比如集合。还有一种不带参数的 when

    1
    2
    3
    4
    5
    6
    fun mix(c1:Color,c2:Color) = 
    when {
    (c1 == RED && c2 == YELLOW) -> "XX"
    (c1 == BLACK) -> "OOO"
    (c2 == WHITE) -> "ASDF"
    }

    因此,在 kotlin 中使用 when 似乎可以覆盖所有场景,包括各种 if-else 结构。

  • for

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    for(i in 0..10){} // 闭合区间
    for(i in 0..10 step 2) // 调整步进
    for(i in 100 downTo 1) // 反向
    for(i in 0 until 10){} // 开区间,不包括 10

    // map 循环输出
    val map = TreeMap<Char,String>()
    for(c in 'A'..'F') {
    val value = Integer.toBinaryString(c.toInt())
    map[c] = value
    }
    for((key,value) in map) {
    println("$key == $value")
    }

    // 列表带下标输出
    val list = arrayListOf("a","b","c")
    for((index,element) in list.withIndex()) {
    println("$index : $element")
    }

    // !in 区间判断
    val number = 5 !in 'a'..'c'
    val letter = 'a' !in 0..9
Kotlin 异常
  • Kotlin 不区分受检异常和未受检异常(因为没有意义),因此不需要抛出未受检异常
  • try 可以作为表达式

3 函数的定义和调用

更好的函数定义
  • 默认参数值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fun <T> doXX(list:Collection<T>,
    a: String? = "A",
    b: String = "B" ) :String
    ```

    由于 Java 没有默认参数值,因此调用 doXX 时必须传所有参数。但也可以用 **JvmOverloads** 注解 doXX 方法,在编译期会帮忙生成重载方法,从最后一个参数开始省略

    - 可以直接在kotlin 文件中定义函数和属性,比如 Util.kt
    ```kotlin
    @file:JvmName("CustomUtil")
    package com

    fun foo() {}

    调用

    1
    2
    3
    4
    5
    6
    import com.UtilKt // 当未使用 @file:Jvm 注解时默认的类名
    import com.CustomUtil // 注解后的类名

    fun my() {
    CustomUtil.foo()
    }

    同理可以定义属性,这一特性称为顶层函数和顶层属性

  • 扩展函数和属性

    • 扩展函数
    1
    2
    3
    4
    5
    6
    // StringUtil.kt
    package com

    fun String.lastChar() : Char {
    return this.get(this.length - 1)
    }

    调用

    1
    2
    3
    import com.lastChar
    import com.lastChar as last // 也可以重命名,解决冲突问题
    val last = "Hello World".lastChar()

    其中 String 叫做 接收者类型,this(“Hello World”) 叫做接收者对象 . 扩展函数可以认为是类的成员函数。

    本质上,扩展函数是静态函数,把调用对象作为第一个参数。因此在 java 代码中调用需要这样

    1
    char c = StringUtilKt.lastChar("Java World");

    由于扩展函数是静态函数,他不可以被重写,因此当父类和子类添加了相同的扩展函数时,他由变量的静态类型确定,而不是运行期的类型。同时如果扩展函数和类中的成员函数相同时,成员函数优先使用。

    • 扩展属性

      1
      2
      3
      4
      5
      // StringUtil.kt
      package com

      fun String.lastChar : Char
      get() = get(length -1)

      调用

      1
      val result = "kotlin".lastChar // kotlin
      1
      Char result = StringUtilKt.getLastChar("Java");
      集合的处理(可变参数、中缀调用和库的表示)
  • 可变参数函数定义

    1
    fun listOf<T>(vararg values: T) : List<T> {}

    java 用 … ,kotlin 用 vararg

  • 中缀调用和结构处理

    1
    val map = mapOf(1 to "one",2 to "two",3 to "three")

    to 是一种特殊的函数调用,即中缀调用。函数必须用 infix 修饰

    1
    2
    // 函数原型
    infix fun Any.to(o: Any) = Pair(this,o)

    结构处理

    1
    val (first,second) = 1 to "one"
字符串分割 (更加友好)
局部函数和扩展

相当于在方法内部定义方法,是代码更紧凑。淡然也可以把定义在内部的方法当做类的扩展函数处理,这个就看具体设计了。

1
2
3
4
5
6
7
8
9
10
fun saveUser(user:User) {
// validate 局部函数
fun validate(value:String,name:String) {
if(value.isEmpyt()){
throw Exception("error")
}
}
validate(user.name,"Name")
validate(user.address,"Address")
}

4 类、对象和接口

修饰符
  • 访问修饰符

Java 的类和方法默认都是 open 的,而 Kotlin 中默认都是 final 的。

修饰符 相关成员 评注
final 不能被重写 类中成员默认
open 可以被重写 需要明确地表明
abstract 必须被重写 只能在抽象类中使用;抽象成员不能有实现
override 重写父类或接口中的成员 如果没有使用 final,表明重写的成员默认是开放的
  • 可见性修饰符

在 Kotlin 中默认是 public 的,用 internal 表达模内部可见的含义。

internal 在编译阶段,最终会变成 public 的,因此会出现某些类可以通过 java 访问但不能通过 kotlin 访问的场景

修饰符 类成员 顶层声明
public 所有地方可见 所有地方可见
internal 模块中可见 模块中可见
protected 子类中可见 -
private 类中可见 文件中可见
内部类和嵌套类

在 Kotlin 中由于 fun 是一等公民,而不像 Java 中的类,因此在同一个文件中声明的类的包含关系有差异。

类 A 在另一个类 B 中声明 在 Java 中 在 Kotlin 中
嵌套类(不存储外部类的引用) static class A class A
内部类(持有外部类的引用) class A inner class A
密封类

被 scaled 标识的类,其直接子类必须嵌套在父类中,保证了代码边界条件的规范性。

1
2
3
4
scaled class Expr {
class Num() : Expr()
class Sum() : Expr()
}
自定义访问器

比较玄幻

编译器生成的方法: 数据类和类委托
数据类
1
data class People(val name:String,val age:Int)

data 标记的数据类会根据属性主动生成友好的 toString(),hashCode(),equals(),get()/set() 等方法。对上述 People 文件生成的 class 反转成 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public final class People {
@NotNull
private final String name;
private final int age;

@NotNull
public final String getName() {
return this.name;
}

public final int getAge() {
return this.age;
}

public People(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}

@NotNull
public final String component1() {
return this.name;
}

public final int component2() {
return this.age;
}

@NotNull
public final People copy(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
return new People(name, age);
}

// $FF: synthetic method
public static People copy$default(People var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.name;
}

if ((var3 & 2) != 0) {
var2 = var0.age;
}

return var0.copy(var1, var2);
}

@NotNull
public String toString() {
return "People(name=" + this.name + ", age=" + this.age + ")";
}

public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age);
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof People) {
People var2 = (People)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}

return false;
} else {
return true;
}
}
}

可以看到

  • 由于在定义 People 的时候,name:String 默认非空,编译后的代码添加了 @NotNull 注解,并在构造函数里进行了空判断。
  • toString 打印了所有属性
  • hashCode 会优先根据 name 属性生成,name 为空时会用 age 生成
  • equals 的实现和我们正常的实现思路基本一致,比较对象及所有属性。
类委托
1
2
class DelegateCollection<T>(
innerList: Collection<T> = ArrayList()) : Collection<T> by innerList

DelegateCollection 实现 Collection 后默认行为通过 by 委托给 innerList(即 ArrayList 实现),而需要包装的行为则可以由自己实现。避免了装饰者模式中的模板代码。

装饰者模式一句话介绍: 创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不用被修改,只需要由原始类的实例进行装发。

object 关键字用法

这个关键字定义一个类并同时创建一个实例

常用场景:

  • 对象声明,创建单例

将类声明和该类的单一实例声明结合到一起实现

1
2
3
4
5
6
7
8
9
10
 object Single {
val prop = "单例"

fun doXX() {}
}

// 调用

val v = Single.prop
Single.doXX()

可以理解为用 object 标记的类在声明时就创建了该类的对象。同时他没有构造函数,其他地方也无法创建他的实例,因此他天然就是个单例。在 Java 中使用时稍微有些差别:

1
2
Single.INSTANCE.doXX();
String v = Single.INSTANCE.getProp();

可以用 @JvmStatic 注解方法或熟悉,避免 INSTANCE

1
2
@JvmStatic
fun doXX() {}
1
Single.doXX();
  • 伴生对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Factory private constructor(val brand: String) {
companion object {
val ONE_BYTE = 1024
val ONE_MB = ONE_BYTE * ONE_BYTE
val ONE_GB = ONE_MB * ONE_MB
fun appleFactory() = Factory("Apple")
fun androidFactory() = Factory("Google")
}
}

// 调用

val oneByte = Factory.ONE_BYTE
Factory.appleFactory()
Factory.androidFactory()

相当于是 Java 中的静态方法和静态变量。通过 companion object 进行包裹即可。还可访问类的 private 构造函数,因此是工厂模式的典范。

  • 对象表达式

用来声明匿名对象,替代 Java 中匿名内部类的用法。

1
2
3
4
5
6
7
8
fun doSomeThing(param:Int) {
Thread(object :Runnable {
override fun run() {
println("$param")
}

})
}

这可以看到,在内部类当中,可以直接访问方法参数,而在 Java 中这个参数必须是 final 的。Kotlin 会自动完成捕获。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 用 object 可以实现多个接口
*/
val multiInterfaceImpl = object :Runnable ,Callable<Int>{
override fun call(): Int {
return 1
}

override fun run() {

}
}

可以看到用 object 可以实现多个接口,这是 Java 做不到的。

匿名对象并不是单例

加个鸡腿呗.