上一篇基础篇地址:《Kotlin学习小记-基础篇》
把自己学习的笔记整理了一次,理解也加深了些。这一部分我对Sequence的研究偏多,也是我最有兴趣的一个part。
这篇博客主要是讲:高阶函数,集合中的函数式API,Sequence(序列),Lambda。
高阶函数:以其他函数作为参数或返回值的函数。高阶函数是函数式编程的重要特性。
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
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})
}
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 。把返回的函数赋值给变量,要使用该变量,则需要传递两个参数才能使用。
过滤集合中大于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
将集合中的字符串都转换为大写,将其打印出来。
fun useMap(){
listOf("allen","joe","chris").map { it.toUpperCase() }.forEach (::println)
}
执行结果:
ALLEN
JOE
CHRIS
遍历所有元素,为每一个创建一个集合,最后把所有集合放在一个集合中。
fun useFlatmap(){
listOf(2,4,5,8,9,12).flatMap { listOf(it,it-1) }
}
执行结果:
[2,1,5,4,8,7,9,8,12,11]
(这是我个人最最最感兴趣的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()
步骤:
- 通过asSquence()方法将一个列表转换为序列;
- 在这个序列上进行相应的操作;
- 最后通过toList()方法将序列转换为列表。
sequence提高效率的原因:使用sequence时,filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。序列中元素的值时惰性的,意味着,该值被取用时才去求值。这样做的好处是,好处:性能得到提升;可以构造出一个无限的数据类型。
理解为:在需要时才进行求值的计算方式
; late init by lazy这种叫懒加载或者惰性。
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()
这里一共执行了两类操作:
filter{ it>2 }.map{ it*2 }
filter和map的操作返回都是序列,这类操作称为中间操作
。
toList
这个操作将序列转换为List,这类操作称为末端操作
。
序列操作分为两类:中间操作,末端操作。
对普通集合进行链式操作时,有些操作会产生中间集合,当用这类操作来对序列进行求值的时候,它们被称为中间操作。
每一次中间操作返回的都是一个序列
,产生的新序列内部知道如何去变换原来序列中的元素。
中间操作都是采用惰性求值
。
末端操作就是一个返回结果的操作,它的返回值不能是序列
,必须是一个明确的结果,如:列表,数字,对象等。
末端操作一般放在链式操作的末尾
。
在执行末端操作的时候,会触发中间操作的延迟计算
,将‘被需要’状态打开。
序列和集合的操作不同:序列在执行链式操作时,会将所有的操作都应用在一个元素上。白话来说就是,第1个元素执行完所有的操作之后,第2个元素再去执行所有的操作,以此类推。
若filter和map的位置可以相互调换,优先使用filter,这样会减少一部分开销。
原因 :当列表中的元素在满足filter条件之后,才会去执行后面的map。这样就减少了一部分的开销。
对于sequence序列和集合的性能方面有兴趣的,可以看一下国外程序员对它们俩的基准测试:
https://gist.github.com/ajalt/c6c120c7cc13a49bc2a8bf62d433d455
(访问这个网站是要的,作为一个程序员,我想你可以成熟的用自己的双手创造条件!)
很多人拿 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。
在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编译器可以进行类型推断,所以不需要声明参数列表的类型。
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()
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接口。 关于内联函数这一块,我放到下一节里去总结分析。
此处使用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)
}
1.函数中,最后一个参数是函数类型,可以将lambda移到函数的括号外面。
2.若函数的参数只有一个lambda,那么函数的小括号可省略。
3.若Lambda表达式中只有一个参数,可使用默认参数it代替。
4.若Lambda表达式有多个参数,若某个参数未使用,可用“_”下划线取代其名字。
5.入参,返回值,与形参一致的函数,可以用方法引用的方式作为实参传入。( 方法引用见往下望一眼就有了 ⊙▽⊙! )
在java8中的方法引用的使用方式 类名::方法名
而kotlin沿袭了Java8的习惯。
假设有个类名为Animals,它里面有一个public的fly()方法,再另一个类里面我们该如何调用它呢?方法如下:
val animals=::Animals
animals::fly
初学的时候我对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的框架搭建》,近日就着手开始搬起来)
(阿sweet最近的一些小牢骚,和本文学习内容毫无关系!)
总有一些伤害过你的人,总是能厚颜无耻,毫无愧疚之心的的活在这个世界上。笑一笑就好。