java to kotlin (2) - lambda编程

前言

Kotlin Note 是我学习kotlin整理笔记向博客,文章中的例子参考了kotlin in action这本书籍,同时我也极力推荐这本书,拥有Java基础的人可以很快的就使用kotlin来完善自己的编程技巧。

不过我不想让博客变成简单的复制粘贴笔记,因此对内容进行了精简,同时增加了与Java的对比和转换,一些详细内容不会整理出来,详细的内容我觉得查阅api和翻书就可以了。

基础预备知识

博客中的例子需要一些简单的基础知识包括如下

Java8基础

  • Java8中函数式编程的一些基本知识,例如lambda表达式 函数接口 Stream Api等等,可以参考我之前写的
    Java8函数之旅中的 篇或者简单的查阅相关资料即可。

kotlin中的变量

  • kotlin中变量的类型可以由编译器推导,只需要使用var val关键字来标注变量与不可变量即可,如果需要显示的标注,用冒号隔开写在变量后面。( 只要不加逗号都可以叫做一句话吧... :) )例如
    var a = 5 //variable 可变量
    val b = 10 //value   不可变量
    var c : String = "Hello World" // 显示的指定String类型
    var d = "Hello World" // 编译器会推导d的类型为String

data class

  • data class 等于java bean的简化写法,大家都知道一个java bean应当拥有get set toString equals hashCode copy 等等方法与特性,data class就是利用关键字data在语言层面上构建了一个java bean,如果有用过lombok的小伙伴应该会熟悉(lombok中在Java类上使用注解@Data在运行阶段生成以上说的一些方法来简化Java开发)

lambda表达式 语法

Java中的lambda语法

Java8中的lambda语法还是很简洁的,与kotlin中的也十分相似,可以参考开始认识lambda,这里举简单的例子。

Consumer<String> out = (String s) -> {System.out.println(s)};//标准lambda表达式
Consumer<String> simpleOut = s -> System.out.println(s);//简化版
Consumer<String> methodRefOut = System.out::println; //方法引用版本

Java中的lambda表达式本质上是匿名内部类,从上面的例子也可以看到我使用了Consumer这个类来接收lambda表达式。

kotlin中的lambda语法

kotlin中的lambda表达式与Java中的十分类似,相同的例子如下。

val out = { x: String -> System.out.println(x) } //普通lambda表达式用{}包裹
val intOut: (String) -> Unit = { println(it) } //使用it来代替参数
val methodRefOut: (String) -> Unit = System.out::println //方法引用
  • kotlin中lambda表达式使用{}来包裹,其余与Java基本相同
  • 如果只有一个参数,kotlin中可以使用默认的it来代替参数
  • 第三行使用了方法引用,不同点是kotlin中的lambda表达式是函数而非匿名内部类,可以从(String)->Unit的类型可以看出

演示

下面使用一段集合操作的例子来演示,我们首先构建一个Personjava bean

data class Person(val name: String, val age: Int)

非常简洁,是吧?我们的类Person拥有姓名和年龄,现在我们有这么一个需求,给定一个PersonList集合,寻找到其中年龄最大的人并打印出来,下面看看不一样的做法。

命令式的操作 in Java

    public static void findOldestPerson(List<Person> personList) {
        int maxAge = 0;
        Person theOldest = null;
        //遍历集合,如果发现有年龄更大的人就更新最大值
        for (Person p : personList) {
            if (p.getAge() > maxAge) {
                maxAge = p.getAge();
                theOldest = p;
            }
        }
        System.out.println(theOldest);
    }

    public static void main(String[] args) {
        List<Person> personList = Arrays.asList(
                new Person("jack", 22),
                new Person("rose", 25),
                new Person("Tom", 19));
        
        findOldestPerson(personList);
    }

输出结果

Person(name=rose, age=25)

这里可以注意到我们在java的代码中使用了kotlin中定义的data class,并且使用效果很不错。
可这样命令式的代码还是有点多了,并且这样的逻辑我们可能会经常用到,因此将之归纳整理成为类库再复用是更好的选择。

使用Java8的stream api

import static java.util.Comparator.*;

        List<Person> personList = Arrays.asList(
                new Person("jack", 22),
                new Person("rose", 25),
                new Person("Tom", 19));

        Person theOledest = personList.stream()
                .max(comparingInt(Person::getAge))
                .get();
        System.out.println(theOledest);

这里使用了max方法,传入了一个比较器,详细知识可以查看Java8函数之旅 (五) -- Java8中的排序

使用kotlin的lambda

    val personList = listOf(Person("jack", 22), Person("rose", 25))
    println(personList.maxBy{p:Person -> p.age })//标准写法
    println(personList.maxBy{it.age})//使用it来简化参数
    println(personList.maxBy(Person::age)) //方法引用

上面使用了几种不同的写法来表述这个操作,这里选出标准写法

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

这段代码的可读性还是很好的,花括号中是lambda表达式,表示对于maxBy函数来说,要比较的是Person的年龄。但语法上还是有一些啰嗦,这里一步一步的进行简化

  • 过多的标点符号破坏了可读性
    对于这一点,kotlin中约定如果lambda表达式是函数调用的最后一个参数,参数可以放到括号外面
personList.maxBy(){p:Person -> p.age }

并且当lambda表达式是函数的唯一一个参数时,可以去掉空括号

personList.maxBy{p:Person -> p.age }
  • 类型可以从上下文推断,不需要显示的指明
personList.maxBy{p -> p.age }
  • 当只有一个参数时,可以用it的默认名称
personList.maxBy{it.age }

不过值得注意的是,it的这种写法虽然可以简化你的代码,但是会降低可读性,因此根据实际情况来考虑使用哪一种。

常见的集合函数api

函数式的编程风格在集合操作中有很多优势,大部分的操作都可以利用类库来完成,简化代码,提升效率。下面介绍一些基本常用的操作,事实上这些操作几乎存在在任何支持lambda表示的语言中,例如c# scala java8等等,因此如果熟悉这些概念,简单的看看语法就ok了:)

基础操作 filter , map

这两个想必大家是十分熟悉了,过滤与映射,用法也与Java中的用法十分类似,例子如下。

  • filter 过滤
    val list = listOf(1, 2, 3, 4, 5)
    println(list.filter { it % 2 == 1 })
    
// 选出所有的奇数 result : [1, 3, 5]
  • map 映射
    val list = listOf(1, 2, 3, 4, 5)
    println(list.map { it * 2 })
    
// 集合元素的值都翻倍 result : [2, 4, 6, 8, 10]

对集合进行判断 all,any,count,find

  • all 检查所有元素是否满足条件
    val list = listOf(1, 2, 3, 4, 5)
    println(list.all { it > 0 }) // result : true
    println(list.all { it > 1 }) // result : false
  • any 检查是否有任意一个元素满足条件
    val list = listOf(1, 2, 3, 4, 5)
    println(list.any { it == 5 }) // result : true
  • count 获得满足条件的元素的个数
    val list = listOf(1, 2, 3, 4, 5)
    println(list.count { it >= 3 }) //result : 3
  • find 获得第一个满足条件的元素
    如果找到了就返回第一个满足条件的元素,如果没找到就返回null,因此find拥有一个同义的apifirstOrNull
    val list = listOf(1, 2, 3, 4, 5)
    println(list.find { it >= 1 }) //result : 1
    println(list.firstOrNull { it >= 10 }) //result : null

groupBy 将List分组为map

groupBy可以将集合中的元素按照元素的某一个属性记性分类,相同的属性存在一个key中,例子如下

    val persons = listOf(Person("jack", 22),
            Person("jack", 28),
            Person("rose", 25))

    persons.groupBy { it.name }
            .forEach{key, value -> println("key : $key -> value : $value") }

// result : 
//key : jack -> value : [Person(name=jack, age=22), Person(name=jack, age=28)]
//key : rose -> value : [Person(name=rose, age=25)]

可以看到生成的map是按照Person的姓名进行分组

flatMap , flatten

  • flatMap 映射然后平铺
    flatMap 与前面提到的map操作很像,区别在于map只有一个操作,那就是映射。而flatMap是在映射完之后,进行了合并(平铺)的操作,例子如下
    val lists = listOf(listOf(1, 2), listOf(3, 4))
    println(lists.map { it.map { it * 2 } })
    // result : [[2, 4], [6, 8]]
    println(lists.flatMap { it.map { it * 2 } })
    // result : [2, 4, 6, 8]

上面的例子有一点绕口,lists里面包含里2个集合也就是[[1,2],[3,4]],使用map只能将里面的元素给映射,却不能将这2个集合给整合(平铺)成一个集合,而flatMap就可以做到,相信通过结果这其中的区别应该很容易发现

  • flatten 平铺
    如果你只想平铺不想映射,就可以使用flatten
    val lists = listOf(listOf(1, 2), listOf(3, 4))
    println(lists.flatten())
    //result : [1, 2, 3, 4]

Sequence 与 Stream

上面的集合操作api很容易联想到Java8中的stream流 api,可事实这两者并不完全一样,Java8中的流apilazy延迟操作的。lazy操作是函数编程中一个很常见也很有用的操作,上文介绍的这些api并不是lazy的,如果想转换为惰性的话,这时候Sequence就派上用场了。(ps : 关于惰性求职与及早求值可以查看Java8函数之旅 (二) --Java8中的流外部迭代与内部迭代这一小段。
因此我觉得在这里用Java8中的StreamSequence做类比是最合适不过的了。

下面的例子中使用这样的一个peron 集合来做操作

    val persons = listOf(Person("jack", 22), Person("rose", 25))

normal filter,map

    persons.map(Person::name).filter { it.startsWith("j") }

上面的这段操作很简单,首先将person集合映射成了他们名字的字符串集合,接着过滤出名字以j开头的字符串,通过翻看kotlin官方文档可以得知,上面这段操作会生成2个列表,一个用于保存filter的结果,一个用于保存map的结果。如果数据量不多的话并没有什么问题,可如果数据量十分大的话,这样的操作调用就不合适了。

因此我们需要将这样的操作变成了java8中的stream流式操作,在这里我们使用asSequence转为序列操作

sequence filter,map

    persons.asSequence()
            .map(Person::name)
            .filter { it.startsWith("j") }
            .toList()

首先将集合转换为sequence,接着进行一系列惰性求值操作,最后附加一条及早求值再转换为集合,这样的代码和java8中的真的太类似了,下面贴一段java8版本的。

stream filter,map

        persons.stream()
                .map(Person::getName)
                .filter(name -> name.startsWith("j"))
                .collect(toList());

相似度高达99% ! 其实也没有99%啦 :)

区别与使用场景

既然sequencestream这么类似,那么应该怎么选择呢?

  • 如果你是Java的老版本也想体验一下函数式编程与流式操作的快感,那么毫无疑问你只有sequence选择啦(stream是基于Java8的,而kotlin是基于Java6的)

  • Java8中流的过人之处在于提供了十分方面的并行流,只需要使用parallelStream()即可使用多核CPU来计算啦~~ 而这一点sequence中并没有提供

因此究竟怎么选择,还是要看你的Java版本和实际需求

SAM

解释

SAM这个词听起来很高端,也很不让人理解,其实简而言之就是,当你的kotlin的代码在调用java的一些函数接口的时候,可以无缝转换(这一点其实编程者不会明显的感觉到,因为是编译器在作用)

SAM的全称Single Abstract Method Conversions,翻译过来单抽象方法(接口)转换,那大家都清楚,在Java8中,如果你的接口只有一个抽象方法(未实现的方法),那么这样的接口就称之为函数式接口,换言之,这样的接口作为参数时,你可以直接传递lambda表达式
例如在Java

new Thread(() -> System.out.println(123))

正是因为thread的参数时一个实现runnable接口的类,而runnable接口的源码如下

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

无论是从注解和方法签名都可以断定,这就是一个函数式接口,所以当kotlin在调用这类api的时候,编译器会100%的编译成Java版本的字节码以达到无缝转换的作用。再说的直白点就是,kotlin中的lambda表达式不是匿名内部类,但Java8中的函数接口却是的,因此在Java8中存在一些方法(例如上面提到的thread)会接受这些看起来像lambda参数而实际上匿名内部类的函数接口,而当kotlin调用这些方法的时候,编译器就会将kotlin的纯正lambda转化为匿名内部类以达到适配的效果。

验证

例子如下

    val number = 5
    Thread{ println(number) }

这是一段kotlin构建线程的代码,使用kotlin的字节码工具查看字节码

    LINENUMBER 8 L2
    NEW java/lang/Thread
    DUP
    NEW BlogKt$main$1
    DUP
    ILOAD 1
    INVOKESPECIAL BlogKt$main$1.<init> (I)V
    CHECKCAST java/lang/Runnable
    INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V
    INVOKEVIRTUAL java/lang/Thread.start ()V
   L3

可以看到

    CHECKCAST java/lang/Runnable
    INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V

很明显的生成转换了Runnable的实例,我们再将字节码反编译成Java代码

      final int number = 5;
      (new Thread((Runnable)(new Runnable() {
         public final void run() {
            int var1 = number;
            System.out.println(var1);
         }
      }))).start();

验证了这一理论,字节码的checkcast就是强转的(Runnable),下面一行就是生成Runnable实例

一句话总结

SAM就是kotlin在调用Java的函数式接口的时候,能够准确的将kotlin中的lambda表达式转化为对应的Java的匿名内部类的一种编译器的操作。

带接收者的lambda表达式

kotlin中有很多扩展性很高并且很有趣的函数,这些函数可以简化你的代码,同时也是强大的DSL的基础。这里介绍2个,一个是with函数,一个是applay函数。

例子

现在我们要构建一个字母表的函数,初始代码如下

fun alphabet(): String {
    var result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\n字母表构建好了")
    return result.toString()
}

fun main(args: Array<String>) {
    println(alphabet())
}
// 输出结果
>>> ABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> 字母表构建好了

通过观察可以发现函数alphabet中调用了很多次result实例的方法,因此result这个词语反复的在出现,这时候我们就可以通过with函数来简化这段代码

with 函数

代码如下

fun alphabet(): String {
    var sb = StringBuilder()
    return with(sb) {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\n字母表构建好了")
        this.toString()
    }
}

语法with(sb){ }看起来感觉像是一种新的语法结构,其实并不是,这只是函数调用,我们观察一下with函数的函数签名

public inline fun <T, R> with(receiver: T, block: T.() -> R): R

发现最后一个参数是lambda表达式,那么根据之前介绍kotlin中lambda表达式的第二条,如果一个函数的最后一个参数是一个lambda表达式,那么可以将参数的花括号移到外面。这样一解释是不是就清楚了许多?sb是with的第一个参数,而后面花括号的就是第二个参数,也就是一段lambda表达式。

这个方法签名值得让人注意的是这一段with(receiver: T, block: T.() -> R) T.()的意思是第二段的lambda表达式的默认参数就是前面的receiver,因此上面的代码with(sb) 后面的这一段lambda表达式中默认方法的调用者都是stringbuilder

我们再对上面的代码做一点改进

fun alphabet(): String {
    return with(StringBuilder()) {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\n字母表构建好了")
        toString()
    }
}

第一个参数直接将构造函数结果赋予进去,最后一行省略this

apply 函数

apply函数与with函数十分类似,区别在于with的返回值是lambda表达式的返回值,也就是lambda表达式的最后一行,而apply的返回值是调用者本身,观察方法签名也可以得出这个结论。

public inline fun <T> T.apply(block: T.() -> Unit): T

下面我们用applay函数来写上面这个例子

fun alphabet(): String {
    return StringBuilder().apply {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\n字母表构建好了")
    }.toString()  //区别就在于返回值,这里在外面调用toString
}

在这里apply接受stringBuilder然后lambda表达式里默认参数就是stringBuilder最后返回值也是stringBuilder

apply在很多时候都很有用,例如其中一个场景就是在初始化一些属性的时候,例如在安卓中初始化一个textView

fun createViewWithCustomAttributes(context: Context) = 
    TextView(context).apply{
        text = "Sample Text"
        textSize = 20.0
        setPadding(10, 0, 0, 0)
    }

使用buildString

withapply函数式最基本与最通用的附带接受者的lambda函数,事实上很多类库中对这些基础函数进行了封装因此会出现很多很好用的封装之后的接收者函数,这类函数众多,没法一一将来,也没什么必要,大家可以自行查阅api以及相关资料: ) 这里我们还是用上面的例子来讲解,使用buildString来构造
先看看它的签名与代码

public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
        StringBuilder().apply(builderAction).toString()

观察签名发现这个函数就是为你省去了创建stringBuilder与结尾的toString操作,这下子就容易理解了,使用buildString构建字母表的代码如下

fun alphabet(): String {
    return buildString {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\n字母表构建好了")
    }
}

默认的为你提供了stringBuilder以及结束时的toString,你只需要负责构建逻辑就OK了

DSL的强力构建武器

这类带接收者的lambda是构建dsl的强力武器,在后面的部分我也会给出dsl的例子来构建属于自己的语言,其中就大量的利用到了这个特性。

总结

本篇博客是一篇整理向博客,参考了kotlin in action这本书的第五章节,同时将kotlin中的lambda与java8中的lambda进行了对比,可以发现两者之间的差别并不是很大,因此作为一个熟悉java语言的人是很可以很快的适应kotlin的。本篇的核心知识点如下

  • kotlinjava8 lambda语法的区别
  • 一些常用的集合函数api
  • SequenceStream的异同
  • 利用SAM进行java版本与kotlin版本的lambda的无缝转换
  • kotlin中灵活多变的带接收者的函数

如果你能阅读完本篇,希望能激起你对kotlin语言的一点兴趣 : )

转载于:https://www.cnblogs.com/invoker-/p/8410587.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值