美文网首页
Kotlin 学习之lamabda表达式

Kotlin 学习之lamabda表达式

作者: 天蝎_少 | 来源:发表于2018-12-25 17:51 被阅读0次

    一.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中一个已有的标准库函数.

    相关文章

      网友评论

          本文标题:Kotlin 学习之lamabda表达式

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