掌握 Java 8 Lambda 表达式

作者: rain 分类: 移动 发布时间: 2016-03-16 23:46 6 条评论

Lambda 表达式 是 Java8 中最重要的功能之一。使用 Lambda 表达式 可以替代只有一个函数的接口实现,告别匿名内部类,代码看起来更简洁易懂。Lambda 表达式 同时还提升了对 集合 框架的迭代、遍历、过滤数据的操作。

匿名内部类

在 Java 世界中,匿名内部类 可以实现在应用程序中可能只执行一次的操作。例如,在 Android 应用程序中,一个按钮的点击事件处理。你不需要为了处理一个点击事件单独编写一个独立的类,可以用匿名内部类完成该操作:

通过匿名内部类,虽然代码看起来不是很优雅,但是代码看起来比使用单独的类要好理解,可以直接在代码调用的地方知道点击该按钮会触发什么操作。

Functional Interfaces(函数型接口)

定义 OnClickListener 接口的代码如下:

OnClickListener 是一个只有一个函数的接口。在 Java 8 中,这种只有一个函数的接口被称之为 “Functional Interface”。

在 Java 中 Functional Interface 用匿名内部类实现是一种非常常见的形式。除了 OnClickListener 接口以外,像 Runnable 和 Comparator 等接口也符合这种形式。

Lambda 表达式语法

Lambda 表达式通过把匿名内部类五行代码简化为一个语句。这样使代码看起来更加简洁。

一个 Lambda 表达式 由三个组成部分:

参数列表 箭头符号 函数体
(int x, int y) -> x + y

函数体可以是单个表达式,也可以是代码块。如果是单个表达式的话,函数体直接求值并返回了。如果是代码块的话,就和普通的函数一样执行,return 语句控制调用者返回。在最外层是不能使用 break 和 continue 关键字的,在循环中可以用来跳出循环。如果代码块需要返回值的话,每个控制路径都需要返回一个值或者抛出异常。

下面是一些示例:

第一个表达式有两个整数型参数 x 和 y,表达式返回 x + y 的值。第二个表达式没有参数直接返回一个表达式的值 42,。 第三个有一个 string 参数,使用代码块的方式把该参数打印出来,没有返回值。

Lambda 示例

Runnable Lambda

来看几个示例, 下面是一个 Runnable 的示例:

这两个实现方式都没有参数也没有返回值。Runnable lambda 表达式使用代码块的方式把五行代码简化为一个语句。

Comparator Lambda

在 Java 中,Comparator 接口用来排序集合。在下面的示例中一个 ArrayList 中包含了一些 Person 对象, 并依据 Person 对象的 surName 来排序。下面是 Person 类中包含的 fields:

下面是分别用匿名内部类和 Lambda 表达式实现 Comparator 接口的方式:

可以看到 匿名内部类可以通过 Lambda 表达式实现。注意 第一个 Lambda 表达式定义了参数的类型为 Person;而第二个 Lambda 表达式省略了该类型定义。Lambda 表达式支持类型推倒,如果通过上下文可以推倒出所需要的类型,则可以省略类型定义。这里由于 我们把 Lambda 表达式用在一个使用泛型定义的 Comparator 地方,编译器可以推倒出这两个参数类型为 Person 。

Listener 表达式

最后来看看 View 点击事件的表达式写法:

注意, Lambda 表达式可以当做参数传递。类型推倒可以在如下场景使用:

  • 变量定义
  • 赋值操作
  • 返回语句
  • 数组初始化
  • 函数或者构造函数参数
  • Lambda 表达式代码块中
  • 条件表达式中 ? :
  • 强制转换表达式

使用 Lambda 表达式提升代码

本节通过一个示例来看看 Lambda 表达式 如何提升你的代码。Lambda 表达式可以更好的支持不要重复自己(DRY)原则并且让代码看起来更加简洁易懂。

一个常见的查询案例

编码生涯中一个很常见的案例就是从一个集合中查找出符合要求的数据。例如有很多人,每个人都带有很多属性,需要从这里找出符合一些条件的人。

在本示例中,我们需要查找符合三个条件的人群:
– 司机:年龄在 16 以上的人才能成为司机
– 需要服役的人:年龄在 18到25岁的男人
– 飞行员:年龄在 23 到 65 岁的人

找到这些人后,我们可以给这些人发邮件、打电话 告诉他们可以来考驾照、需要服役了等。

Person Class

Person 类代表每个人,该类具有如下属性:

Person 类使用一个 Builder 来创建新的对象。 通过 createShortList 函数来创建一些模拟数据。

常见的实现方式

有 Person 类和搜索的条件了,现在可以撰写一个 RoboContact 类来搜索符合条件的人了:

这里分别定义了 callDrivers、 emailDraftees 和 mailPilots 三个函数,每个函数的名字都表明了他们实现的功能。在每个函数中都包含了搜索的条件,但是这个实现由一些问题:

  • 没有遵守 DRY 原则
    • 每个函数都重复了一个循环操作
    • 每个函数都需要重新写一次查询条件
  • 每个搜索场景都需要很多代码来实现
  • 代码没有灵活性。如果搜索条件改变了,需要修改代码的多个地方来符合新的需求。并且代码也不好维护。
重构这些函数

如何改进这些问题呢?如果把搜索条件判断提取出来,放到单独的地方是个不错的想法。

搜索条件判断封装到一个函数中了,比第一步的实现有点改进。搜索测试条件可以重用,但是这里还是有一些重复的代码并且每个搜索用例还是需要一个额外的函数。是否有更好的方法把搜索条件传递给函数?

匿名类

在 lambda 表达式出现之前,匿名内部类是一种选择。例如,我们可以定义个 MyTest 接口,里面有个 test 函数,该函数有个参数 t 然后返回一个 boolean 值告诉该 t 是否符合条件。该接口定义如下:

使用该接口的实现搜索功能的改进代码如下:

这比之前的代码又改进了一步,现在只需要执行 3个函数就可以实现搜索功能了。但是调用这些代码需要使用匿名内部类,这样调用的代码看起来非常丑:

这就是大家深恶痛绝的匿名内部类嵌套问题,五行代码中只有一行是真正有用的代码,但是其他四行模板代码每次都要重新来一遍。

Lambda 表达式派上用场了

Lambda 表达式可以完美的解决该问题。前面我们已经看到了 Lambda 表达式如何解决 OnClickListener 问题的了。

在看看这里 Lambda 表达式如何实现的之前,我们先来看看 Java 8 中的一个新包:[java language=”.util.function”]/java

在上一个示例中,MyTest functional interface 作为函数的参数。但是如果每次都需要我们自己自定义一个这样的接口是不是比较繁琐呢? 所以 Java 8 提供了这个 java.util.function 包,里面定义了几十个常用的 functional interface。这里 Predicate 这个接口符合我们的要求:

test 函数需要一个泛型的参数然后返回一个布尔值。过滤一个对象就需要这样的操作。下面是如何用 Lambda 表达式实现搜索的代码:

这样使用 Lambda 表达式就解决了这个匿名内部类的问题,下面是使用 Lambda 表达式来调用这些搜索函数的代码:

上面的示例代码可以在这里下载:RoboCallExample.zip

java.util.function 包

该包包含了很多常用的接口,比如:
– Predicate: 判断是否符合某个条件
– Consumer: 使用参数对象来执行一些操作
– Function: 把对象 T 变成 U
– Supplier:提供一个对象 T (和工厂方法类似)
– UnaryOperator: A unary operator from T -> T
– BinaryOperator: A binary operator from (T, T) -> T

可以详细看看这个包里面都有哪些接口,然后思考下如何用 Lambda 表达式来使用这些接口。

改进人名的输出方式

比如在上面的示例中 ,把找到的人名字给打印出来,但是不同的地方打印的格式要求不一样,比如有些地方要求把 姓 放到 名字的前面打印出来;而有些地方要求把 名字 放到 姓 的前面打印出来。 下面来看看如何实现这个功能:

常见的实现

两种不同打印人名的实现方式:

Function 接口非常适合这类情况,该接口的 apply 函数是这样定义的:

public R apply(T t){ }

参数为泛型类型 T 返回值为泛型类型 R。例如把 Person 类当做参数而 String 当做返回值。这样可以用该函数实现一个更加灵活的打印人名的实现:

很简单,一个 Function 对象作为参数,返回一个 字符串。

下面是测试打印的程序:

上面的示例中演示了各种使用方式。也可以把 Lambda 表达式保存到一个变量中,然后用这个变量来调用函数。

以上代码可以在这里下载:LambdaFunctionExamples.zip

当集合遇到 Lambda 表达式

前面介绍了如何配合 Function 接口来使用 Lambda 表达式。其实 Lambda 表达式最强大的地方是配合集合使用。

在前面的示例中我们多次用到了集合。并且一些使用 Lambda 表达式 的地方也改变了我们使用集合的方式。这里我们再来介绍一些配合集合使用的高级用法。

我们可以把前面三种搜索条件封装到一个 SearchCriteria 类中:

每个 Predicate 示例都保存在这个类中,然后可以在后面测试代码中使用。

循环

先来看看结合中的 forEach 函数如何配合 Lambda 表达式使用:

第一个使用了标准的 Lambda 表达式,调用 Person 对象的 printWesternName 函数来打印名字。而第二个用户则演示了如何使用函数引用(method reference)。如果要执行对象上的一个函数则这种函数引用的方式可以替代标准的 Lambda 的语法。最后一个演示了如何 printCustom 函数。注意查看在 Lambda 表达式里面嵌套 Lambda 表达式的时候,参数的名字是有变化的。(第一个 Lambda 表达式的参数为 p 而第二个为 r)

Chaining and Filters

除了循环迭代集合以外,还可以串联多个函数的调用。如下所示:

先把集合转换为 stream 流,然后就可以串联调用多个操作了。这里先用搜索条件过滤集合,然后在符合过滤条件的新集合上执行循环打印操作。

Getting Lazy

上面演示的功能有用,但是集合中已经有循环方法了为啥还需要添加一个新的循环的方式呢? 通过把循环迭代集合的功能实现到类库中,Java 开发者可以做更多的代码优化。要进一步解释这个概念,需要先了解一些术语:

  • Laziness:在编程语言中,Laziness 代表只有当你需要处理该对象的时候才去处理他们。在上面的示例中,最后一种循环变量的方式为 lazy 的,因为通过搜索条件的对象只有 2 个留着集合中,最终的打印人名只会发生在这两个对象上。
  • Eagerness: 在集合中的每个对象上都执行操作别称之为 eager。例如一个 增强的 for 循环遍历一个集合去处理里面的两个对象,并称之为更加 eager 。
stream 函数

前面的示例中,在过滤和循环操作之前,先调用了stream 函数。该函数把集合对象变为一个 java.util.stream.Stream 对象。在 Stream 对象上可以串联调用各种操作。默认情况下,一个对象被处理后在 stream 中就不可用了。所以一个特定 stream 对象上的串联操作只能执行一次。 同时 Stream 还可以是顺序(默认如此)执行还可以并行执行。最后我们会演示并行执行的示例。

变化和结果(Mutation and Results)

前面已经说了, Stream 使用后就不能再次使用了。因此,在 Stream 中的对象状态不能改变,也就是要求每个元素都是不可变的。但是,如果你想在串联操作中返回对象该如何办呢? 可以把结果保存到一个新的集合中。如下所示:

上面示例中的 collect 函数把过滤的结果保存到一个新的结合中。然后我们可以遍历这个集合。

使用 map 来计算结果

map 函数通常配合 filter 使用。该 函数使用一个对象并把他转换为另外一个对象。下面显示了如何通过map 来计算所有人的年龄之和。

第一种使用传统的 for 循环来计算平均年龄。第二种使用 map 把 person 对象转换为其年龄的整数值,然后计算其总年龄和平均年龄。

在计算平均年龄时候还调用了 parallelStream 函数,所以平均年龄可以并行的计算。

上面的示例代码可以在这里下载: LambdaCollectionExamples.zip

总结

感觉如何?是不是觉得 Lambda 表达式棒棒哒,亟不可待的想在项目中使用了吧。 神马? 你说 Andorid 不支持 Java 8 不能用 Lambda 表达式。好吧,其实你可以使用 gradle-retrolambda 插件把 Lambda 表达式 抓换为 Java 7 版本的代码。 还不赶紧去试试!!

本文出自 云在千峰,转载时请注明出处及相应链接。

本文永久链接: http://blog.chengyunfeng.com/?p=902

Ɣ回顶部