【Kotlin】学习小记--进阶篇(一)

Kotlin学习小记之进阶篇(一)

  • 前言
  • 一、高阶函数
      • 1.函数可赋值给变量
        • 函数类型
      • 2.函数可做其他函数的参数
      • 3.函数可做其他函数的返回值
  • 二、集合中的函数式API
    • filter
    • map
    • flatmap
  • 三、Sequence(序列) —— 性能之王(最爱的part)
      • 普通集合低效原因
      • sequence高效率的原因
      • 什么是惰性求值?
      • 序列操作方式:
        • 1.中间操作
        • 2.末端操作
      • 序列和Stream的比较
  • 四、Lambda
    • 1.Java8的Lambda
    • 2.kotlin的Lambda
        • 关于Function与invoke
    • 3.简化kotlin中的lambda表达式
        • 简化Lambda表达式的规则
        • 方法引用(它是简化版的Lambda表达式)
        • Lambda和函数,闭包
  • 总结

上一篇基础篇地址:《Kotlin学习小记-基础篇》

前言

把自己学习的笔记整理了一次,理解也加深了些。这一部分我对Sequence的研究偏多,也是我最有兴趣的一个part。
这篇博客主要是讲:高阶函数集合中的函数式APISequence(序列),Lambda

一、高阶函数

高阶函数:以其他函数作为参数返回值的函数。高阶函数是函数式编程的重要特性。

1.函数可赋值给变量

函数类型

val sum={
        x:Int,y:Int->x+y
    }
    
fun print(){
        print(sum(1,2))
        print(sum(3,5))
}

上面的 val sum={ x:Int,y:Int->x+y } 代码可变为:

val sum:(Int,Int)->Int={x,y->x+y}

上述代码中,sum是函数类型,接收两个Int类型的数据,再返回一个Int类型的数据。

函数类型的特性:
1.a.一个圆括号括起来的参数类型列表 b.一个返回类型
2.有一个额外接收这类型:类型 A.(B)->C 表示:在A的接收者对象上以一个B类型参数来调用并返回一个C类型值的函数。
3. (挂起函数会在稍后做介绍,它狠狠狠狠重要!)
挂起函数,它是一种特殊种类的函数类型:suspend()->Unit 或 suspend A.(B)->C

2.函数可做其他函数的参数

fun sumAsParam(a:Int,b:Int,term:(Int)->Int):Int{
        var sum=0
        for(i in a..b){
            sum+=term(i)
        }
        return sum
    }

fun main(arrays:Array<String>){
        val identity={x:Int->x}
        val square={x:Int->x*x}
        val cube={x:Int->x*x*x}

        println(sumAsParam(1,10,identity))
        println(sumAsParam(1,10,square))
        println(sumAsParam(1,10,cube))
}

执行结果:

55
385
3025

简化以上代码的写法:
1.sum函数中,函数类型参数在最后,使用时可以做进一步的简化,将其提至最外面

fun main(arrays:Array<String>){
        println(sumAsParam(1,10) { x:Int->x})
        println(sumAsParam(1,10) { x:Int->x*x})
        println(sumAsParam(1,10) { x:Int->x*x*x})
}

2.由于term的类型是(Int)->Int,它只有一个参数,可忽略声明函数参数,可以用 it 来替代

fun main(arrays:Array<String>){
        println(sumAsParam(1,10) { it})
        println(sumAsParam(1,10) { it*it})
        println(sumAsParam(1,10) { it*it*it})
}

3.函数可做其他函数的返回值

    fun sumForResult(a:Int,b:Int,term:(Int)->Int):Int{
        var sum=0
        for(i in a..b){
            sum+=term(i)
        }
        return sum
    }

    fun sumUseFunction(type:String):(Int,Int)->Int{
        val identity={x:Int->x}
        val square={x:Int->x*x}
        val cube={x:Int->x*x*x}

        return when(type){
            "identity"->return {a,b->sumForResult(a,b,identity)}
            "square"->return {a,b->sumForResult(a,b,square)}
            "cube"->{a,b->sumForResult(a,b,cube)}
            else->{ a,b->sumForResult(a,b,identity)}
        }
    }

    fun main(arrays:Array<String>){
        val list=toList("java","kotlin","python","scala")
        println(list)
        //函数作为其他函数的返回值
        var identyFunction=sumUseFunction("identity")
        println(identyFunction(1,10))

        var squareFunction=sumUseFunction("square")
        println(squareFunction(1,10))

        var cubeFunction=sumUseFunction("cube")
        println(cubeFunction(1,10))
    }

释意:调用sumUseFunction函数,该函数返回的是(Int,Int)->Int 。把返回的函数赋值给变量,要使用该变量,则需要传递两个参数才能使用。

二、集合中的函数式API

filter

过滤集合中大于10的数字,将其打印出来。注:这里的 ::println 是方法引用(后面单独说这个)

    fun useFilter(){
        listOf(2,5,7,12).filter { it>5 }.forEach (::println)
        //上面代码,等于这段代码:listOf(2,5,7,12).filter { it>5 }.forEach{(println(it))}
    }

执行结果:

7
12

map

将集合中的字符串都转换为大写,将其打印出来。

fun useMap(){
        listOf("allen","joe","chris").map { it.toUpperCase() }.forEach (::println)
}

执行结果:

ALLEN
JOE
CHRIS

flatmap

遍历所有元素,为每一个创建一个集合,最后把所有集合放在一个集合中。

fun useFlatmap(){
        listOf(2,4,5,8,9,12).flatMap { listOf(it,it-1) }
}

执行结果:

[2,1,5,4,8,7,9,8,12,11]

三、Sequence(序列) —— 性能之王(最爱的part)

(这是我个人最最最感兴趣的part!!!)
很多博文都直接简化了sequence的重要性,甚至是只言片语,但是我在查看了很多资料之后,我对它是真的很爱,并且觉得它很有意思,也很重要。在我了解之后,就真的很爱很爱它!
在这里插入图片描述
首先,我们先看一段相当常规的处理集合的代码:

val list=listOf(1,2,3,4,5)
list.filter{ it>2 }.map{ it*2 }

这是一个常规处理集合的操作,如上操作可以帮我们解决大部分的问题,但当list中的元素非常多的时候(如超过10万),如上操作在处理集合的时候就显得比较低效。

普通集合低效原因

在这里插入图片描述

filter方法和map方法都会返回一个新的集合,也就意味着上面的操作会产生两个临时集合,list会先调用filter方法,然后产生的集合会再次调用map。若list中元素很多,这笔开销定是不小。而这,也是sequence为什么会出现的原因!

如何使用sequence?

list.asSquence().filter{ it>2 }.map{ it*2 }.toList()

步骤:

  1. 通过asSquence()方法将一个列表转换为序列;
  2. 在这个序列上进行相应的操作;
  3. 最后通过toList()方法将序列转换为列表。

sequence高效率的原因

sequence提高效率的原因:使用sequence时,filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。序列中元素的值时惰性的,意味着,该值被取用时才去求值。这样做的好处是,好处:性能得到提升;可以构造出一个无限的数据类型。

什么是惰性求值?

理解为:在需要时才进行求值的计算方式; late init by lazy这种叫懒加载或者惰性。
【Kotlin】学习小记--进阶篇(一)_第1张图片

fun useSequence(){
        listOf(1,4,5,7,8,9,13,12)
            .asSequence()
            .filter { it>2 }
            .map { it+1 }.toString()
}

这里有一点,我们要调用 toXXX 它才会被激活。
最开始的时候,我进入了一个误区,我以为sequence序列就是用来替代普通集合的,后来去仔细查了资料,sequence的作用是优化集合,对集合起的是互补作用。

序列操作方式:

list.asSquence().filter{ it>2 }.map{ it*2 }.toList()

这里一共执行了两类操作:

  1. filter{ it>2 }.map{ it*2 }
    filter和map的操作返回都是序列,这类操作称为中间操作
  1. toList
    这个操作将序列转换为List,这类操作称为末端操作

序列操作分为两类:中间操作,末端操作。

1.中间操作

对普通集合进行链式操作时,有些操作会产生中间集合,当用这类操作来对序列进行求值的时候,它们被称为中间操作。
每一次中间操作返回的都是一个序列,产生的新序列内部知道如何去变换原来序列中的元素。
中间操作都是采用惰性求值

2.末端操作

末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,如:列表,数字,对象等。
末端操作一般放在链式操作的末尾
在执行末端操作的时候,会触发中间操作的延迟计算,将‘被需要’状态打开。

序列和集合的操作不同:序列在执行链式操作时,会将所有的操作都应用在一个元素上。白话来说就是,第1个元素执行完所有的操作之后,第2个元素再去执行所有的操作,以此类推。

若filter和map的位置可以相互调换,优先使用filter,这样会减少一部分开销。

原因 :当列表中的元素在满足filter条件之后,才会去执行后面的map。这样就减少了一部分的开销。

对于sequence序列和集合的性能方面有兴趣的,可以看一下国外程序员对它们俩的基准测试:
https://gist.github.com/ajalt/c6c120c7cc13a49bc2a8bf62d433d455
(访问这个网站是要的,作为一个程序员,我想你可以成熟的用自己的双手创造条件!)

【Kotlin】学习小记--进阶篇(一)_第2张图片

序列和Stream的比较

很多人拿 Sequence序列 和Java8的 Stream流 进行比较,我也看了一下,觉得挺有意思的,也来说说吧。
(我遇到自己感兴趣的东西就会多研究两分钟,如此执拗的我,嘤嘤嘤,鬼毛病…)
在这里插入图片描述

1.Java8出来后,在Java中也可以像Kotlin中那样操作集合。

筛选数字大于5的数:

numbers.stream().filter(it -> it.num > 5).collect( toList() );

但是相对于Kotlin,Java的操作方式还是有些繁琐。现将集合转换为stream,操作完成后,将stream转换为List,这种操作类似于Kotlin中的序列。
而Java8的流和Kotlin中的序列一样,也是惰性求值,意味着Java8的流也存在中间操作,末端操作。so,必须经过上面的一系列转换。

2.Stream是一次性的
与Kotlin的序列不同,Java8中的流是一次性的。创建了一个Stream,我们只能在这个Stream上遍历一次;当你遍历完成之后,这个流则被消费掉,必须在创建一个新的Stream,才能再遍历一次。

3.Stream能够并行处理数据

Java8的流很强大,它能够在多核架构上并行地进行流的处理。
并行处理数据这个特性,Kotlin的序列目前还没实现,如果需要处理多线程的集合,仍然需要依赖Java。

四、Lambda

在这里插入图片描述

1.Java8的Lambda

在Java8之前,我们使用Thread如下:

new Thread(new Runnable() {    
        public void run() {        
              System.out.println("test");    
       }
}).start();

在Java8后我们使用Lambda来表达:

new Thread(()->System.out.println("test")).start();

java没有像kotlin那样拥有函数类型,所以需要借助SAM类型来实现Lambda表达式。

java8的Lambda语法:

(参数列表) -> {函数体}

无参表达式:

()->{函数体}

单个参数表达式:

(x) -> {函数体} 也可以简化为 x -> {函数体}

多个参数的表达式:

(x,y,z) -> {函数体}

若函数体只有一行代码,则可以省略函数体的大括号。
Java编译器可以进行类型推断,所以不需要声明参数列表的类型。

2.kotlin的Lambda

1.kotlin最终还是编译成.class文件,由JVM进行加载。

2.Lambda的语法:
1.一个Lambda表达式必须通过 {} 来包裹;
2.如果Lambda声明了参数部分的类型,且返回值类型支持类型推导。那么Lambda变量就可以省略函数类型声明;
3.如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略。

若Lambda表达式返回的不是Unit,则默认最后一行表达式的值类型就是返回值类型。

在kotlin中的lambda表达式条件,配合下面的代码观看更为直观:

{参数列表 -> 函数体}

步骤分析:

1.大括号
2.参数及其类型
3.加上“->”符号
4.函数体位于3.之后
5.函数体最后一句表达式是lambda的返回值
Notice:lambda若无参,则可省“->”

用kotlin实现一个Thread:

Thread{ println("thread") }.start()

kotlin比java的lambda表达式更为简洁!
【Kotlin】学习小记--进阶篇(一)_第3张图片

关于Function与invoke

1.sum函数本质上是实现kotlin.jvm.functions.Function2 接口.

class ShowFunctionUse {
    fun main(args: Array<String>) {
        val sum = {
                x: Int, y: Int -> x + y
        }
        println(sum(3, 5)) // 8
        println(sum(4, 6)) // 10
    }
}

查看一下 Kotlin Bytecode ,会发现它确实是这样。

(Bytecode方法:在IDEA中 Tools -> Kotlin ->Show Kotlin Byecode ;在Bytecode的代码页选择 Decompile 就完成!)

public final class ShowFunctionUse {
   public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Function2 sum = (Function2)null.INSTANCE;
      int var3 = ((Number)sum.invoke(3, 5)).intValue();
      boolean var4 = false;
      System.out.println(var3);
      var3 = ((Number)sum.invoke(4, 6)).intValue();
      var4 = false;
      System.out.println(var3);
   }
}

如上所示,Function接口,invoke方法皆被调用。

有多种方式可以反编译成字节码,例如使用 idea 自带工具"Tools->Kotlin->Show Kotlin Bytecode";
或者使用javap命令反编译.class文件,亦或者使用 BytecodeViewer 反编译.class文件都能够将 Kotlin 的代码反编译成字节码。
有时候字节码看不清晰的话,反编译成 Java 代码效果会更好。

那看到上面的代码,我在思考,是不是意味着所有的函数都会实现Function接口,调用invoke方法呢?

答案:不是

那么什么时候实现Function接口和Invoke方法呢?

答:内联函数 不会实现Function接口。 关于内联函数这一块,我放到下一节里去总结分析。

3.简化kotlin中的lambda表达式

此处使用View的点击事件作为示例,一步一步推导如何做简化的。

1.Java代码下的点击事件,代码如下:

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

2.kotlin搬java的代码来写,粗狂代码如下:

view.setOnClickListener(object :View.OnClickListener{
    override fun onClick(v: View?) { 
        println("click")
    }
})

3.对上述代码使用Lambda表达式

view.setOnClickListener({ v ->   
    println("click")
})

4.参数为函数类型,且为最后一个参数,可将参数移到函数的括号外面

view.setOnClickListener() { v ->  
    println("click")
}

5.若参数只有一个Lambda表达式,则函数的小括号可以省略

view.setOnClickListener { v ->
    println("click")
}

6.若在点击事件时,要使用view,则可以省略 v-> ,使用默认参数 it 进行替代。

view.setOnClickListener {
    it.visibility=View.VISIBLE
    it.background= getDrawable(R.drawable.ic_launcher_background)
}

简化Lambda表达式的规则

1.函数中,最后一个参数是函数类型,可以将lambda移到函数的括号外面。
2.若函数的参数只有一个lambda,那么函数的小括号可省略。
3.若Lambda表达式中只有一个参数,可使用默认参数it代替。
4.若Lambda表达式有多个参数,若某个参数未使用,可用“_”下划线取代其名字。
5.入参,返回值,与形参一致的函数,可以用方法引用的方式作为实参传入。( 方法引用见往下望一眼就有了 ⊙▽⊙! )

方法引用(它是简化版的Lambda表达式)

在java8中的方法引用的使用方式 类名::方法名
而kotlin沿袭了Java8的习惯。
假设有个类名为Animals,它里面有一个public的fly()方法,再另一个类里面我们该如何调用它呢?方法如下:

val animals=::Animals
animals::fly

Lambda和函数,闭包

初学的时候我对Lambda,函数不是很能清晰得区分开来,接下来整理了它们之间的区别。
Lambda和函数的区别:

1.fun在没有等号,只有花括号的情况下,是常见的代码块函数题;若返回值非Unit值,则必须带return。

fun foo(x:Int){ print(x) }
fun foo(x:Int,y:Int) : Int{ return x * y }

2.fun带有等号,是单表达式函数题,这种情况可省略return。

fun foo(x:Int,y:Int) = x+y

3.不管是用val还是fun,如果是等号+花括号的语法,那么构建的就是一个Lambda表达式,Lambda的参数在花括号内部声明。
若左侧是fun,则Lambda表达式是函数题,必须通过()或invoke来调用Lambda。

val foo={ x:Int,y:Int->x+y }  // foo.invoke(1,2)或foo(1,2)

fun foo( x:Int ) ={ y:Int->x+y } // foo(1).invoke(2)或foo(1)(2)

闭包
不知道你有没有发现,匿名函数体,Lambda(以及局部函数,object表达式)在语法上都存在 {} ,由这对花括号包裹的代码块如果访问了外部环境变量,则被称为一个闭包,它可以被看成‘访问外部环境变量的函数’。
和Java不一样的是:Kotlin中的闭包不仅可以访问外部变量,还可以对其进行修改。

var sum=0
listOf( 1,2,3 ).filter{ it>0 }.forEach{
     sum+=it
}

总结

上一篇基础篇的地址指南:《Kotlin学习小记-基础篇》

下一篇将要总结的内容是:内联函数内联属性扩展函数扩展属性委托运算符重载中缀表达式Kotlin的泛型

如若有疑问和不准确的地方请指出,Thx,coder bro!

(近期因个人原因,暂时不会进行下一篇kotlin进阶篇(二)的内容总结。

先进行一篇用Kotlin进行的《MVVM+Retrofit+LiveData+Room+DataBinging的框架搭建》,近日就着手开始搬起来)

【Kotlin】学习小记--进阶篇(一)_第4张图片

(阿sweet最近的一些小牢骚,和本文学习内容毫无关系!)
总有一些伤害过你的人,总是能厚颜无耻,毫无愧疚之心的的活在这个世界上。笑一笑就好。

你可能感兴趣的:(kotlin,android)