Kotlin 学习之lamabda表达式

一.lamabda初体验

1.假如现在有个需求,需要从一个集合中找出对应的最大的元素
例如找出person类的集合中年龄最大的人,person类如下:

class Person(var name : String,var age : Int) {

    override fun toString(): String {
        return "Person(name=$name,age=$age)"
    }

}

如果用普通的方式,你可能需要遍历集合,判断年龄,然后比较,得出年龄最大的那个人.
假如现在用集合的库函数maxBy.该函数正好接收一个lamabda表达式,maxBy()函数的定义如下:

fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? 

从函数lamabda表达式中可以看到,它会接收一个泛型T作为实参,也就是集合的泛型T,运行结果是一个比较器类型,并且max函数本身返回一个泛型T.
现在我们调用maxBy()尝试下:

fun main(args: Array<String>) {
    val personList = listOf(Person("aaa", 12), Person("bbb", 13))
    println(personList.maxBy{it.age})
}

对,就是这样看起来简单,方法中的it代表person对象,也就是上面的T,当函数接收参数只有一个时,可以用it关键字指代,当然你要写成这样也是可以的:

  println(personList.maxBy { person : Person -> person.age })

并且,lamabda还有一种更简便的形式,当表达式刚好是函数或者属性的委托,可以用成员引用替换,就像这样:

  println(personList.maxBy(Person::age))

注意,这里是括号,而非中括号.

二.lamabda定义

lamabda就是一小段代码的编码形式,你可以称之为一个代码块,因为它总是被一对中括号括起来.它也是一个表达式,所谓表达式就是有值得一个代码块,可以被变量所引用.不过最常见的还是被当做函数参数传递接收lamabda表达式的函数.具体形式如下图:


image.png

它由参数指向函数里,参数也就是需要传递的或者接收的实参
3.lamabda语法与简化
回到上面那个例子,如果说最普通的方式调用maxBy函数,应该是这样,就像函数的定义一样:

 println(personList.maxBy ({person : Person -> person.age}))

但是lamabda表达式规定当函数参数是lamabda表达式并且lamabda表达式作为函数参数的最后一个参数时可以移到括号的外面,就像这样;

println(personList.maxBy () {person : Person -> person.age})

如果lamabda是唯一的参数时,这个括号可以去掉,就像这样:

println(personList.maxBy { person : Person -> person.age })

就成了我们上文中提到的那种写法.
然后由于kotlin中的类型推导,可以去掉person的类型声明:

println(personList.maxBy { person -> person.age })

因为maxBy()函数中表达式的参数时泛型T,kotlin会自动推导出这个参数名称person的类型就是Person
然后如果这个lamabda只接受一个参数,且这个参数可以用类型推导推导出来,可以用it指代这个参数.就像上面那样写法.这个叫做it约定.
而且,lamabda不是只可以有一行语句,可以有多行,最后一行作为结果返回,就像这样:

println(personList.maxBy { 
pritln("aaaaa")
person : Person -> person.age 
})

三.lamabda在作用域中访问变量

lamabda表达式很多时候用于Java中匿名内部类表达形式,也就是说可以用lamabda表达式替换匿名内部类.
但是,这里需要注意的是,java中匿名内部类访问函数中的形参,或者函数定义的变量是要定义成final类型的,但是在lamabda中不需要,一样可以访问并且修改.就像这样:

fun forEachList(ageList: List<Int>) {
    var prefix : String = ""
    ageList.forEach {
        prefix = "bbb"
        print(it)
    }
}

在lamabda中访问函数中定义的局部变量并修改其中的值,这种方式叫变量被lamabda表达式捕捉.而在java中只能捕捉final类型的变量

捕捉的定义:所谓捕捉就是如果这个变量是final类型的变量,它会和lamabda一起被保存下来稍后执行,如果这个类型是个可变变量,它会被一个类包装器引用起来,然后这个引用会和lamabda一起被保存下来,然后你就可以修改它的值.就像定义一个final类型的集合,集合是不可变的,但集合里面的元素是可变的

 val myList : MutableList<Int> = mutableListOf()
 myList.add(1)

成员引用

上文已经提到过成员引用的实例.
所谓成员引用就是当lamabda中调用的是一个方法或者一个属性值时可以用一种简明语法代替.例如上例中maxBy函数中访问的是person.age,那么此时age就是person中的一个属性.就如这样:

  println(personList.maxBy(Person::age))

这里要注意: 不管是引用的成员还是函数,都不要在引用的名称后面加括号.
成员引用顶层函数:

fun book() {
    println("KOTLIN")
}

run(::book)

如果一个lamabda需要把一个或者多个参数委托给一个函数,如下面这样:

    val action = {student : Student,message : String ->
        sendEmail(student,message)
    }

这时候使用成员引用会非常方便:

val nextAction = ::sendEmail

此时::sendEmail等价于下面这个lamabda表达式:

 {student : Student,message : String ->
      sendEmail(student,message)
 }

构造方法引用存储或者延期创建类的实例:

class Cap(name : String)

fun main(args: Array<String>) {
    //创建对象被保存成一个值
    val createCap = ::Cap
    val cap = createCap("OCap")
}

还可以用来引用扩展函数:

fun Cap.readerName() {
    println(name)
}

val readerCap = Cap::readerName

集合的库函数使用lamabda表达式

filter函数
filter顾名思义就是过滤的意思,过滤掉不想要的数据,filter的lamabda表达式中定义的事过滤条件,不符合条件的元素将会被剔除掉.如下条件:

val numberList = listOf(1,2,3,4,5,6)
 val filterList = numberList.filter { it % 2 == 0 }

打印结果:
[2, 4, 6]
map函数
map简单理解就是映射的意思,即把一个元素映射(变换)成另一个元素,就像这样:

//每个元素变成它的平方
 val mapList = numberList.map { it * it }

打印结果:
[1, 4, 9, 16, 25, 36]
map也可以过滤元素,只不过是讲元素过滤或者也可称之为变换成另一个元素,这里的过滤指的是过滤元素的属性.如下面这样:

val personList = listOf(Person("bob",21), Person("tina",21))
val mapPerson = personList.map { it.age }

打印结果:
[21, 21]
通过使用mapOf建立集合:

    //通过mapOf函数建立集合
    val mapOfList = mapOf(0 to "aaa",1 to "bbb")
    //{0=aaa, 1=bbb}

通过mapValues映射集合的values

    //通过mapValues映射集合的value
    val mapValueList = mapOfList.mapValues { it.value.toUpperCase() }
    //{0=AAA, 1=BBB}

此外,还有fileterKey,filterValue,mapKey也是同样的道理,用于过滤,变换key和value值
all函数
all用来判断所有元素是否满足某一条件,返回一个布尔值,例如判断集合中人的年龄是否大于20:

val all = personList.all { it.age > 20 }

打印结果:
true
any函数
any用来判断至少有一个元素满足某一条件,返回一个布尔值:

val any = personList.any { it.age > 21 }

打印结果:
false
count函数
count函数用来判断集合中满足条件的元素个数,当然你也可以先过滤元素,然后用.size方法来统计个数,但是这样会创建一个中间集合,而count函数只会用来跟踪元素的个数,而不关心元素本身,所有更加的高效

val count = personList.count { it.age > 19 }

打印结果:
2
find函数和firstOrNull函数
find用来发现集合中是否包含满足某一条件的元素,如果满足,返回第一个找到的元素,如果不满足则返回null.也可以用firstOrNull函数,作用是一样的,而且方法表现更明确

    val find = personList.find { it.age == 21 }
    println(find)

    val findOrNull = personList.firstOrNull{ it.age > 21 }
    println(findOrNull)

groupBy函数
groupBy函数可以用来对元素进行分组,可以把相同分组条件的元素分为同一组,然后结果是一个map,key是分组的条件值,value是符合这一条件的列表:

 val personList1 = listOf(Person("bob",20), Person("tina",19))
 val groupBy = personList1.groupBy { it.age }

打印结果:

{20=[Person(name=bob,age=20)], 19=[Person(name=tina,age=19)]}

flatMap函数
flatMap主要是实现两步操作,首先map也就是映射或者说变换成满足条件的集合,之后再flat也就是将所有元素平铺成一个集合:

    val alist = listOf("abc","def","ghi")
    var flatMap = alist.flatMap { it.toList() }
    println(flatMap)

打印结果如下:

[a, b, c, d, e, f, g, h, i]

惰性操作集合Sequence

定义:Sequence是一个接口,表示是一个可以逐个列举元素的元素序列,它只有一个方法iterator,用来从序列中获取值.
优点: 避免创建中间集合,对数据量较大的集合做中间操作(过滤,变换)更高效.并且由于执行包含惰性,那末端操作未执行之前,所有的中间操作将不会执行.
所谓中间操作,末端操作:


image.png

示例如下所示:

val alist = listOf("abc","def","ghi")
val sequence = alist.asSequence().map { it.toUpperCase() }.filter { it == "ABC" }
sequence.toList()

在toList操作之前,map,filter操作都是延迟执行的,也就是说此事不会执行这些变换操作,只有在结果操作执行后才会执行.
另外一个和集合的区别,计算顺序:
对集合来说,比如下面这段代码:

alist.map { it.toUpperCase() }.filter { it == "ABC" }

先对全部的元素执行map操作,再对所有元素执行filter操作
对序列来说,下面代码:

alist.asSequence().map { it.toUpperCase() }.filter { it == "ABC" }

对每一个元素顺序运用map操作和filter操作,处理完一个元素,再去处理另一个元素.
例如如下代码操作:

val filterList1 = listOf(1, 2, 3, 4).map { it * it }.find { it > 3 }

执行过程如下:
1.对1执行map操作得到1,调用find函数判断是否大于3,结果是否
2.对2执行map操作得到4,调用find函数判断是否大于3,结果满足,返回,操作执行完毕,接下来的3,4不会再执行操作
流程图如下:


image.png

集合操作又被称为及早操作,序列操作相对而言又被称之为惰性操作.
创建序列,除了在集合上运用asSequence操作,还可以使用generateSequence函数,该函数生成序列并根据条件生成下一个元素.举一个小例子:

//算出0到100求和
generateSequence(0) { it + 1 }.takeWhile { it <= 100 }.sum()

这里同样注意下调用sum操作后之前的求值才会执行,也就是序列的惰性操作特性

lamabda使用,函数式接口

所谓函数式接口,如下所示:

    public interface OnFocusChangeListener {
        void onFocusChange(View v, boolean hasFocus);
    }

像上面这样的只有一个方法的接口,称为函数式接口,也叫作单方法接口(SAM接口).在需要SAM接口作为参数时使用lamabda一般会更为方便也让代码看起来更简洁,符合习惯.
比如先定义下面的函数:

fun postPone(delay: Int,runnable: Runnable) {
    println(delay)
    runnable.run()
}

然后调用这个函数,先按常规方式,定义一个匿名内部类:

   postPone(100, object: Runnable {
        override fun run() {
            println()
        }
    })

再按lamabda调用方式:

postPone(100, Runnable { println() })

比较两种方式:
1.调用方式上,第一种明显代码更多,而且语法更复杂,而第二种就简单的多
2.从性能上,此时第二种相比较第一种方式不会每次都创建runnable对象,如果lamabda表达式没有访问定义它的函数的变量,那么这个匿名对象不会每次都创建而是可以重用.
如果访问了包围它的作用域中捕捉了变量,那么这个变量会被保存,那么每次都会创建这个对象,此时就等价于第一种了,就像下面这样:

fun handId(id: Int) {
    postPone(100, Runnable { println(id) })
}

此时每次调用都会创建对象.如果没有捕捉外部函数的变量,那么这个lamabda所代表的对象就是单例的,如果捕捉了变量,那么这个lamabda所代表的class文件会创建多个对象,并且类中会生成对应的字段用来保存这个值.lamabda底层会被编译成一个class文件.
上面的这段代码:

Runnable { println(id) }

这个用法叫做SAM的构造函数.SAM构造方法只接收一个参数,一个被用作方法体的lamabda表达式,并返回实现了这个接口的一个实例.

带接收者的lamabda

with函数

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

接收两个参数:一个是接收者,一个是lamabda表达式,第一个参数会作为lamabda表达式的接受者,所谓接收者就是lamabda中作为this所指代的对象,在表达式中可以显示使用this,或者省略this直接调用接收者也就是第一个参数的方法
例如有下面这个例子,构建一串字符串:

fun buildString() : String {
    val stringBuilder = StringBuilder()
    stringBuilder.append("aaa")
    stringBuilder.append("bbb")
    stringBuilder.append("ccc")
    return stringBuilder.toString()
}

现在用with函数改写这段代码:

fun buildStringWith() = with(StringBuilder()){
    append("aaa")
    append("bbb")
    append("ccc")
    toString()
}

其中返回值就是with函数中最后一行代码
如果要引用外部类的方法,例如toString(),如下所示:

//其中Outer代表外部类的类名Outer
this@Outer.toString()

apply函数

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

如果你要返回的不是lamabda的执行结果,而是接收者对象,那么这个时候需要用到apply函数,apply函数返回是这个接收者对象.如函数定义的最后一行返回的是this,同时,apply函数被声明成的是一个扩展函数.
使用场景:apply函数可以在任意对象上使用,它通常用来构建一个对象,就像java中的构造者模式一样,创建符合条件的对象.还有一些标准的库函数就是实现一些像上面一样的具体功能,例如buildString函数就是StringBuilder中一个已有的标准库函数.

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

推荐阅读更多精彩内容