Kotlin (五)在集合里面感受Lambda

作者: zcwfeng | 来源:发表于2020-12-26 23:20 被阅读0次

    5.1 简化表达

    举个Android里面最常用的例子,java总普遍的用法

    view.setOnClickListener(new OnClickListener(){
      @Override
      public void onClick(View v){
      ...  
      }
    })
    

    翻译成kotlin并且简化

    view.setOnClickListener(object:OnClickListener {
                override fun void onClick(v:View){
                    ...
                }
            })
    
    按照Java 单一抽象方法OnClickListener 可以用Kotlin函数替代
    fun setOnclickListener(listener:(View)->Unit)
    ->Lambda
    
    view.setOnClickListener({
    。。。
            })
    -> kotlin语法糖,listener是唯一的参数,所以可以省略括号
    
    view.setOnClickListener{
    。。。
            }
    
    
    带有接收者的Lambda

    View 接收者类型扩展invisible方法

    fun View.invisible(){
        visibility = View.INVISIBLE
    }
    

    接收者函数类型

    
    val sum:Int.(Int)->Int = { other ->plus(other)}
    
    fun main() {
        println(2.sum(1))
    }
    
    int型变量调用sum传入一个int型变量参数,进行plus操作
    

    官网一个小例子

    class HTML{
        fun body(){
            println("Test custom HTML")
        }
    }
    fun html(init:HTML.()->Unit):HTML{
        val html=HTML()
        html.init()
        return html
    }
    
    // 调用
    html{
            body()
        }
    
    with 和 apply

    作用:再写Lambda时候,省略需要多次书写的对象名,默认用this指向它

    Android中我们会给视图控件绑定属性。with例子

    fun bindData(bean:ContentBean){
            val titleTv = findViewById<TextView>(R.id.titleTv)
            val contentTv = findViewById<TextView>(R.id.contentTv)
            with(bean){
                titleTv.text = this.title
                contentTv.text = this.content
                titleTv.textSize = this.titleFontSize
                contentTv.textSize = this.contentFontSize
            }
        }
    

    不用with会写很多重复的bean
    with在kotlin中的定义

     public inline fun <T, R> with(receiver: T, block: T.() -> R): R 
    

    第一个参数是一个接收者,第二个参数是一个创建这个类型的block。因此在接收者调用block的时候可以在Lambda直接使用this代替bean

    看下apply 的写法

    fun bindData(bean:ContentBean){
            val titleTv = findViewById<TextView>(R.id.titleTv)
            val contentTv = findViewById<TextView>(R.id.contentTv)
           
            bean.apply {
                titleTv.text = this.title
                contentTv.text = this.content
                titleTv.textSize = this.titleFontSize
                contentTv.textSize = this.contentFontSize
            
            }
        }
    

    apply 的定义

    public inline fun <T> T.apply(block: T.() -> Unit): T 
    

    直接声明了一个T的扩展,block参数是一个返回Unit类型的函数

    with的block返回自由的类型。with和apply很多情况可以互相替代

    5.2 集合的高阶函数API

    map 简化

    java8 之前的遍历

    int[] list = {1,2,3,4,5,6};
            int newList[] = new int[list.length];
            for (int i = 0; i < list.length; i++) {
                newList[i] = list[i] * 2;
            }
    
    

    java8

     int newList[] = Arrays.stream(list).map(x-> x*2).toArray();
    

    kotlin

    val list = listOf(1,2,3,4,5,6);
    val newList = list.map{it*2}
    

    map 后面的表达式就是一个匿名带参的函数, map接收函数

    fun foo3(bar:Int) = bar * 2
    val newList2 = list.map { foo(it) }
    

    map 接收函数,然后将集合每个元素用这个函数操作,将操作结果返回,最后生成一个新的集合

    map源代码

    public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
        return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
    }
    
    public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
        for (item in this)
            destination.add(transform(item))
        return destination
    }
    

    首先定义了map的扩展方法,他的实现主要依赖mapTo。两个参数,第一个MutableCollection 集合,第二个是一个方法 (transform: (T) -> R)。就是将transform方法产生的结果添加到新集合里面去,返回一个新集合,避免我们for并且产生临时变量

    filter、count 集合筛选
    data class Student(val name:String,val age:Int,val sex:String,val score:Int)
    val jielun = Student("jielun",30,"m",85);
    val david = Student("david",35,"f",80);
    val lilei = Student("lilei",32,"m",90);
    val lili = Student("",31,"m",97);
    val jack = Student("jack",18,"m",92);
    val pan = Student("pan",20,"m",82);
    val students = listOf(jielun,david,lilei,lili,jack,pan)
    val mstudents= students.filter { it.sex == "m" }
    

    看下filter源码

    public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
        return filterTo(ArrayList<T>(), predicate)
    }
    
    public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
        for (element in this) if (predicate(element)) destination.add(element)
        return destination
    }
    

    看完map源码filter就比较好理解,依赖filterTo,接收两个参数,第一个MutableCollection 集合,第二个 predicate: (T) -> Boolean 返回boolean的函数(Lambda表达式)函数返回true保留否则丢弃

    其他过率方法还有
    filterNot,过率掉满足条件的
    filterNotNull,过率掉null元素
    count,统计满足条件的元素个数----显得到满足条件列表再统计,效率有点低

    val stuCount= students.filter { it.sex == "m" }.size
    

    sumBy、sum、fold、reduce 别样的求和方式

    java中我们做法是for然后累加,用sumBy 一行

    val scoleTotal = students.sumBy { it.score }
    

    拿最开始的list的int数组求和,sum和sumBy差不多

    val total = list.sum()
    val total2 = list.sumBy { it }
    
    fold 是一个比较强的API ,先看下实现
    public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R {
        var accumulator = initial
        for (element in this) accumulator = operation(accumulator, element)
        return accumulator
    }
    
    

    fold 接收两个参数,第一个,initial 初始值,第二个是一个operation函数。 实现的时候通过for遍历集合每个元素,每次都调用operation函数---也有两个参数,第一个是上一次调用这个函数的结果(第一次使用initial出事值),第二个参数就是当前遍历的元素。
    如何使用

    val foldTotal = students.fold(0){acumulator,student->acumulator+student.score}
    

    fold很好的利用了递归思想

    reduce 和 fold很相似,唯一区别没有初始值,看下源码

    public inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {
        val iterator = this.iterator()
        if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
        var accumulator: S = iterator.next()
        while (iterator.hasNext()) {
            accumulator = operation(accumulator, iterator.next())
        }
        return accumulator
    }
    

    如果我们不需要初始值,那么可以用reduce

    val reduceTotal = students.reduce{acumulator,student->acumulator+student.score}
    

    groupBy 分组

    通常我们用java 写很多for和if进行循环和条件判断,kotlin groupBy提供了语法糖

        students.groupBy { it.sex }
    

    返回 Map<String,List<Student>>

    {m=[Student(name=jielun, age=30, sex=m, score=85), Student(name=lilei, age=32, sex=m, score=90), Student(name=, age=31, sex=m, score=97), Student(name=jack, age=18, sex=m, score=92), Student(name=pan, age=20, sex=m, score=82)], f=[Student(name=david, age=35, sex=f, score=80)]}
    
    

    扁平化--处理嵌套集合:flatMap,flatten

    将list2 变成和前面list一样的普通集合

    val list2 = listOf(listOf(jielun, david), listOf(lilei, lili, jack), listOf(pan))
     println(list2.flatten())
    >>>>>输出
    [Student(name=jielun, age=30, sex=m, score=85), Student(name=david, age=35, sex=f, score=80), Student(name=lilei, age=32, sex=m, score=90), Student(name=, age=31, sex=m, score=97), Student(name=jack, age=18, sex=m, score=92), Student(name=pan, age=20, sex=m, score=82)]
    
    

    看下源码

    public fun <T> Iterable<Iterable<T>>.flatten(): List<T> {
        val result = ArrayList<T>()
        for (element in this) {
            result.addAll(element)
        }
        return result
    }
    

    就是循环遍历多个集合合并成了一个集合

    如果我们需要得到的是加工一下的集合 flatMap

        list3.flatMap { it.map {it.name} }
    >>>>>输出
    [jielun, david, lilei, , jack, pan]
    
    

    看下flatMap源码

    public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
        return flatMapTo(ArrayList<R>(), transform)
    }
    
    public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
        for (element in this) {
            val list = transform(element)
            destination.addAll(list)
        }
        return destination
    }
    

    flatMapTo 接收两个参数,第一个为一个列表,改列表是一个空列表。另外一个是一个函数。改函数返回一个序列。
    遍历集合的元素,然后将集合元素传入transform的到一个列表,将列表元素添加到空列表destination中,这样经过transform函数处理的扁平化列表。

    如果需要扁平化处理集合flattern就好,需要对元素加工那么用flatMap

    5.3 集合的相关设计

    2020-12-26 17.34.11.png

    1.list 是一个可以重复的列表,元素存储方式是线性存储

        println(listOf(1,2,3,4,5,6))
    >>>>
    [1, 2, 3, 4, 4, 6]
    
    
    1. map 没有实现Iterator和Collection。Map用来表示键值对元素集合,键不能重复
        println(mapOf(1 to 1 ,2 to 2,3 to 3,3 to 4))
    >>>
    {1=1, 2=2, 3=4}
    
    
    1. Set 表示一个补课重复的集合。实现有两种HashSet 是Hash散列存储无序和TreeSet 底层是二叉树,有序。我们一般说的是无序
        println(setOf(1,2,3,3,4,4,5))
    >>>
    [1, 2, 3, 4, 5]
    
    

    可变集合与只读集合

    kotlin 虽然是基于java但是做了改动,分为可变集合和不可变集合

    1. 可变集合,都有一个前缀“Mutable”
    val mutableList = mutableListOf(1,2,3,4)
        mutableList[0] = 0
        println(mutableList)
    >>>>
    [0, 2, 3, 4]
    
    

    2 只读集合
    val list = listOf(1,2,3,4) 如果尝试改变编译器报错

    特殊情况:被改

    val writeList = mutableListOf(1,2,3,4,5)
        val readList:List<Int> = writeList
        writeList[0] = 0
        println(readList)
    
    >>> 
    [0, 2, 3, 4, 5]
    
    

    和java互相操作

    -》java
    
    public static List<Integer> fooJava(List<Integer> list){
            for (int i = 0; i < list.size(); i++) {
                list.set(i,list.get(i) * 2);
            }
            return list;
        }
    
    -》kotlin
    fun bar(list:List<Int>){
        println(fooJava(list))
    }
    
    ->
    val list = listOf(1,2,3,4)
        bar(list)
        println(list)
    
    》》》
    [2, 4, 6, 8]
    [2, 4, 6, 8]
    
    

    传入的list被改变了

    5.4 惰性集合

    如果集合元素数量比较大,使用上线的操作效率比较低

    通过序列化提高效率
    val list = listOf(1,2,3,4)
    list.filter{it > 2}.map{it * 2}
    这样会操作两个临时集合先filter,然后返回集合在用map处理产生新的集合
    如果filter处理数据量大,开销就很大
    
    使用序列
        list.asSequence().filter{it > 2}.map{it * 2}
    
    

    中间操作

    list.asSequence().filter{
            println("filter$it")
            it > 2
        }.map{
            println("map$it")
            it * 2
        }
    

    什么也没输出,知道末尾加上“.toList()”才输出了结果

    末端操作
    toList() 就是末端操作,就是链式调用最后需要输出结果而不是序列化的东西。
    对比asSequence 和不加输出的结果

    list.asSequence().filter{
            println("filter($it)")
            it > 2
        }.map{
            println("map($it)")
            it * 2
        }.toList()
    >>>>>>>>>>>
    filter(1)
    filter(2)
    filter(3)
    map(3)
    filter(4)
    map(4)
    

    所有的中间操作被执行

    ilter{
            println("filter($it)")
            it > 2
        }.map{
            println("map($it)")
            it * 2
        }
    >>>>>>>>
    filter(1)
    filter(2)
    filter(3)
    filter(4)
    map(3)
    map(4)
    

    普通的链式调用先执行玩filter在执行map。所以建议能先用filter的尽量先用filter。

    序列可以是无限的

    惰性计算最大的好处就是构造出来一个无限的数据类型。

    // 创造无限序列
        val naturalNumList = generateSequence(0){it +1 }
        println(naturalNumList.takeWhile { it<=9 }.toList())
    >>>> 
    
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    虽然不能穷举状态,但是我们可以通过条件控制我们想要多少数据。给我们一个无限的感觉
    

    和 java8 Stream对比

    1. java8 使用函数式API
      上面students 学生按照性别筛选
    students.stream().filter(it -> it.sex == "m").collect(toList());
    

    类似kotlin的序列,java需要将集合转换成stream流,操作完成后,还要将stream转换为List,java8这种也是惰性计算的

    1. java8 的stream是一次性的
      如果创建了一个stream,在这个stream上只能遍历一次,这个流就被消费掉。必须创建新的stream才能再次遍历
      students.stream().filter(it -> it.sex == "m").collect(toList());
      students.stream().filter(it -> it.sex == "f").collect(toList());
      3.Stream 能够并行处理数据(kotlin 目前貌似还不行)
            students.parallelStream().filter(it -> it.sex == "m").collect(toList());
    
    

    多核架构可以并行处理

    5.4 内联函数

    Lambda会带来一定的开销。内联函数主要是针对Lambda的优化。
    java却不需要,因为java7之后jvm引入了invokedynamic的技术,他会自动做lambda优化

    优化Lambda
    Lambda虽然语法简单,但是在Kotlin每声明一个Lambda就会产生一个匿名函类,该匿名类包含一个invoke方法,作为Lambda的调用方法,每次调用还会创建一个对象。因为Kotlin主要是对Android的开发语言,Kotlin在Android中必须引入某有方法来优化,就是内联函数

    1.java的Invokedynamic

    与kotlin这种再编译器通过硬编码生成Lambda转换类机制不同,java在se7 之后通过invokedynamic实现了在运行期才产生相应翻译代码。在invokedynamic 被调用的时候会产生一个匿名类替换中间代码invokedynamic,后续会直接使用这个匿名类。

    • 因为运行时候产生,字节码只能看到固定的invokedynamic,需要静态生成类的个数及字节码大小显著减小。
    • 编译时候写死字节码不同,利用invokedynamic可以把实际的翻译策略隐藏在jdk库中,提高了灵活性,向后兼容,后期可以继续对翻译策略优化
    • JMM天然支持针对这种方式Lambda的翻译和优化,开发者开发时候不需要关心这些优化。

    2 内联函数
    Kotlin 为了Android要兼容java se6 所以不能采用invokedynamic。
    采用了另一种解决方案内联函数。c++和C#等语言也支持这种特性。简单说可以用inline关键字来修饰函数。这些函数体编译期间被嵌入到每一个被调用的地方,减少额外生成匿名类和减少时间开销。

    我们看一个普通自定义高阶函数foo,接受一个类型()->Unit 的Lambda

    fun foo(block:()->Unit){
        println("before block")
        block()
        println("after block")
    }
    
    fun main() {
        foo {
            println("dive into kotlin...")
        }
    }
    

    反编译查看

    public final class KT内联函数Kt {
       public static final void foo(@NotNull Function0 block) {
          Intrinsics.checkParameterIsNotNull(block, "block");
          String var1 = "before block";
          boolean var2 = false;
          System.out.println(var1);
          block.invoke();
          var1 = "after block";
          var2 = false;
          System.out.println(var1);
       }
    
       public static final void main() {
          foo((Function0)null.INSTANCE);
       }
    
       // $FF: synthetic method
       public static void main(String[] var0) {
          main();
       }
    }
    

    调用foo产生一个Function0的类型的block类,然后通过invoke方法执行,这样会增加额外的开销。我们给foo函数加上inline修饰符

    public final class KT内联函数Kt {
       public static final void foo(@NotNull Function0 block) {
          int $i$f$foo = 0;
          Intrinsics.checkParameterIsNotNull(block, "block");
          String var2 = "before block";
          boolean var3 = false;
          System.out.println(var2);
          block.invoke();
          var2 = "after block";
          var3 = false;
          System.out.println(var2);
       }
    
       public static final void main() {
          int $i$f$foo = false;
          String var1 = "before block";
          boolean var2 = false;
          System.out.println(var1);
          int var3 = false;
          String var4 = "dive into kotlin...";
          boolean var5 = false;
          System.out.println(var4);
          var1 = "after block";
          var2 = false;
          System.out.println(var1);
       }
    
       // $FF: synthetic method
       public static void main(String[] var0) {
          main();
       }
    }
    

    上面集合高阶函数API也是用内联函数实现的,所以内联函数优化上很有必要。

    内联函数不是万能,不要随便用
    对于普通函数没必要用内联
    避免有大量函数体的函数内联,会导致过多字节码量
    一旦一个函数被内联,就不能获取闭包的私有成员。除非声明为internal

    noinline 避免函数参数被内联

    有时候函数需要接收多个参数,我们只想让Lambda参数内联。我们可以noinline加到不想内联参数的开头

    做个测试

    inline fun foo(block:()->Unit,noinline block2:()->Unit){
        println("before block")
        block()
        block2()
        println("after block")
    }
    
    fun main() {
        foo ({
            println("dive into kotlin inline...")
        },{
            println("no inline...")
        })
    }
    

    对比查看字节码

    public final class KT内联函数Kt {
       public static final void foo(@NotNull Function0 block, @NotNull Function0 block2) {
          int $i$f$foo = 0;
          Intrinsics.checkParameterIsNotNull(block, "block");
          Intrinsics.checkParameterIsNotNull(block2, "block2");
          String var3 = "before block";
          boolean var4 = false;
          System.out.println(var3);
          block.invoke();
          block2.invoke();
          var3 = "after block";
          var4 = false;
          System.out.println(var3);
       }
    
       public static final void main() {
          Function0 block2$iv = (Function0)null.INSTANCE;
          int $i$f$foo = false;
          String var2 = "before block";
          boolean var3 = false;
          System.out.println(var2);
          int var4 = false;
          String var5 = "dive into kotlin inline...";
          boolean var6 = false;
          System.out.println(var5);
          block2$iv.invoke();
          var2 = "after block";
          var3 = false;
          System.out.println(var2);
       }
    
       // $FF: synthetic method
       public static void main(String[] var0) {
          main();
       }
    }
    

    可以看到block2 没有被内联

    非局部返回
    fun foo(){
        println("before local return")
        localReturn()
        println("after local return")
        return
    }
    
    fun localReturn() {
        return
    }
    
    fun main() {
        foo()
    }
    >>>>>输出
    before local return
    after local return
    

    localReturn 没有起作用

    fun foo(returnning:()->Unit){
        println("before local return")
        returnning()
        println("after local return")
        return
    }
    
    fun main() {
        foo{return}
    ------报错
    }
    

    报错了,Lambda表达式正常情况不能return,修改加inline

    inline fun foo(returnning:()->Unit){
        println("before local return")
        returnning()
        println("after local return")
        return
    }
    
    fun main() {
        foo{return}
    }
    》》》》》
    before local return
    
    

    中间返回了,因为inline lamba 被替代,所以return生效

    crossinline

    为了避免带有return的Lambda参数产生破坏,可以阻止此类问题,用crossinline关键字修饰

    fun main() {
    ------这里报错
        foo{return}
    //    testfoo { return@testfoo }
    }
    inline fun testfoo(crossinline returnning:()->Unit){
        println("before local return")
        returnning()
        println("after local return")
        return
    }
    
    具体化参数类型 reified

    由于Kotlin也有泛型擦除机制,我们无法获得一个参数的类型。然而,由于内联函数直接在字节码生成相应函数体现,我们又可以获得具体类型。我们可以用reifield修饰符实现

    fun main() {
        getType<Int>()
    }
    
    inline fun <reified T>getType(){
        println(T::class)
    }
    >>>>>
    class java.lang.Integer (Kotlin reflection is not available)
    
    

    这个在Android里比较有用

    inline fun <reified T:Activity> Activity.startActivity(){
        startActivity(this,T::class.java)
    }
    
    调用,简化代码
            startActivity<MainActivity>()
    
    

    相关文章

      网友评论

        本文标题:Kotlin (五)在集合里面感受Lambda

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