Kotlin学习(5)Lambda编程

定义

Lambda表达式,本质上就是可以传递其他函数的一小段代码。有了lambda,可以轻松的把通用的代码结构抽取成库函数。

1.Lambda表达式语法

{ x: Int, y: Int -> x + y }
  • lambda表达式由花括号包围
  • 实参和lambda表达式由 ->分隔开
  • ->前面是实参,这里的实参不需要用括号括起来

可以把lambda表达式存储在一个变量中,并将这个变量当做普通函数调用使用

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

可以用库函数run来执行传递给他的lambda

run { println(42) }

2.Lambda表达式使用场景

  1. 作为函数的参数传递

    在开发中,我们经常需要用一段代码存储和传递一小段行为。比如:安卓中的按钮点击事件的监听,我们用匿名内部类的方式来实现:

    这种方式实现的事件的监听,每一次我们都需要声明一个类并且传递这个类的实例,语法啰嗦。

    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //点击后执行的动作
        }
    });
    

    这种方式是Kotlin中的实现,把函数当做值来处理,使用lambda表达式使语法更简洁。

    button.setOnClickListener {
        
    }
    
  1. Lambda的集合操作

    由于lambda的帮助,在Kotlin中对集合很多的操作都被封装在了库里,避免了很多重复代码。

    通过将一个lambda表达式传递给maxBy库函数,轻松地找出了集合中年龄最大Person对象

    val persons = listOf(Person(1,"tom"),Person(2,"jack"), Person(200,"joey"))
    println(persons.maxBy { it.age })
    

    如果lambda刚好是函数或者属性的委托,可以用成员引用替换

    persons.maxBy(Person::age)
    

3.lambda语法的简化

以下代码是未经过任何简化的语法:花括号中的代码片段是lambda表达式,他作为实参传递给maxBy函数。这个lambda表达式接受一个Person类型的对象,并返回他的年龄。

persons.maxBy({ p: Person -> p.age })

在Kotlin中有几个约定,可以用来简化lambda表达式

  1. 如果lambda表达式是函数的最后一个实参,它可以放到括号外边

    persons.maxBy() { p: Person -> p.age }
    
  2. 当lambda是函数的唯一实参时,可以去掉代码中的空括号

    persons.maxBy { p: Person -> p.age }
    
  3. 如果lambda表达式参数的类型可以被推导出来,可以省略参数类型

    persons.maxBy { p -> p.age }
    
  4. 使用默认参数名it代替命名参数(如果上下文期望的只有一个参数的lambda且参数的类型可以推断出来,就会生成这个名称)

    persons.maxBy { it.age }
    

4.在作用域中访问变量

在函数的内部使用lambda时,可以访问这个函数的参数

fun printDataWithPrefix(datas: Array<String>, prefix: String) {
    
    datas.forEach {
        println("$prefix $it")
    }
}

在函数的内部使用lambda时,可以访问在lambda前定义的局部变量

//统计传入集合中,可以 对2取余等于0和不等于0的个数
fun getOddCounts(nums: List<Int>) {
  //定义局部变量
    var oddCounts = 0
    var normalCounts = 0

    nums.forEach {
        if (it % 2 == 0) {
            oddCounts++
        } else {
            normalCounts++
        }
    }
    println("oddCounts is $oddCounts,normalCounts is $normalCounts")
}

与Java不一样的是,Kotlin中的在lambda内部可以访问非final变量,甚至可以修改他们。我们称这些变量被lambda捕捉

5.成员引用

Kotlin中可以把函数转换成一个值,这样我们就可以传递一个普通的函数。

Fruit::color
fruits.maxBy(Fruit::color)

这种表达式叫做成员引用,可以创建一个调用单个方法或者访问单个属性的函数值。使用时,用::把类名称和要引用的成员分开。

  1. 成员引用还可以用作引用顶层函数
fun topPrint() {
    println("顶层函数调用")
}

run(::topPrint)
  1. 如果lambda要委托给一个接受多个参数的函数,提供成员引用代替它会非常方便
val action = { fruit: Fruit, message: String -> sendEmail(fruit, message) }

val nextAction = ::sendEmail
  1. 可以用构造方法引用存储或者延迟执行创建类实例的动作
//构造方法引用:在类名前加双引号 ::
val plantFruit = ::Fruit
val f = plantFruit(20)
  1. 可以用成员引用的方式访问扩展函数
fun Fruit.isRed() = color > 15
val test = Fruit::isRed

6.集合的函数式API

函数式编程风格在操作集合时提供了很多优势,大多数任务可以通过库函数完成,来简化代码

filter函数:

遍历集合并选出应用给定lambda后会返回true的那些元素

//使用filter函数,返回偶数集合
val datas = listOf(1, 2, 3, 4, 5)
println(datas.filter { it % 2 == 0 })

filter函数可以从集合中过滤我们不想要的元素,但是并不会改变这些元素

map函数:
  1. 对集合中的每一个元素应用给定的函数,并将结果收集到一个新集合。
//将传入的数字列表,变成他们的平方的列表
val list = listOf(1, 2, 3)
println(list.map { it * it })
  1. 可以对map应用过滤和变换操作
//过滤掉map中 key < 1的元素
val maps = mapOf(1 to "one", 2 to "two", 3 to "three")
println(maps.filterKeys { it > 1 })

filterKeysmapKeys过滤和变换map的键

filterValuesmapValues过滤和变换map的值

all any count find函数

用于检查集合中元素是否符合某个条件

  1. all函数判断是否所有元素满足判断式

    //定义age >= 18对可以吸烟的判断式
    val canSmoke = { h: Humen -> h.age >=18 }
    //检查是否所有人都可以吸烟
    println(humens.all(canSmoke)) 
    
  2. any函数检查集合中是否至少存在一个匹配的元素

    //判断是否有可以吸烟的人
    println(humens.any(canSmoke))
    
  3. find函数返回第一个符合条件的元素

    //查找第一个可以吸烟的人
    println(humens.find(canSmoke))
    
  4. count函数检查有多少元素满足判断式

    //统计可以吸烟的客户
    println(humens.count(canSmoke))
    
groupBy函数

把所有元素按照不同的特征划分成不同的组

val fruits = listOf(Fruit(1), Fruit(2))
println(fruits.groupBy { it.color })
>> {1=[Fruit(color=1)], 2=[Fruit(color=2)]}

groupBy函数的结果是一个map,是元素分组依据的键和集合元素之间的映射。

flatMap函数

处理嵌套集合中的元素:首先根据作为实参给定的函数对集合中的每个元素做变换(或者说映射),然后把多个列表合并(或者说平铺)成一个列表。

val strings = listOf("ac", "abcf", "your")
println(strings.flatMap { it.toList() })
>> [a, c, a, b, c, f, y, o, u, r]

7.惰性集合操作:序列

  1. 序列使用场景

在对集合进行链式操作时,这些函数操作会创建中间集合,每一步的结果都会存在一个临时列表中。当集合元素很多时,这样的调用会是很低效的。

books.map(Book::title)
            .filter { it.length > 2 }

为了提高效率,可以把集合变成序列。

  • 这种方式不会创建储存元素的中间变量
  • 用扩展函数asSequence把任何集合转换成序列,调用toList做反向转换
  • 通常对大型集合进行链式操作时,要是用序列
books.asSequence()
        .map (Book::title)
        .filter { it.length > 2 }
        .toList()
  1. 序列的操作

    序列的操作分为:中间操作和末端操作

    中间操作:对序列执行的一系列变换操作,返回另一个序列,这个新序列知道如何变换原始序列中的元素。中间操作是惰性的。

    //以下代码缺少了末端操作。执行后没有任何打印,证明中间操作未执行
    books.asSequence()
                .map { println(it.title) }
    

    末端操作:返回一个结果,这个记过可能是集合、元素、数字,或者从初始化集合的变换序列中获取的任意对象。只有当末端操作被调用时,中间操作才会执行

    //上述代码加入了 末端操作:toList
    books.asSequence()
            .map { println(it.title) }
            .toList()
    >>  Java
     Kotlin实战
    

对序列来说,所有操作是按照顺序应用在每一个元素上:处理完第一个元素(先映射再变换),然后再处理第二个元素。如果轮到某些元素时已经取到结果,其后面的元素就不会再发生任何变换。

//序列中,第二个元素2变换为4 > 3,此时已经找到了满足的值。剩余的元素就不再变换了
listOf(1, 2, 3, 4).asSequence()
        .map { println(it * it); it * it }
        .find { it > 3 }
>> 1
   4
//对集合操作时,每个元素都经过了map变换操作
listOf(1,2,3,4)
        .map { println(it * it); it * it }
        .find { it > 3 }
>> 1
   4
   9
  16

对集合执行操作时,先使用filter再使用map有助于减少变换总次数。如果map在前,每个元素都会发生变换。

  1. 创建序列

    使用generateSequence可以创建一个序列。通过给定序列的第一个元素,和获取每个后续元素的方式来实现

    //指定前一个元素为0,并给定规则,创建出0~100的数字序列
    val numberTo100 = generateSequence(0) { it + 1 }.takeWhile { it <= 100 }
    //对序列进行求和
    println(numberTo100.sum())
    

8.使用Java函数式接口

在使用Kotlin时,我们可以将Lambda传递给Java方法。Kotlin的Lambda可以无缝的和Java API互操作。

在Java中,我们需要创建一个匿名类的实例作为实参传递给setOnClickListener,实现点击的监听

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //点击后执行的动作
    }
});

Kotlin中,我们可以传递一个Lambda来代替这个实例

button.setOnClickListener{ view -> println("按钮点击") }

这个lambda接受一个View类型的参数,和OnClick方法一样

SAM类型接口

能够直接传递lambda的原因是:OnClickListener是一个SAM类型的接口,即只有一个抽象方法的接口,又称为函数式接口。定义了这种类型的接口,使得以其为参数的的方法,可以在调用时,传递一个lambda表达式作为参数。

  1. 把lambda当做参数传递给Java方法

    可以把lambda传给任何期望函数式接口的方法,编译器会把lambda自动转换为一个实现了当前SAM接口的匿名类的实例,并且将lambda作为SAM接口单抽象方法的方法体

//这个方法需要传递一个Runnable类型的参数
public static void postponeComputation(int delay,Runnable runnable){

}
//Kotlin中调用时,就可以直接传递一个lambda作为实参,编译器会自动把它转换成一个Runnable实例
 Test.postponeComputation(100){
        println("print")
    }

通过显式的创建一个匿名对象也能达到同样的效果,但是这样每次调用都会创建一个新的匿名类的实例

Test.postponeComputation(1000, object : Runnable {
    override fun run() {

    }
})

使用lambda表达式,创建的匿名类实例可以在多次调用之间复用

 Test.postponeComputation(100){
        println("print")
    }
//以上代码等价于 显式声明一个Runnable的实例并存储在一个变量中,则每次调用时使用就是这个实例
val runnable = Runnable { println("print") }
Test.postponeComputation(100,runnable)

当有变量被lambda表达式捕获时,每次调用将不再重用同一个实例了。这种情况下,每次编译器都会创建一个新对象,其中存储着变量的值。

fun caculateResult(number: Int) {
    Test.postponeComputation(10000) {
        println(number.toString())
    }
}
  1. SAM接口的构造器,将lambda显式的转换为函数式接口

    ​ SAM接口的构造器是编译器提供的,他能让你显式的将lambda表达式显式的转换为一个函数接口的实例。当编译器不能自动完成lambda到函数式接口的转换时,可以使用这种显式的转换方式。

    //这个函数需要返回函数式接口,但是不能直接返回lambda,我们需要将它包裹在SAM的构造器中
    fun createSpecificRunnable(): Runnable {
        return Runnable { println("runnable") }
    }
    
     SAM接口构造器的名字和接口的名字相同。构造器只有一个参数----一个lambda表达式,它会被用做SAM接口的单抽象方法的方法的方法体---并且返回一个实现了这个接口的实例。
    

    ​ 除了返回值以外,构造器还可以用在要将由lambda生成的函数式接口保存在变量中时.下面代码中把通过构造器将SAM接口保存在了listener变量中,达到重用的效果。

    val listener = View.OnClickListener { v ->
        val text = when (v.id) {
            R.id.editText -> "text1"
            R.id.editText2 -> "text2"
            R.id.editText3 -> "text3"
            else -> "unknown"
        }
        Toast.makeText(context,text,Toast.LENGTH_SHORT).show()
    }
    
  1. 带接受者的lambda withapply

    在lambda中,不使用任何附加的修饰符就能调用不同对象的方法。这种lambda叫做带接收者的lambda

    with函数

    许多语言中都有特定的语句,能让你在对一个对象执行多次操作时,不用重复写对象的名字来进行操作。Kotlin中以库函数的形式来提供,with函数

    fun alphbet(): String {
    
        val stringBuilder = StringBuilder()
        return with(stringBuilder) {
            for (letter in 'a'..'z') {
                this.append(letter)
            }
            this.toString()
        }
    }
    

上面的with函数 接受两个参数

  • stringBuilder
  • 一个lambda

with函数把它第一个参数转换成作为第二个参数传给它的lambda的接收者。便可以显式的通过this引用来访问这个接收者

apply函数

apply函数几乎和with函数一样,不同的是apply函数返回的作为参数传递给它的对象。

//with函数返回的StringBuilder类型,调用toString()转换成String
fun alphbet2(): String = StringBuilder().apply {

    for (letter in 'a'..'z') {
        this.append(letter)
    }
}.toString()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352