美文网首页Android
Java老鸟如何玩转Kotlin

Java老鸟如何玩转Kotlin

作者: 波澜步惊 | 来源:发表于2020-01-13 18:01 被阅读0次

    前言

    以一个java老鸟的角度,如何去看 kotlin。 Java源代码应该如何用Kotlin重构。 如何正确学习kotlin并且应用到实际开发中。本文将会探究。

    本文分两大块,重难点和潜规则。

    重难点:Kotlin中可以独立出来讲解的大块知识点。提供单独Demo。这部分大多数是Kotlin开创的新概念(相比于Java)。

    潜规则:Kotlin是谷歌用来替换Java的,它和java百分百完全兼容,但是实际上java转成kotlin之后,需要我们手动修改很多东西,甚至某些部分必须打散重构来达到最优编码。其中,kotlin的某些特性和java不同,甚至完全反转。这部分知识点比较零碎,单独Demo不方便提供,就以小例子的形式来写。

    正文大纲

    • 重难点

      • lambda以及操作符
      • 高阶函数以及操作符
      • Kotlin泛型
      • 集合操作
      • 协程
      • 操作符重载
    • 潜规则

      • Kotlin文件和类不存在一对一关系

      • 共生体

      • 继承

      • 修饰符

      • 空指针问题

    正文

    重难点

    lambda表达式

    lambda表达式是JDK1.8提出的,目的是为了改善Java中大量出现的只有一个方法的回调函数的累赘写法。这里不讨论Java的lambda. Kotlin中lambda已经融入代码核心,而不是像java一样是个注解+语法糖。

    基础:

    image.png

    一个lambda表达式如图:

    lambda 表达式的4个部分

    • 外围的{}大括号
    • 参数列表:x:Int,y:Int
    • 连接符 ->
    • 方法体 x+y

    举个栗子

    这是一个kotlin文件:

    /**
     * 比如说这样,我要给这个doFirst方法传一个执行过程,类型就是一个输入2个Int,输出一个Int的拉姆达表达式
     */
    fun calculate(p1: Int, p2: Int, event1: (Int, Int) -> Int, event2: (Int, Int) -> Int) {
        println("执行doFirst")
        println("执行event1 ${event1(p1, p2)}")
        println("执行event2 ${event2(p1, p2)}")
    }
    
    //测试lambda表达式
    fun main() {
        val sum = { x: Int, y: Int ->
            print("求和 ")
            x + y
        }
        val diff = { x: Int, y: Int ->
            print("求差 ")
            x - y
        }
        //kotlin里面,可以把拉姆达表示当作一个普通变量一样,去传递实参
        calculate(p1 = 1, p2 = 2, event1 = sum, event2 = diff)
    }
    

    定义了一个calculate函数, p1,p2 是Int,而event1和event2 则是 lambda表达式. 高级语言特性,一等函数公民:函数本身可以被当作普通参数一样去传递,并且调用。那么, kotlin的lambda内核是怎么样的?

    image-20200103175246666.png

    上图能够看出,

    1. calculate方法的后面2个参数,被编译成了 Function2 类型。
    2. 执行event1,event2,则是调用其invoke方法
    3. main函数中,出现了null.INSTANCE, 这里比较诡异,INSTANCE应该是用来获取实例的,但是为什么是null.INSTANCE

    而看了Function2的源码,简直亮瞎了我的钛合金狗眼:

    image.png

    Function.kt文件中:

    Function接口,Function2接口.....Function22接口。好了,不要质疑谷歌大佬的设计思路,反正大佬写的就是对的...这里用22个接口(至于为什么是22个,猜想是谷歌觉得不会有哪个脑子秀逗的开发者真的弄22个以上的参数挤在一起吧),表示kotlin开发中可能出现的lambda表达式参数列表。

    再举个栗子

    给一个Button 设置点击事件,kotlin的写法是:

    val btn = Button(this)
    val lis = View.OnClickListener { println("111") }
    btn.setOnClickListener(lis)
    

    或者:

    val btn = Button(this)
    btn.setOnClickListener { println("111") }
    

    setOnClickListener 方法的参数是 OnClickListener 接口:

    public interface OnClickListener {
        void onClick(View v);
    }
    

    类似这种符合lambda表达式特征的接口,都可以使用上述两种写法来大大简化代码量。

    最后一个栗子

    不得不提一句,lambda表达式有一个变形,那就是:当lambda表达式作为方法的最后一个参数时,可以lambda表达式放到小括号外面。而如果只有一个参数就是lambda表达式,那么括号可以省略

    这个非常重要,不了解这个,很多地方都会感觉很蛋疼。

    fun testLambda(s: String, block: () -> String) {
        println(s)
        block()
    }
    
    fun testLambda2(block: () -> String) {
        block()
    }
    
    fun main() {
        testLambda("第一个参数") {
            println("block函数体")
            "返回值"
        }
        testLambda2 {
            println("block函数体")
            "返回值"
        }
    }
    

    总结

    Kotlin中lambda表达式可以当作普通参数一样去传递,去赋值,去使用。


    高阶函数以及操作符

    上文提到,kotlin中lambda表达式已经融入了语言核心,而具体的体现就是高阶函数,把lambda表达式当作参数去使用. 这种将lambda表达式作为参数或者返回值的函数,就叫高阶函数。

    官方高阶函数

    Kotlin谷歌已经给我们封装了一些高阶函数。

    • run
    • with
    • apply
    • also
    • let
    • takeif 和 takeunless
    • repeat
    • lazy

    run函数详解

    代码如下(这里为了展示代码全貌,忽视androidStudio的代码优化提示):

    class A {
        val a = 1
        val b = "b"
    }
    fun testRun() {
        //run方法有两种用法,一个是不依赖对象,也就是作为全局函数
        run<String> {//我可以规定返回值的类型
            println("我是全局函数")
            "返回值"
        }
        val a = A()
        //另一种则是 依赖对象
        a.run<A,String> {//这里同样可以规定返回值的类型
            println(this.a)
            println(this.b)
            "返回值"
        }
    }
    fun main() {
        testRun()
    }
    

    如上所示:

    run函数分为两类

    • 不依赖对象的全局函数。

    • 依赖对象的"类似"扩展函数。

      两者都可以规定返回值类型(精通泛型的话,这里应该不难理解,泛型下一节详解)。

    阅读源码:

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

    run函数被重载,参数有所不同

    • 前者 参数类型为 ()->R ,返回值为 R ,函数体内执行block(),并且返回执行结果
    • 后者 参数类型为 T.()->R ,返回值为R , T.() 明显是依赖 T类的(貌似T的扩展函数),返回值依然是R,执行完之后返回结果。
    • 并且,可以发现 两者都是内联函数 inline (执行代码时不进行方法的压栈出栈,而是类似于直接在目标处执行代码段)

    所以,前者不需要依赖对象,后者必须依赖对象(因为它是T类的"扩展函数")

    使用场景

    根据run方法的特性,无论是不是依赖对象,它都是封装了一段代码,而且还是inline的。所以:

    • 如果你不想把一段代码独立成方法,又不想让它们看上去这么凌乱,最重要的是告诉后来的开发者 这段代码是一个整体,不要随便给我在里面插代码,或者拆分。那就用run方法,把他们整合到一个作用域中。

      run {
          println("这是一段代码逻辑很相近的代码段")
          println("但是我不想把他独立成一个方法")
          println("又担心别人会随便改")
          println("所以用run方法把他们放在同一个作用域中")
          println("小组中其他人看到这段,就知道不要把无关代码插进来")
      }
      
    • 更神奇的是,这个run函数是有返回值的,参数返回值可以利用起来:

      fun testRun2(param1: String, param2: String, param3: String) {
          //我想让这几个参数都不为空,如果检查是空,就不执行方法主体
          val checkRes: Boolean = run<Boolean> {
              when {
                  param1.isNullOrEmpty() -> {
                      false
                  }
                  param2.isNullOrEmpty() -> {
                      false
                  }
                  param3.isNullOrEmpty() -> {
                      false
                  }
                  else -> true
              }
          }
      
          if (checkRes){
              println("参数检查完毕,现在可以继续接下来的操作了")
          }else{
              println("参数检查不通过,不执行主体代码")
          }
      }
      fun main() {
        testRun2("1", "2", "")
      }
      

    main运行结果:

    参数检查完毕,现在可以继续接下来的操作了

    小结论

    run方法在整合小段代码的功效上,还是很实用的

    其他高阶函数

    上面列举出来的这些系统高阶函数原理上都差不多,只是使用场景有区别,因此除了run之外,其他的不再详解,而只说明使用场景。

    apply

    和run只有一个区别,run是返回block()执行之后的返回值,而,apply 则是返回this,因此 apply必须依赖对象。而由于返回了this,因此可以连续apply调用。

    fun testApply() {
        val a = A()
        a.apply {
            println("如果一个对象要对它进行多个阶段的处理")
        }.apply {
            println("那么多个阶段都挤在一起则容易混淆,")
        }.apply {
            println("此时可以用apply将每一个阶段分开摆放")
        }.apply {
            println("让程序代码更加优雅")
        }
    }
    
    fun main() {
        testApply()
    }
    
    with

    下面的代码应该都很眼熟,Glide图片加载框架的用法,with(context)然后链式调用

    Glide.with(this).load(image).asGif().into(mImageView);
    

    Kotlin中的with貌似把这一写法发扬光大了(只是类比,不用深究),场景如下:

    class A {
        val a = 1
        val b = "b"
    
        fun showA() {
            println("$a")
        }
    
        fun showB() {
            println("$a $b")
        }
    }
    
    fun testWith() {
        val a = A()
        with(a) {
            println("作用域中可以直接引用创建出的a对象")
            this.a
            this.b
            this.showA()
            this
        }.showB()
    }
    
    fun main() {
        testWith()
    }
    

    细节

    1. with(a){} 大括号内的作用域里面,可以直接使用 当前a对象的引用,可以this.xxx 也可以 a.xxx
    2. with(a){} 大括号作用域内的最后一行是 返回值,如果我返回this,那么with结束之后,我可以继续 调用a的方法
    also

    also和with一样,必须依赖对象,返回值为this。因此它也支持链式调用,它和apply的区别是:

    image.png

    apply的block,没有参数,但是 also 则将this作为了参数。这么做造成的差异是:

    作用域 { } 内调用当前对象的方式不同。

    class A {
        val a = 1
        val b = "b"
    
        fun showA() {
            println("$a")
        }
    
        fun showB() {
            println("$a $b")
        }
    }
    fun testApply() {
        A().apply {
            this.showA()
            println("=======")
        }.showB()
    }
    
    fun testAlso() {
        A().also {
            it.showA()
            println("=======")
        }.showB()
    }
    

    apply 必须用this.xxx, also则用 it.xxx.

    let

    类比到run:

    public inline fun <T, R> T.run(block: T.() -> R): R {
        return block()
    }
    public inline fun <T, R> T.let(block: (T) -> R): R {
        return block(this)
    }
    

    只有一个区别:run的block执行不需要参数,而let 的block执行时需要传入this。

    造成差异为:

    A().run {
        println("最后一行为返回值")
        this
    }.showA()
    
    A().let {
        println("最后一行为返回值")
        it
    }.showA()
    

    run{} 作用域中 只能通过this.xxx操作当前对象,let 则用 it.xxx

    takeif 和 takeunless

    这两个作用相反,并且他们必须依赖对象。看源码:

    public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
        return if (predicate(this)) this else null
    }
    public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
        return if (!predicate(this)) this else null
    }
    

    predicate 是 (T)->Boolean 类型的lambda表达式,表示断言判断,如果判断为true,则返回自身,否则返回空

    class A {
        val a = 0
        val b = "b"
    
        fun showA() {
            println("$a")
        }
    
        fun showB() {
            println("$a $b")
        }
    }
    
    fun testTakeIfAndTakeUnless() {
        println("test takeIf")
        A().takeIf { it.a > 0 }?.showB()
        println("==========")
        println("test takeUnless")
        A().takeUnless { it.a > 0 }?.showB()
    }
    
    fun main() {
        testTakeIfAndTakeUnless()
    }
    

    执行结果:

    test takeIf
    ==========
    test takeUnless
    0 b
    

    takeIf / takeUnless适用于将条件判断的代码包在一个作用域{}中,然后 用 ?.xxxx的安全调用符 来 执行对象操作。

    repeat

    repeat是 多次循环的傻瓜版写法。

    fun testRepeat() {
        repeat(10) {
            print("$it ")
        }
    }
    
    fun main() {
        testRepeat()
    }
    

    执行结果:

    0 1 2 3 4 5 6 7 8 9 
    
    lazy

    lazy的作用是: 延迟初始化val定义的常量。

    class B {
        val i: Int by lazy {
            println("执行i初始化")
            20
        }
    
        init {
            println("构造函数执行")
        }
    }
    

    如果只是初始化B对象,却没有使用到变量i, 那么延迟初始化不会执行。

    fun main() {
        B()
    }
    

    执行结果:

    构造函数执行
    

    而如果使用到了变量i,才会去在调用之前初始化:

    fun main() {
        println("i 变量的值是:" + B().i)
    }
    

    执行结果:

    构造函数执行
    执行i初始化
    i 变量的值是:20
    

    总结

    Kotlin官方提供的高阶函数,run,apply,also,let,with等,旨在使用{}作用域,将联系紧密的代码封在一个作用域内,让一个方法内部显得 有条不紊,阅读观感更好,可维护性更高,代码更优雅。上述,除了lazy之外,所有的 函数都在Standart.kt文件内部。


    Kotlin泛型

    类型擦除

    我们在编码java的时候,写一个泛型类,可能是这样的

    class Plate<T>{
        T t;
     
        Plate(T t){
            this.t = t;
        }
        
        T get(){
            return t;
        }
        
        void set(T t){
            this.t =
                t;
        }
    }
    

    以上是java代码。

    Java泛型是伪泛型,在编译之后,所有的泛型写法都会被移除,而会用实际的类型去替换。

    mian函数运行的时候,<T> 被移除。而原来的T,就变成了Object。

    所以,Plate的字节码反编译过来就应该是

    class Plate{
        Object t;
     
        Plate(Object t){
            this.t = t;
        }
        
        Object get(){
            return t;
        }
        
        void set(Object t){
            this.t = t;
        }
    }
    

    那么既然运行的时候,泛型限制全都没有了。那么怎么保证 泛型的作用呢?

    答案:编码的时候,编译器帮我们进行校验。

    strList.add(123);//报错
    

    PECS 法则 和 上下边界的问题

    public class Panzi<T> {
        T mT;
        public Panzi(T t) { mT = t;}
        public T get() { return mT; }
        public void set(T t) {  mT = t; }
    }
    
    class Fruit {}
    class Banana extends Fruit {}
    class Apple extends Fruit {}
    
     Panzi<Apple> applePanzi = new Panzi<>(new Apple());
     Panzi<Fruit> fruitPanzi = new Panzi<>(new Fruit());
     fruitPanzi = applePanzi;
    

    虽然 Apple和Fruit是父子继承关系。但是Panzi<Apple>Panzi<Fruit>是半毛钱关系都没有,如果你想fruitPanzi = applePanzi ,把后者赋值给前者,是会报编译错误的。

    如果想让装水果的盘子和 装 苹果的盘子发生一点关系,能够后者赋值给前者.

    Panzi<Apple> applePanzi = new Panzi<>(new Apple());
    Panzi<? extends Fruit> fruitPanzi = new Panzi<>(new Fruit());
    fruitPanzi = applePanzi;
    

    那就必须使用到上下边界的关键字 extends

    extends 在泛型中表示指定上界,也就是说,实际类型都必须在Fruit之下(包括Fruit自己)。那么既然apple也是Fruit的子类,那么赋值就可以做到。

    • PECS法则

      刚才说到了上边界 extends。而下边界是 super关键字

      Panzi<? extends Fruit> extendsFruit = new Panzi<>(new Apple());
      Panzi<? super Fruit> superFruit = new Panzi<>(new Fruit());
      

      super关键字,在泛型中表示定义下界,实际类型必须在Fruit之上,同时也在Object之下(包括Fruit和Object)

      所以会出现这么一个情况:

      Panzi<? extends Fruit> extendsFruit = new Panzi<>(new Apple());
      Panzi<? super Fruit> superFruit = new Panzi<>(new Object());
      

      我们有这么两个Panzi,前者是 Fruit作为泛型上界,一个是Fruit作为下界。

      现在,我们从Panzi中去调用get/set方法。会发现。

      PE:

       extendsFruit.set(new Apple()); // 编译报错!
       Fruit fruit2 = extendsFruit.get();// 编译正常
      

      为何?因为 Fruit作为上界,我get出来的类型可以确定一定是Fruit类型的。但是我set进去的时候,JVM无法判定实际类型(因为泛型被擦除,JVM只人为set(Object t) 的参数是一个Object),JVM要求是Object,但是你却给了一个Apple,编译器无法处理。所以干脆 java的泛型,? extends 定义了上界,只允许get,不允许set。这就是PECS中的PE,意思就是 Pruducer Extends ,生产者 Extends,只取不存。

      相对应:

      CS: 则是 Cunsumer super 消费者只存不取。

      Object object = superFruit.get(); //get,get出来虽然不报错,但是没有任何意义。因为不能确定类型,只知道是一个Object,无法调用API
      superFruit.set(new Fruit());// 但是set进去的时候,可以确定一定是一个Fruit的
      

    这就是java泛型的 PECS法则.

    kotlin泛型使用实例

    java泛型里面比较纠结的难点就是类型擦除和PECS法则了。

    那么kotlin泛型,原理上和java泛型和没有区别。只是写法上有了区别。

    open class Fruit
    
    class Apple : Fruit()
    
    class Banana : Fruit()
    
    class Panzi<T>(t: T) {
    
        var t: T = t
    
        fun get(): T {
            return t
        }
    
        fun set(t: T) {
            this.t = t
        }
    }
    
        fun test1() {
            // 试试能不能相互赋值
            var fruitPanzi: Panzi<Fruit> = Panzi(Fruit()) //一个水果盘子
            var applePanzi: Panzi<Apple> = Panzi(Apple()) //一个苹果盘子
            //试试相互赋值
            //        fruitPanzi = applePanzi  // 编译报错
            //        applePanzi = fruitPanzi  // 编译报错
            //双方完全是不相干的类,不能相互赋值 ,
        }
    
        /**
         * 加边界之后再赋值
         */
        fun test2() {
            //如果你非要认为苹果盘子归属于水果盘子,那么可以这样
            var fruitPanzi2: Panzi<out Fruit> = Panzi(Fruit()) //一个水果盘子
            var applePanzi2: Panzi<Apple> = Panzi(Apple()) //一个苹果盘子
    
            fruitPanzi2 = applePanzi2  //那么这就是out决定泛型上边界的案例
        }
    
        /**
         * PECS法则,OUT表示 ? extends 决定上界,上界生产者只取不存
         */
        fun test3() {
            //看一下get set方法
            // 决定上界之后的泛型,只取不存
            var fruitPanzi2: Panzi<out Fruit> = Panzi(Fruit())
            fruitPanzi2.get()
            //        fruitPanzi2.set(Apple())  // 这里编译报错,和java泛型的表现一样
        }
    
        /**
         * PECS法则,IN表示 ? super 决定下界,下界消费者,只存不取
         */
        fun test4() {
            //试试泛型下界 in
            var fruitPanzi: Panzi<in Fruit> = Panzi(Fruit())
            fruitPanzi.set(Fruit())//可以set,但是看看get
            val get = fruitPanzi.get()//不会报错,get出来的类型就完全不能确定了,只知道是 顶级类Any? 的子类,获得它也没有意义
    
        }
    

    集合操作

    Kotlin的集合,并没有重新开创一套规则,它的底层依然是java的Collection。Kotlin提供了可变集合和不可变集合的接口。

    • 不可变集合:ListSetMap (内部元素不可以 增减 或者 修改,在定义的时候就已经将容量和内部元素定死)

    • 不可变集合: MutableList , MutableSet,MutableMap(声明的时候可以随意指定初始值,后续可以随意增删和修改内部元素)

    集合操作分为:对象的创建api的调用

    对象的创建

    方式有多种,以不可变集合 List 为例,kotlin的List底层和Java的List一致,底层数据结构是数组。

    静态指定元素值

    fun main() {
        val listOf = listOf<String>("str1", "str2", "str3")
        listOf.forEach { print("$it ") }
    }
    

    执行结果:

    str1 str2 str3
    

    通过动态创建过程来指定元素值

    fun main() {
    
        val list = List(3) {
            "str$it"
        }
        list.forEach { print("$it ") }
    }
    

    执行结果:

    str0 str1 str2 
    

    api的调用

    对象已经创建,我们要利用kotlin提供的方法来完成业务代码。

    一级难度api(all,any,count,find,groupBy)
    fun testCollectionFun() {
        val ages = listOf<Int>(1, 2, 3, 4, 5, 6, 7, 100, 200)
        //那么是不是所有的元素都大于10
    
        ages.apply { print("all:") }.all { it > 10 }.apply { println(this) } //结果是false
    
        //是不是存在任意一个元素大于10
        ages.apply { print("any:") }.any { it > 10 }.apply { println(this) }
    
        // 符合指定条件的元素个数
        ages.apply { print("count:") }.count { it < 10 }.apply { println(this) }
    
        //找到第一个符合条件的元素
        ages.apply { print("find:") }.find { it > 10 }.apply { println(this) }
    
        // 按照条件进行分组
        val groupBy = ages.apply { print("groupBy:") }.groupBy { it > 10 }
        groupBy[false].apply { println(this) }
        groupBy[true].apply { println(this) }
    
    }
    
    fun main() {
        testCollectionFun()
    }
    

    针对数组元素的简单判断,上述提供了简明的示例代码,用List为例,至于Set和Map类似。可以自主去推断写法。

    执行结果:

    all:false
    any:true
    count:7
    find:100
    groupBy:[1, 2, 3, 4, 5, 6, 7]
    [100, 200]
    
    **二级难度api **(filter,map,flatMap,flatten)
    • filter和map
    fun testCollectionFun2() {
        //二级难度api
        val ages = listOf<Int>(1, 2, 3, 4, 5, 6, 7, 100, 200)
        // 只保留大于10的元素,并返回一个新数组
        ages.filter { it > 10 }.apply { println(this) }
        //遍历List的所有元素,根据条件返回值,创建新的元素内容并放到新List中返回出来
        ages.map { if (it > 10) "大于10" else "小于等于10" }.apply { println(this) }
    }
    fun main() {
        testCollectionFun2()
    }
    

    执行结果:

    [100, 200]
    [小于等于10, 小于等于10, 小于等于10, 小于等于10, 小于等于10, 小于等于10, 小于等于10, 大于10, 大于10]
    
    • flatMap,因为稍复杂
    // 比如一个学生,作为一个实体
    class Student(name: String, math: Int, chinese: Int) {
        val name: String = name
        val score = Score(math, chinese)
    }
    
    //学生的成绩分数作为一个主体
    class Score(math: Int, chinese: Int) {
        val math: Int = math
        val chinese: Int = chinese
    }
    
    fun testFlatMap() {
        val students = listOf(
                Student("zero", 100, 80),
                Student("alvin", 70, 70),
                Student("lance", 90, 60)
        )
        //我只想统计所有的数学成绩的总分和平均分,怎么办
        students.flatMap { listOf(it.score.math) }.apply {
            var total = 0
            this.forEach {
                total += it
            }
            println("数学成绩的总分是:$total  平均分是:${total / this.size}")
        }
    }
    
    fun main() {
        testFlatMap()
    }
    

    执行结果:

    数学成绩的总分是:260  平均分是:86
    

    当面对复杂数据结构时,我们想要提炼出其中的某一层数据,并不关心其他无关字段。就适合使用 flatMap 进行扁平化提炼

    • flatten

    意味:"平铺"

    fun testFlatten(){
        val list = listOf(listOf("Str1","Str3"), listOf("Str4","Str2"))
        val listsOfList = list.flatten()
        println("平铺之前:$list \n平铺之后$listsOfList")
    }
    
    fun main() {
        testFlatten()
    }
    

    执行结果:

    平铺之前:[[Str1, Str3], [Str4, Str2]] 
    平铺之后[Str1, Str3, Str4, Str2]
    

    在存在List嵌套的情况下,flatten可以把复合式的数据结构 变成 扁平式的。它和flatMap不同,flatMap适合在复杂数据结构中在指定的层级形成一个集合,方便统计和计算。而flatten则更适用于类型相似的集合嵌套扁平化操作。适用场景还是有差别的。


    协程

    想了很久,关于协程的内容,在官网上确实有很多内容,基础知识概念,基本使用,以及 流操作,通道,异常处理,并发处理等,不方便在这里展开。具体的参照:Kotlin中文网

    image.png

    这里具体去学习。本章节,只总结一下近期查阅资料并经本人验证的知识点。

    概念

    英文 coroutines : /,kəuru:'ti:n/ 意: 协同程序。 简称协程。

    Kotlin提出协程概念,是为了简化异步编程,让开发者更容易控制函数的执行流程。

    协程和线程的联系和区别

    在操作系统OS中,进程资源分配的最小单位线程任务调度的最小单位。而协程则是处在线程内部的“微线程”,或者说轻量级线程。 由于线程在OS中是稀缺资源,所有OS平台的线程数量都是有上限的,平时编程我们会用线程池来管理线程数量,熟悉线程池的同学应该知道,线程池管理线程,无论是核心线程还是非核心线程,都不会随意去创建,除非迫不得已。

    线程解决异步问题

    • 多线程同步编程可以通过加锁解决数据的线程安全问题,但是加锁会降低程序执行效率,并且锁多了,会有死锁隐患
    • 线程的状态转换完全由内核控制,程序员开发者无法干涉
    • 线程的是稀缺资源,不能随意创建,使用线程解决异步问题,线程的初始化,上下文切换(CPU轮转),线程状态切换(sleep,yield...), 变量加锁操作(synchronized关键字),都会使得线程的使用代价比较大

    协程解决异步问题

    • 协程是运行在线程之上的优化产物,或称“微线程”。协程依赖线程运行,复杂的底层逻辑被封装在库内,使用时无需关心所处线程状态
    • 使用协程,开发者可以自己控制协程的状态(suspend挂起,resume恢复),而不会像线程那样依赖底层调度,时间片争夺。
    • 一个线程可以跑多个协程,一个协程也可以分段在多个线程上执行
    • 协程 是 非阻塞的,当前协程挂起之后,所在线程资源并不会浪费,它会去执行其他协程(如果有的话)
    • 协程 相对于线程这种OS中的稀缺资源,它是极其轻量级的,就算你开一百万个协程,对于系统的压力也不会像大量线程那样大(别说一百万个,linux系统的线程数量上线是1000,超过这个值系统就无法正常运行).
    • 总之一句话 : 协程的出现,让程序开发者对程序逻辑的掌控提升到了一个新的境界,想象一下,一个函数正在执行,你想让他在某个时刻暂停,然后在另一个时刻继续。利用线程恐怕很难做到。协程中,轻而易举。

    基本使用

    module级别的build.gradle 中 引入库依赖,推荐使用最新版(目前稳定版是1.3.3)

    dependencies {
        //...
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3" // 如果你需要用到协程调度器Dispatchers的话,必须加上这个依赖
    }
    
    协程的创建

    创建协程有多种方式,全局/独立,同步/异步,并且可以指定 "协程调度器"

    • runBlocking

      fun main() {
          runBlocking {
              println("这是runBlocking协程...")
          }
      }
      
    • launch

      fun main() {
          runBlocking {
              println("这是runBlocking协程...")
              launch {
                  println("这是runBlocking内部的runBlocking协程...")
                  delay(2000)
                  println("延迟了2000MS之后,再打印")
              }
          }
      }
      
    • GlobalScope.launch

      fun main() {
          println("协程,相对于主线程来说,都是异步的,也就是说,你在这里插入协程逻辑,主线程的逻辑并不会被阻塞")
          GlobalScope.launch {
              delay(3000)
              println("GlobalScope.launch 创建协程 ")
          }
          println("主线程继续")
          Thread.sleep(5000)
      }
      
    • Global.async

      fun main() {
          runBlocking {
              val async: Deferred<String> = GlobalScope.async {
                  println("这是一个异步协程,他将返回一个Deferred")
                  delay(2000)
                  "异步任务返回值"
              }
              println("主线程继续:" + async.await())
          }
      
          Thread.sleep(5000)
      }
      
    “骚操作”

    关心协程的人一般都会十分关注它到底能给我们异步编程带来怎样的便利。这里总结几个不用协程实现起来很麻烦的骚操作

    • 如果有一个函数,它的返回值需要等到多个耗时的异步任务都执行完毕返回之后,组合所有任务的返回值作为 最终返回值

      fun test6(): String = runBlocking {
          var finalRes = ""
          coroutineScope {
              launch {
                  delay(1000)
                  finalRes = finalRes.plus("1")
              }
              launch {
                  delay(2000)
                  finalRes = finalRes.plus("2")
              }
      
              launch {
                  delay(3000)
                  finalRes = finalRes.plus("3")
              }
          }
          finalRes
      }
      
      fun main() {
          val test6 = test6()
          println("最终返回值是: $test6")
      }
      

      最终返回结果为(延迟3秒之后打印):

      最终返回值是: 123
      
    • 如果有一个函数,需要顺序执行多个网络请求,并且后一个请求依赖前一个请求的执行结果

      import kotlinx.coroutines.*
      
      suspend fun getToken(): String {
          for (i in 0..10) {
              println("异步请求正在执行:getToken :$i")
              delay(100)
          }
          return "ask"
      }
      
      suspend fun getResponse(token: String): String {
          for (i in 0..10) {
              println("异步请求正在执行:getResponse :$token $i")
              delay(100)
          }
      
          return "response"
      }
      
      fun setText(response: String) {
          println("setText 执行,时间:  ${System.currentTimeMillis()}")
      }
      
      fun main() {
          GlobalScope.launch(Dispatchers.Unconfined) {
              var token = GlobalScope.async(Dispatchers.Unconfined) {
                  return@async getToken()
              }.await() // 创建异步任务,并且 阻塞执行 await 是阻塞执行取得结果
      
              var response = GlobalScope.async(Dispatchers.Unconfined) {
                  return@async getResponse(token)
              }.await() // 创建异步任务,并且立即执行
      
              setText(response)
          }
      
          Thread.sleep(20000)
      }
      

      执行结果:

      异步请求正在执行:getToken :0
      异步请求正在执行:getToken :1
      异步请求正在执行:getToken :2
      异步请求正在执行:getToken :3
      异步请求正在执行:getToken :4
      异步请求正在执行:getToken :5
      异步请求正在执行:getToken :6
      异步请求正在执行:getToken :7
      异步请求正在执行:getToken :8
      异步请求正在执行:getToken :9
      异步请求正在执行:getToken :10
      异步请求正在执行:getResponse :ask 0
      异步请求正在执行:getResponse :ask 1
      异步请求正在执行:getResponse :ask 2
      异步请求正在执行:getResponse :ask 3
      异步请求正在执行:getResponse :ask 4
      异步请求正在执行:getResponse :ask 5
      异步请求正在执行:getResponse :ask 6
      异步请求正在执行:getResponse :ask 7
      异步请求正在执行:getResponse :ask 8
      异步请求正在执行:getResponse :ask 9
      异步请求正在执行:getResponse :ask 10
      setText 执行,时间:  1578904290520
      
    • 当前正在执行一项异步任务,但是你突然不想要它执行了,随时可以取消

      fun main() {
          // 协程任务
          val job = GlobalScope.launch(Dispatchers.IO) {
              for (i in 0..100){// 每次挂起100MS,100次也就是10秒
                  println("协程正在执行 $i")
                  delay(100)
              }
          }
      
          // 但是我在1秒之后就取消协程
          Thread.sleep(1000)
          job?.cancel()
          println( "btn_right 结束协程")
      }
      

      执行结果(本该执行100轮的打印,只持续了10轮):

      协程正在执行 0
      协程正在执行 1
      协程正在执行 2
      协程正在执行 3
      协程正在执行 4
      协程正在执行 5
      协程正在执行 6
      协程正在执行 7
      协程正在执行 8
      协程正在执行 9
      btn_right 结束协程
      
      Process finished with exit code 0
      
    • 如果你想让一个任务最多执行3秒,超过3秒则自动取消

      import kotlinx.coroutines.*
      
      
      fun main() = runBlocking {
          println("限时任务中结果是:" + getResFromTimeoutTask())
      }
      
      suspend fun getResFromTimeoutTask(): String? {
          // 忘了,它会保证内部的协程代码都执行完毕,所以不能这么写
          return withTimeoutOrNull(1300) {
              for (i in 0..10) {
                  println("I'm sleeping $i ...")
                  delay(500)
              }
              "执行结束"
          }
      }
      

      执行结果

      I'm sleeping 0 ...
      I'm sleeping 1 ...
      I'm sleeping 2 ...
      限时任务中结果是:null
      
      Process finished with exit code 0
      
    总结

    协程作为kotlin 区别于java的新概念,它的出现是为了解决java不好解决的问题,比如层层回调导致代码臃肿,比如 异步任务执行流程不好操控等。本章节篇幅有限,无法展开说明,但是对于新手而言,看完本章应该能对协程的作用有一个大概的认知。本人也是初步研究,后续有更深入的了解之后,再进行专文讲解吧。


    操作符重载

    概念

    说人话,像是一元操作符 ++自加,二元操作符 +相加 ,默认只支持数字类型,比如Int. 但是通过操作符的重载,我们可以让任意类 都能 ++自加,且返回一个想要的对象。操作符执行的逻辑,完全看我们如何去设计。

    分类

    按元素级别

    • 一元

      表达式 对应函数
      +a a.unaryPlus()
      -a a.unaryMinus()
      !a a.not()
      a++ a.inc()
      a-- a.dec()
    • 二元

      表达式 对应函数
      a+b a.plus(b)
      a-b a.minus(b)
      a*b a.times(b)
      a/b a.div(b)
      a%b a.rem(b)
      a..b a.range(b)
      a in b b.contains(a)
      a !in b !b.contains(a)
      a[i] a.get(i)
      a[i,j] a.get(i,j)
      a[i_1,...,i_n] a.get(i_1,...,i_n)
      a[i]=b a.set(i,b)
      a[i,j]=b a.set(i,j,b)
      a[i_1,...,i_n]=b a.set(i_1,...,i_j,b)
      a() a.invoke()
      a(i) a.invoke(i)
      a(i,j) a.invoke(i,j)
      a(i_1,...,i_n) a.invoke(i_1,...,i_n)
      a+=b a.plusAssign(b)
      a-=b a.minusAssign(b)
      a*=b a.timesAssign(b)
      a/=b a.divAssign(b)
      a%=b a.modAssign(b)
      a > b a.compareTo(b)>0
      a < b a.compareTo(b)<0
      a>=b a.compareTo(b)>=0
      a<=b a.compareTo(b)<=0

    按实现方式

    • 成员函数

    • 扩展函数

    栗子

    看到上面的一大堆,肯定有点懵,看个例子解决疑问。上面我用两种维度来对操作符重载进行了分类,那么,先试试:成员函数的方式来重载一个一元操作符

    class A(i: Int, j: Int) {
        var i: Int = i
        var j: Int = j
        /**
         * 重载++操作
         */
        operator fun inc(): A {
            return A(i++, j++)
        }
        override fun toString(): String {
            return "[i=$i , j=$j]"
        }
    }
    

    如上代码,注意看:

     operator fun inc(): A {
            return A(i++, j++)
     }
    

    Kotlin的操作符重载和 c++,dart语言内的操作符重载写法完全不同,它不再是直接把操作符放到了 重写的过程中,而是每一种支持重载的操作符都有一个对应的 函数名

    正如:上表格中的 a++ 操作符,对应的函数就是 a.inc()

    调用的时候:

    fun main() {
        var a = A(1, 2)
        println("a:$a")
        println("a++:${a++}")
    }
    

    打印结果:

    a:[i=1 , j=2]
    a++:[i=2 , j=3]
    

    再看一个二元运算符重载的栗子,这次我们不用成员函数,而是用扩展函数:

    class A(i: Int, j: Int) {
        var i: Int = i
        var j: Int = j
        override fun toString(): String {
            return "[i=$i , j=$j]"
        }
    }
    /**
    * 重载A类的 x+y 操作
    */
    operator fun A.plus(a: A): A {
        return A(this.i + a.i, this.j + a.j)
    }
    fun main() {
        val x = A(1,1)
        val y = A(2,2)
        println(x+y)
    }
    

    这里演示的是 A类的 x+y 操作符重载。细节应该不用多说。

    打印结果:

    [i=3 , j=3]
    

    再来一个较为复杂的栗子, 重载 a[i]

    /**
     * 比如,B类中有一个成员,list,我想重载操作符,直接取到list中的元素
     */
    class B {
        val list: MutableList<Int> = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
    }
    //a[i]
    operator fun B.get(i: Int): Int {
        return list[i]
    }
    fun main() {
        val b = B()
        println("${b[2]}")
    }
    

    打印结果:

    3
    

    最后一个栗子:a > b,对应函数为:a.compare(b)

    /**
     * 学生class
     */
    data class Student(val math: Int = 0, val chinese: Int = 0, val english: Int = 0)
    
    fun Student.toString():String{
        return "[math:${math} chinese:${chinese} english:${english}]"
    }
    fun Student.totalScore(): Int {
        return math + chinese + english
    }
    
    
    /**
     * 比如,我们要直接比较两个学生的总分
     */
    operator fun Student.compareTo(s: Student): Int {
        return this.totalScore() - s.totalScore()//比较2个学生的总分
    }
    
    fun main() {
        val s1 = Student(math = 50, chinese = 90, english = 100)
        val s2 = Student(math = 80, chinese = 70, english = 60)
    
        println("s1:${s1}")
        println("s2:${s2}")
        //比如存在这两个学生,我要知道他们的总分谁高谁低
        println("学生s1,s2的总分:${if(s1 > s2) "s1比较高" else "s2比较高" }")
    
    }
    

    打印结果:

    s1:Student(math=50, chinese=90, english=100)
    s2:Student(math=80, chinese=70, english=60)
    学生s1,s2的总分:s1比较高
    

    总结

    通过以上几个栗子,应该能看出,kotlin的操作符重载,编码和使用都十分简单。重载之前需要确定两件事

    • 根据业务需求,确定要重载的是哪一个操作符,虽然操作符的最终执行逻辑完全由我们自定义,但是我们重载操作符的目的是为了 让使用者更简单的理解业务代码,所以,要选择原本意义更加符合业务需求的操作符。Kotlin支持的操作符在上述表格中都列举出来了,支持大部分一元二元操作符,但是二元中不支持===的重载,不支持三元操作符bool?a:b这种 。
    • 确定重载函数的 入参类型个数,以及返回值类型,并且编写操作符的执行逻辑。

    潜规则

    从Java转到kotlin,基本上都会存在java代码与kotlin共存的问题。而且为了快速转型,可能会直接把java类转成kotlin类,而这个过程中,涉及到java和kotlin的交互,往往会磕磕碰碰,以下总结了一部分 java kotlin交互方面的问题.

    Kotlin文件和类不存在一对一关系

    kotlin的文件,可以和类名一致,也可以不一致。这种特性,和c++有点像,毕竟c++的.h 和 .cpp文件是分开的。只要最终编译的时候对的上,文件名其实无所谓的。Java中,一个类文件的类名和文件名不一致,如果是public类,就会报异常。

    在kotlin中,可以写成一致,如:

    不一致:

    image.png

    这样做的意义在于:

    如果有很多个行数很短的类:在java中可能要占用大量的文件个数(Java中可以用内部类的形式解决),kotlin中则可以把这些类都放到同一个kt文件中,不用内部类也能解决。


    共生体

    Java中的静态 static关键字,在kotlin中不复存在,作为替换,Kotlin提出了共生体的概念。如果是kt文件去调用kt类的“静态”方法(不依赖对象),则要求后者的类结构中增加一个 companion object 成员变量。并且可以在 成员中写上 你想要定义的"静态"成员变量和成员方法

    class Test001(_name: String) : Person(_name) {
        companion object {
            const val s: String = ""
            const val s2: String = ""
    
            fun t1(){
    
            }
        }
    }
    
    fun main(){
        Test001.s
        Test001.t1()
    }
    

    注:每一个kotlin类中,只能有一个共生体对象.

    但是在java调用kt的"静态"成员方法时,必须带上共生体,但是,访问"静态"成员变量,则不能带:

    public static void main(String[] args) {
            Test001.Companion.t1();//Java访问kt的t1()共生体方法,必须带上Companion
            String s2 = Test001.s;// 而访问共生体成员变量,不能带Companion
     }
    

    好纠结。为什么要这么设计。算了。查了一下kt反编译之后的Java源码:

    image.png

    共生体变成了Java类中的静态内部类,包含t1()方法。而s,s2 则是普通的静态变量。


    修饰符

    修饰符指的是 类 和 成员变量,成员方法 前面的 权限访问关键字。原 Java拥有 private ,protected,default ,public ,访问权限分别为: 本类内部,同包名或子类,同包名,全局。

    然而,kotlin新增了一个概念,internal ,表示,相同Module内可访问,跨Module则不行。

    并且,java和kotlin的 private ,protected,default ,public 的访问权限还有区别,但是我这里就不详述了,因为我觉得意义不大。能不能访问,写代码的时候编译器会告诉你,当场警告你,你就会修改代码。如果有问题。可以把kotlin Decompile成Java代码自己去对比试试。如有需要,后期再说吧。


    空指针问题

    通常要快速的将 旧java代码转化成kotlin代码,是拷贝java代码粘贴到kotlin文件内,让as自动转化,但是这种方式,容易造成很多空指针问题,有一些是很直白的报了编译错误,而有一些则是隐藏起来,等到程序运行时才会报错。直接报错的就不提了,下面演示隐藏的空指针问题:

    Kotlin类:

    class Student(name:String) {
        var name: String = name
    
        fun showName(tag: String) {
            println("$tag : $name")
        }
    }
    

    Java调用kt:

    public class Main {
        public static void main(String[] args) {
            Student s = new Student("zhou");
            s.showName(null);
        }
    }
    

    此时,如果运行main函数,就会报出:

    image.png

    告诉我们参数tag不可为null。但是奇怪的是,在java代码中,居然不会报编译错误。贼特么诡异。

    解决方案:

    在方法参数后面加上问号,变成这样:

    image.png

    没有基本数据类型

    Kotlin之中没有基本数据类型,它只有:
    Int,Short,Long,Float,Double,Byte ,Char,Boolean 这样的包装类型。
    为什么没有?没有必要去纠结,但是只提供包装类型有一个好处,那就是 方便扩展函数的定义。
    我们可以很轻松地对 Int,类型去扩展函数。
    比如: Kotlin自带了很多扩展函数:

    image.png

    这是系统定的,我们也可以自己来定义:

    fun Int.plus100(): Int {//自定义扩展
        return this + 100
    }
    fun main() {
        val a: Int = 20
        println("${a.plus100()}")
    }
    

    继承

    在用kt重构部分模块的过程中,我发现频繁出现下面的问题:

    Kotlin基类:

    abstract class Person(name: String) {
        var name: String? = name
    }
    

    Java子类:

    image.png

    由于我是从基础类开始重构,所以,在原先的Java代码中频繁出现了类似这种 访问权限不足的问题。一个一个去改成setName函数,工作量巨大。后来找到一个办法:

    在kotin中加入 @JvmField 变成这样:

    abstract class Person(name: String) {
        @JvmField
        var name: String? = name
    }
    

    @JvmField可以让 kotlin的成员属性变成公有的,kt转化成java时,会是如下这样:

    public abstract class Person {
       @JvmField
       @Nullable
       public String name;
    
       public Person(@NotNull String name) {
          Intrinsics.checkParameterIsNotNull(name, "name");
          super();
          this.name = name;
       }
    }
    

    兼容原先的Java代码。不用大面积修改了。


    默认支持可选命名参数

    了解高级语言语法的同学肯定知道 可选命名参数可选位置参数,经测试: Kotlin的任何方法(包括构造方法和普通和方法),可以这么写:

    fun test001(s: String, s1: String) {
        println("$s -  $s1")
    }
    
    fun main() {
        test001(s = "1111", s1 = "2222") //卧槽,Kotlin默认支持 可选命名参数
    }
    

    这种特性可以很好的避免了Java中出现的一个方法包含N个参数 把人眼睛看花的情况:

    private void test(String s1, String s2, String s3, String s5, String s6, String s7, String s8, String s9, String s10, String s11, String s12) {
            //...
        }
    

    比如如上面所示,一个方法,有12个String参数,看多了会骂娘,谁特么写的。然而,用kotlin:

    fun test(s1: String, s2: String, s3: String, s4: String, s5: String, s6: String, s7: String, s8: String, s9: String, s10: String, s11: String, s12: String) {}
    fun main() {
        test(s1 = "",s2 = "",s3 = "",s4 = "",s5 = "",s6 = "",s7 = "",s8 = "",s9 = "",s10 = "",s11 = "",s12 = "")
    }
    

    直觉上这种语法,融入了 建造者设计模式。让同一个函数的多个参数不再混乱。当然如果你怀旧的话,你也可以用原始方法,12个string依次摆下去。反正我不会这么做。


    类,成员方法 默认封闭

    和Java相反,kotlin给类,成员方法 都采用了默认封闭原则。具体体现为:类,默认不可继承,成员方法默认不可重写(继承时)。如果要继承类,或者重写父类方法,必须在父类中手动加入 open 关键字,子类中重写方法必须加上override关键字 :

    kotlin父类:

    open class Student(name:String) {
        var name: String = name
    
        open fun showName(tag: String?) {
            println("$tag : $name")
        }
    }
    

    kotlin子类:

    class StudentExt(name: String) : Student(name) {
        override fun showName(tag: String?) {
            super.showName(tag)
            println("xxxxx")
        }
    }
    

    Kotlin中方法和函数的区别

    函数,是c/c++的词汇,而方法,则是Java里面。现在kotlin中同时存在了方法和函数,那么区别在哪里?

    通常我们人为:在Kotlin类内部,称为成员方法。而在类外部定义的,则成为全局函数(这里就不用去讨论kotlin变成java之后长什么样)。

    应用到具体场景,一句话解释清楚:

    A.kt 中有一个A类,它有a()成员方法。 同时我们可以在 B.kt中给A类扩展一个函数。创建一个A类对象之后,我们既可以调用a()成员方法,又可以调用它的扩展函数。

    A.kt

    class A {
        fun a() {}
    }
    

    B.kt

    fun A.foo(){}// 扩展A的一个函数
    
    fun main() {
        val a = A()//创建对象
        a.a() //调用成员方法
        a.foo() //调用扩展函数
    }
    

    结语

    Java转kotlin,给我的感觉就是:

    1. kotlin对于一个function内部的管理更加有条理,它引入了 scope 作用域的概念,利用基于lambda表达式的高阶函数,把function内部的代码块管理起来,让代码可读性更高
    2. kotlin的代码行数会大大减少,因为kotlin设计了很多顶层函数,高阶函数,使用大量的链式调用,把可以占用行数的代码都浓缩在一行。这样做的结果是,一行代码的信息量大大增加,对于新手是噩梦,但是对于kotlin熟手,会感觉很美妙。
    3. 关于协程,本文只做了最简单的管中窥豹描述,未曾详细说到的东西还有很多。但是可以肯定一点,协程的出现,颠覆了 android开发的异步编程思维,原本很多不敢想的,原本很多java实现起来要绕很多路的,在kotlin上都可以很优雅地实现。

    参考资料

    Kotlin in Action 下载pdf书籍(https://www.7down.com/soft/209822.html
    菜鸟教程 https://www.runoob.com/kotlin/kotlin-tutorial.html
    kotlin中文站 https://www.kotlincn.net

    相关文章

      网友评论

        本文标题:Java老鸟如何玩转Kotlin

        本文链接:https://www.haomeiwen.com/subject/zmdnactx.html