阅读 967

kotlin 实战之核心基础特性总结

工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

特别说明,kotlin 系列文章均以 Java 差异为核心进行提炼,与 Java 相同部分不再列出。随着 kotlin 官方版本的迭代,文中有些语法可能会发生变化,请务必留意,语言领悟精髓即可,差异只是语法层面的事情,建议不要过多关注。

kotlin 编译及反编译

对于 kotlin 来说,如果你不用 IDE(其本质也是走的命令行行为),则其编译与反编译原理基本与 java 如出一辙,kotlin 的 kotlinc 命令对应 java 的 javac,用法也相同;kotlin 的 kotlin 命令对应 java 的 java 命令,都是执行程序;由于 kotlin 编译后也是 class 字节码,所以其反编译依然使用 java 的 javap 等工具,这点和 groovy 类似。

需要特别注意的是 kotlin 中非 class 声明的 kotlin 文件(譬如一个FileName.kt文件中顶级定义了变量或者方法)默认会生成一个以 Kt 追加文件名的 class 文件(譬如生成FileNameKt.class 文件,而 kotlin 中编写的类编译后与 java 类似,不存在这个特性。这个特性可以通过指定的注解打破,具体看后面总结。

var 与 val 及变量类型

与 java 不同的是,kotlin 的变量类型可以是类似下面这样的:

fun main() {
    //val 定义的变量值不能被修改,类似 java 的 final
    val a: Int = 1
    //kotlin 自动类型推断 b 为 Int 类型,kotlin 中的 Int 在 jvm 中表示为 java 的 int 类型,可以看注释得知
    val b = 2
    //var 定义变量,后面可以对变量值做修改
    var c: Int
    c = 34

    var d: Long = 10
    var e = 20
    //d = e     编译错误,无法自动类型转换,小类型赋值给大类型是不被允许的,java 可以
    d = e.toLong()  //合法
}

//Any 为 koltin 中的基类,类似 java 的 Object 类,但是又不一样
fun convert(arg: Any): String? {
    //is 类似 java 的 instanceof
    return if (arg is String) {
        arg.toUpperCase()
    } else {
        null
    }
}
复制代码

!!.?.?:asas?的区别

在 kotlin 中!!.?.都是用来判断空参数异常的。?.的含义是这个参数可以为空且程序继续运行下去;!!.的含义是这个参数如果为空则抛出异常。

?.在 kotlin 中的使用案例如下:

val testClass: TestClass? = null
testClass?.func() //如果有返回值则当testClass为空返回 null,反之返回正常返回值
println "done" //当testClass为空则继续执行这里
复制代码

上面代码对应的 java 实现逻辑如下:

TestClass testClass = null;
        
if (testClass != null) {
    testClass.func();
}
System.out.println("done")
复制代码

!!.在 kotlin 中的使用案例如下:

val testClass: TestClass? = null
testClass!!.func()
println "done" //当testClass为空则抛出异常,这里没机会执行
复制代码

上面代码对应的 java 实现逻辑如下:

TestClass testClass = null;
        
if (testClass != null) {
    testClass.func();
} else {
    throw new NullPointerException();
}
System.out.println("done")
复制代码

Elvis 表达式?:很像 java 的三目运算符,但又不一样,其使用案例如下:

//当 name 不为 null 则返回 null 给 str,当 name 为 null 则返回 default 给 str
val str = name ?: "default"
复制代码

as 被用做类型转换或者重取别名,当用作取别名时案例如下:

//包及导入特性小节演示过了
//指定导入别名
import cn.yan.test2.funcTest as selfFuncTest
复制代码

当用作类型转换时,as用于执行引用类型的显式类型转换,如果要转换的类型与指定的类型兼容则成功执行,如果类型不兼容则会抛出转换异常;当使用as?运算符进行类型转换,如果要转换的类型与指定的类型兼容则成功执行,如果类型不兼容则返回 null。在 kotlin 中,父类型是禁止转换为子类型的,请务必注意。

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】
open class Fruit
open class Apple(name: String) : Fruit()

val fruit = Fruit()
//抛出 java.lang.ClassCastException 异常
println(fruit as Apple)
//打印为 null,安全的类型转换
println(fruit as? Apple)
复制代码

kotlin 类型UnitNothingNothing?AnyAny?区分

kotlin 中 Unit 类型与 java 中 void 的功能基本相似。如下是 kotlin 源码中 Unit 的源码:

//Unit 类型是一个 object 对象类型
public object Unit {
	//toString 函数返回值
    override fun toString() = "kotlin.Unit"
}
复制代码

在 kotlin 中,当一个函数没有返回值时,我们用 Unit 来表示,而不是 null;大多数时候我们不需要显示地返回 Unit,或者声明一个函数的返回值是 Unit,编译器会自动推断它。跟 kotlin 的其他类型一样,Unit 的基类型是 Any。如果是一个可空的Unit?则父类型是Any?Any?是 Any 的超集,Any?是 kotlin 类型层次的最顶端。

fun main() {
    val unit = testUnit()
    println(unit is Unit) //true
    println(unit is Any) //true
    println(unit is Unit?) //true
    println(unit is Any?) //true

    val unitNullable = testUnitNullable()
    println(unitNullable is Unit) //false
    println(unitNullable is Any) //false
    println(unitNullable is Unit?) //true
    println(unitNullable is Any?) //true
}

fun testUnit(): Unit {}

fun testUnitNullable(): Unit? { return null }
复制代码

我们知道,在 java 中 void 不能是变量的类型,也不能作为值打印输出,java 提供了一个包装类 Void(void 的自动装箱类型),如果我们想让一个方法的返回类型永远是 null,则可以把返回类型定义为这个大写的 Void 类型。

java 中的这个 Void 类型对应 kotlin 的类型就是Nothing?,在 kotlin 中可以理解为不可达,即不返回或者返回不可访问类型,是一种约定,Nothing 的类源码如下:

//外界无法创建 Nothing 实例
public class Nothing private constructor()
复制代码

在 kotlin 中 throw 表达式的返回值就是 Nothing 类型的,表示了一种不可达(因为 throw 表达式执行完毕后就异常了,自然也就是不可达后续流程了),所以如果一个函数返回值是 Nothing,那么这个函数永远不会有返回值。譬如如下场景:

//因为 pick 永远不会反回值,而是直接抛出了异常,这个时候可以用 Nothing 作为 pick 函数的返回值
fun pick(index: Int): Nothing {
    throw Exception()
}
复制代码

所以 Unit 与 Nothing 的区别是,Unit 类型表达式计算结果返回值是 Unit,Nothing 类型表达式计算结果永远是不会反回的。

此外Nothing?可以只包含一个值 null,Nothing?唯一允许的值是 null,可被用作任何可空类型的空引用。譬如如下场景:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】
val test = null //编译器只能推断出类型为 Nothing?,空或者不可达类型
println(test is Nothing?) //true

val test1 = listOf(null) //编译器推断的类型为 List<Nothing?>
复制代码

kotlin 语法糖简化

与 java 不同的是,kotlin 的变量类型可以是类似下面这样的(其实 groovy 也有类似部分特性):

//有返回值原始写法
fun count(arg1: Int, arg2: Int): Int {
    return arg1 + arg2
}
//一行表达式简写
fun count2(arg1: Int, arg2: Int): Int = arg1 + arg2
//自动类型推断简写
fun count3(arg1: Int, arg2: Int) = arg1 + arg2

//无返回值原始写法
fun printCom(arg1: Int, arg2: Int): Unit {
    println("$arg1 --- $arg2")
}
//一行表达式简写
fun printCom1(arg1: Int, arg2: Int) = println("$arg1 --- $arg2")
//自动类型推断简写
fun printCom2(arg1: Int, arg2: Int) {
    println("$arg1 --- $arg2")
}
复制代码

流程控制

java 中的 if 语句仅仅只能当作语句使用,而 kotlin 中 if 即可以当语句使用,还可以当作表达式使用,譬如:

fun main() {
    var x = 10
    var y = 20
    var max: Int
    var min: Int
    //标准写法
    if (x > y) {
        min = y
        max = x
    } else {
        min = x
        max = y
    }
    //简写1,if 可以当作表达式
    min = if (x > y) y else x
    //简写2,if 可以当作表达式,如果 if 后面是代码块则代码块中最后一行返回值返回
    max = if (x > y) {
        println("x > y")
        x
    } else {
        println("x <= y")
        y
    }
}
复制代码

包及导入特性

我们知道,java 包名必须与磁盘目录一致,kotlin 包名没有这个限制,但是建议尽量一致,方便维护阅读。使用其他包中非 class 中定义的方法时可以直接 import 导入包,譬如:

//Test1.kt 文件使用 Test2.kt 文件
package cn.yan.test

import cn.yan.test2.funcTest

funcTest(1, 2)  //调用 test2 包中的 funcTest 方法
复制代码

还可以指定别名使用:

package cn.yan.test
//指定导入别名
import cn.yan.test2.funcTest as selfFuncTest

selfFuncTest(1, 2)  //调用 test2 包中的 funcTest 方法
复制代码

数组及遍历

kotlin 中 for 循环遍历语法与 java 有比较大的差异,具体如下:

fun main() {
    //IntArray 在 jvm 中表示的是 int[] 类型,可以看其注释得知
    var array: IntArray = intArrayOf(1, 2, 3)
    //遍历数组元素
    for (item in array) {
        println(item)
    }
    //遍历数组下标索引
    for (index in array.indices) {
        println("array[$index]=${array[index]}")
    }
    //遍历索引及元素
    for ((index1, value1) in array.withIndex()) {
        println("-array[$index1]=$value1")
    }
}
复制代码

when 关键字

此关键字相对 java 来说是 kotlin 新增的,可以当作 switch 来使用,其 case 没有 java 常量类型限制,可以是任意表达式,如下:

fun testWhen() {
    println(convertStr("he"))   //other
    println(convertStr("h"))    //hello

    var tmp = 12
    var ret = when(tmp) {
        in 0..20 -> "match 0..20"
        30, 31, 32 -> "31, 32, 30"
        33 -> "match 33"
        else -> "other"
    }
    println(ret)
}
//简写
fun convertStr(str: String): String {
    return when(str) {
        "h" -> "hello"
        "w" -> "word"
        else -> "other"
    }
}
//继续简写
fun convertStr1(str: String) = when(str) {
        "h" -> "hello"
        "w" -> "word"
        else -> "other"
    }
复制代码

range 区间

range 也是 kotlin 相对于 java 新增加的东西,比较好用,具体如下:

fun testRange() {
    val a = 5
    val b = 10
    //.. 左右都是闭合区间,.. 本质是 rangeTo 方法
    if (a in 2..b) {
        println("range matched")
    }
    if (a !in 2..b) {
        println("range not matched")
    }

    for (i in 2..10) {
        println("----$i")
    }
    for (i in 2.rangeTo(10)) {
        println("---x---$i")
    }
    //step 是中缀表达式,遍历2到10,每次跳2个index
    for (i in 2..10 step 2) {
        println("step----$i")
    }
    //逆序遍历
    for (i in 10 downTo 2 step 2) {
        println("downTo-step---$i")
    }
}
复制代码

range 也有他的坑,具体如下案例:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】
fun testRun() {
    //打印输出,range 0..2表示左右闭合区间
    if (1 in 0..2) {
        println("1 in the [0, 2]")
    }

    //遍历输出 1、2
    for (i in 1..2) {
        println("for loop $i")
    }

    //不输出任何东西,因为 range 默认按照升序查找,左区间是 4,右侧小于 3,等价于 for(int i=4; i<3; i++)
    for (i in 4..3) {
        println("for loop $i")
    }

    //遍历输出4、3
    for (i in 4 downTo 3) {
        println("for loop current $i")
    }

    //遍历跨间隔输出 6、4、2、0,step 参数必须是正数
    for (i in 6 downTo 0 step 2) {
        println("for loop step $i")
    }

    //遍历输出 0、1,左闭右开区间
    for (i in 0 until 2) {
        println("for loop until $i")
    }
}
复制代码

函数式编程

关于函数式编程,对于 java 来说有很多框架的选择,譬如 rxjava 等等,但是对于 kotlin 来说,其天生支持这一能力,譬如:

fun testList() {
    var array = listOf<String>("aa", "bbb", "c", "dddd", "eeeee")
    //如果bbb在列表则打印
    when {
        "bbb" in array -> println("bbb in the list")
    }
    //函数式编程实现:找出长度大于3,转换为大写,排序后逐个输出
    array.filter { it.length >= 3 }.map { it.toUpperCase() }.sorted().forEach { println(it) }
}
复制代码

解构声明

这个特性很像 ES6 的解构赋值,但又有自己的注意事项规则,譬如,kotlin 中默认能被解构的类必须是 data 类。如下:

//data数据类支持解构
data class RetValue(val statusCode: Int, val message: String)

//kotlin 提供的内置两值返回解构类型 Pair,也有三值的类型 Triple
fun test1(): Pair<Int, String> = Pair(404, "NotFound")

/**
 调用结果:
 code=200, msg=ok
 code1=404, msg1=NotFound
 */
fun testRun() {
    //解构声明
    val (code, msg) = RetValue(200, "ok")
    println("code=$code, msg=$msg")

    val (code1, msg1) = test1()
    println("code1=$code1, msg1=$msg1")
}
复制代码

解构声明与集合的应用实例:

/**
 调用结果:
 key=a, value=666
 key=b, value=999
 key=c, value=000
 ------------
 a=666 GO
 b=999 GO
 c=000 GO
 ------------
 a=666 FF
 b=999 FF
 c=000 FF
 */
fun testRun() {
    val map = mapOf<String, String>("a" to "666", "b" to "999", Pair("c", "000"))
    for ((key, value) in map) {
        println("key=$key, value=$value")
    }
    println("------------")
    map.mapValues { entry -> "${entry.value} GO" }.forEach { println(it) }
    println("------------")
    map.mapValues { (_, value) -> "$value FF" }.forEach { println(it) }
    //等价于完整类型声明写法
    //map.mapValues { (_, value): Map.Entry<String, String> -> "$value FF" }.forEach { println(it) }
    //map.mapValues { (_, value: String) -> "$value FF" }.forEach { println(it) }
}
复制代码

kotlin 可变集合与不可变集合

kotlin 严格区分可变集合与不可变集合。我们要清楚的认识到可变集合的只读视图与实际上真正的不可变集合。单说理论没啥意义,下面这个例子就从方方面面给出了对比结论:

/**
 调用结果:
 list=[aaa, bbb]
 readList=[aaa, bbb]
 listPhoto=[aaa, bbb]
 ---------------------
 list=[aaa, bbb, ccc]
 readList=[aaa, bbb, ccc]
 listPhoto=[aaa, bbb]
 ---------------------
 */
fun testRun() {
    //定义一个可变集合实例 MutableList 继承 List
    val list: MutableList<String> = mutableListOf("aaa", "bbb")
    //不可变集合(这里实质是上面可变集合 list 的一个只读视图)
    val readList: List<String> = list
    //快照(只是复制原集合中的元素,所以返回的集合可以确保不发生变化)
    val listPhoto = list.toList()
    //定义一个不可变集合实例
    val read = listOf<String>("666", "777")

    println("list=$list")
    println("readList=$readList")
    println("listPhoto=$listPhoto")
    println("---------------------")
    list.add("ccc")
    println("list=$list")
    println("readList=$readList")
    println("listPhoto=$listPhoto")
    println("---------------------")

    //编译报错 Unresolved reference: add
    //readList.add("ddd")

    val list1 = list //自动类型推断为可变集合
    list1.add("ddd")

    val list2 = readList //自动类型推断为不可变集合
    //编译报错 Unresolved reference: add
    //list2.add("ddd")

    //编译报错 Type mismatch. Required: MutableList<String> Found: List<String>
    //val list3: MutableList<String> = readList
}
复制代码

kotlin 异常

java 中的 try 是语句,而 kotlin 中的 try 是表达式,表达式的返回值是 try 里面最后一行语句或者 catch 里面最后一行语句。kotlin 中没有像 java 那样的 checked exception。

throw 在 java 中是语句,而 kotlin 中则是一个表达式,所以我们可以将 throw 作为 Elvis 表达式(?:)的一部分。throw 表达式返回一个特殊类型,是 Nothing 类型,这种类型没有值,仅仅用来标记永远没法触达的代码位置。

/**
 调用结果:
 java.lang.ArithmeticException: / by zero
 finally
 -1
 */
fun testRun() {
    val ret: Int = try {
        1/0
    } catch (e: ArithmeticException) {
        println(e)
        -1
    } finally {
        println("finally")
        2
    }
    println(ret)

    val str: String? = "abc"
    //?: 表示前面表达式不为空则返回前面表达式值,否则执行后面表达式,这时候其实已经抛出异常终止,不存在赋值给 str2 的过程
    val str2 = str ?: throw IllegalArgumentException("str is null.")
}

fun exceptionMsg(msg: String): Nothing {
    throw FileNotFoundException(msg)
}

fun exceptionMsg1(msg: String) {
    throw FileNotFoundException(msg)
}
复制代码

kotlin 注解

kotlin 的注解你基本可以完全等价于 java 的注解来理解,所有概念都一致(除过注解使用目标),只是对应类变了而已。最简单的注解声明使用及元注解使用案例:

//kotlin元注解修饰注解类
@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD,
        AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
//定义注解类
annotation class InjectQ

//使用注解
@InjectQ
class TestAn {
    @InjectQ
    lateinit var name: String

    fun test(@InjectQ arg: Int): Int {
        return (@InjectQ 1 + 1)
    }
}
复制代码

注解也可以拥有自己的构造方法,并且构造方法也可以接收参数,参数不能为可空类型(因为 jvm 不支持以 null 的形式存储注解属性值)。注解的构造方法参数允许的类型如下:

  • 与 java 原生类型所对应的类型(Int、Long 等)。
  • 字符串(String)。
  • class 类型。
  • 枚举类型。
  • 其他的注解。
  • 上述类型的数组类型。

如果某个注解被当作其他注解的参数,那么其名字就不需要以@开头,当作普通类看待就行。

//定义注解类
annotation class InjectQ
//定义带参数的注解类
annotation class InjectWWW (val str: String, val injectQ: InjectQ, val nums: IntArray)

//使用注解
@InjectQ
@InjectWWW("666", InjectQ(), [1, 2])
class TestAn
复制代码

如果需要将某个 class 作为注解的参数,请使用 kotlin 的 class(KClass.kt),语法为::class,kotlin 编译器会自动将其转换为 java class,这样 java 代码就能正常看到注解与参数了。

annotation class InjectQ(val v1: KClass<*>, val v2: KClass<out Any>)

//使用注解
@InjectQ(InjectQ::class, InjectQ::class)
class TestAn
复制代码

kotlin 的注解使用目标:在对类的属性或者是主构造方法的参数声明注解时,会存在多个 java 元素都可以通过对应的 kotlin 元素生成出来,因此,在所生成的 java 字节码中就会存在多个可能的位置来生成相应的注解,若想精确指定如何来生成注解,就可以使用注解的使用处目标方式来实现。

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】
annotation class InjectQ(val v1: Int)

//当我们使用了使用处目标方式使用注解就消除了二义性,明确了修饰参数的什么
class TestDD (@field: InjectQ(1) val arg1: String, //修饰arg1的属性
              @get: InjectQ(2) val arg2: Long, //修饰arg2的get方法
              @param: InjectQ(3) val arg3: Int) //修饰构造方法中arg3参数
复制代码

此外注解使用目标还能用于整个文件,譬如:

//文件 Test2.kt
@file: JvmName("TestRun") //指定编译后生成类名不再为默认的 Test2Kt.class,而是 TestRun.class
package cn.yan.test
......
复制代码

【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 blog.csdn.net/yanbober

文章分类
Android
文章标签