为了账号安全,请及时绑定邮箱和手机立即绑定

Java 8 新语法习惯 (Lambda表达式的替代方案)

标签:
Java Android

Lambda 表达式广泛用在函数式编程中,但它们很难阅读和理解。在许多情况下,lambda 表达式存在只是为了传递一个或多个形参,最好将它替换为方法引用。在本文中,将学习如何识别代码中的传递 lambda 表达式,以及如何将他们替换为相应的方法引用。方法引用的使用需要学习,但是长期收益将会大于你的付出。

传递 lambda 表达式是什么?

在函数式编程中常常传递 lambda 表达式作为匿名函数,使用 lambda 作为高阶函数的实参。

示例我们将 lambda 表达式传递给 filter 方法:

public class LambdaDemo {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        numbers.stream()
        .filter(e -> e % 2 == 0)
        .forEach(e -> System.out.println(e));
    }

}

在这段代码中,我们将一个 lambda 表达式传递给 forEach 方法。尽管这两个 lambda 表达式明显具有不同的作用,但它们之间还有另一个重要的细微区别:第一个 lambda 表达式执行了实际的工作,而第二个没有。传递给 forEach 方法的 lambda 表达式就是我们所称的传递 lambda 表达式。表达式 `e -> System.out.println(e) 将它的形参作为实际参数传递给了 println 方法。这个表达式没有任何的错误,但是它的语法相对于这个任务而言过于复杂。为了理解 (parameters) -> body 的用途,我们需要进入 body (也就是表达式右侧) 来查看这个形参发生了什么变化。如果 lambda 表达式没有对该形参执行任何的操作,则努力就是白费的。

在这种情况下将 lambda 表达式替换为方法引用会比较有益。不同于方法的调用,方法引用指的是我们传递形参的方法。使用方法引用也会带来各种各样的形参传递方式。

重写前面的代码,通过方法引用传递形参:

public static void main(String[] args) {
        // TODO Auto-generated method stub
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        numbers.stream()
        .filter(e -> e % 2 == 0)
        .forEach(System.out::println);
    }

执行结果和上面的例子一样。使用方法引用会减少理解代码的工作,随着编写和阅读更多的代码好处会倍增。

传递形参作为实参

下面我们将讨论传递 lambda 表达式的变形,将介绍如何将每种表达式替换为方法引用。

实例方法的实参

lambda 表达式将其形参作为实参传递给实例方法,这非常的常见。在上面的例子中,形参 e 作为实参传递给 println 方法。在变形版本中我们将这个 lambda 表达式替换为方法引用 System.out::println

下面的图展示了 lambda 表达式将形参作为实参传递给实例方法:

图片描述

如果不熟悉方法引用,查看这样的 lambda 表达式能够帮助理解它的结构和形参传递到何处。要将 lambda 表达式更改为方法引用,只需删除通用部分(形参和实参),并在方法调用上将点替换为冒号。

this 上的一个方法实参

前面的例子传递表达式的一种特殊情况是,在当前方法的 context 实例上调用实例方法。假设我们有一个名为 Example 的类,其中包含一个实例方法 increment:

public class Example {
  public int increment(int number) {
    return number + 1;
  }

  //...
}

现在假设我们有另一个实例方法,我们在其中创建了一个 lambda 表达式并将它传递给 stream 的 map 方法,如下:

.map(e -> increment(e))

这可能不是那么的显而易见,但是这个代码结构非常类似于上一个示例。我们都将形参作为实参传递给实例方法。稍微重写此代码,以让相似性更加明显:

.map(e -> this.increment(e))

通过引入 this 做为对 increment 的调用,让表达式的结构变动更加清晰。我们再次以方法引用的方式重写这个代码:

.map(this::increment)

与将 e -> System.out.println(e) 替换为 System.out::println 非常相似的是,可以将 lambda 表达式 e -> increment(e)(或更准确地讲 e -> this.increment(e)) 替换为 this::increment。在两种情况下,代码都更加清晰。

静态方法的实参

上面的例子我们替换的 lambda 表达式将一个形参作为实参传递给实例方法。也可以替换将形参传递给静态方法的 lambda 表达式。

将形参传递给静态方法:

.map(e -> Integer.valueOf(e))

这个例子,我们将 lambda 表达式的形参作为实参传递给了 Integer 类的 valueOf 方法。区别在于,在这个例子中,被调用的方法是静态方法而不是实例方法。与前面的办法一样,我们将这个例子替换为方法引用,只不过注意细节这个方法引用并不是放在实例上,而是将它放在一个类上。

对静态方法的方法引用:

.map(Integer::valueOf)

总结一下:如果 lambda 表达式的目的仅是将一个形参传递给实例方法,那么可以将它替换为实例上的方法引用。如果传递表达式要传递给静态方法,可以将它替换为类上的方法引用。

将形参传递给目标

可能在两种不同的场景中使用 ClassName::methodName 格式。上面看到的是第一种格式,其中的形参作为实参传递给静态方法。现在我们考虑一种变形:形参是方法调用的目标。

使用形参作为目标:

.map(e -> e.doubleValue())

下面是这种 lambda 表达式的结构:

图片描述

模糊性和方法引用

查看方法引用,不容易确定形参传递给了静态方法还是用作了目标。要了解区别,我们需要知道方法是静态方法还是实例方法。从代码可读性角度讲,这不那么重要,但知道该区别对成功编译至关重要。

如果一个类的一个静态方法和一个兼容的实例方法同名,而且我们使用了方法引用,则编译器将认为该调用模糊不清。所以,举例而言,我们不能将 lambda 表达式 (Integer e) -> e.toString() 替换为方法引用 Integer::toString,因为 Integer 类同时包含静态方法 public static String toString(int i) 和实例方法 public String toString()。

您或您的 IDE 可能建议使用 Object::toString 解决这个特定的问题,因为 Object 中没有 statictoString 方法。尽管该解决方案可以编译,但这种小聪明通常没什么帮助。您必须能够确认方法引用正在调用想要的方法。在存在疑问时,最好使用 lambda 表达式,以避免任何混淆或可能的错误。

传递构造函数调用

除了静态和实例方法,也可以使用方法引用来表示对构造函数的调用。考虑从 Supplier 中发出的构造函数调用,Supplier 做为实参提供给 toCollection 方法。

一个构造函数调用示例:

.collect(toCollection(() -> new LinkedList<Double>()));

代码的目的是获取一个数据 Stream ,将它精减或收集到一个 LinkedList 中。toCollection 方法接受一个 Supplier 作为其实参。Supplier 不接受任何形参,因此 () 为空。它返回一个 Collection 实例,该实例在本例中是 LinkedList。

从形参到构造函数实参:

图片描述

收到的形参 (可能是空的) 被作为实参传递给构造函数。在下面的示例中,我们可以将 lambda 表达式替换为对 new 的方法引用。

将构造函数替换为方法引用:

.collect(toCollection(LinkedList::new));

这个例子包含方法引用的代码比包含 lambda 表达式原始的代码简洁的多,因此更加容易的理解。

传递多个实参

我们已经看到了传递一个多 0 个形参的例子。但是拉姆表达式不仅限于 0 或 1 个形参,他们也适用于多个实参。

将 lambda 表达式执行 reduce():

.reduce(0, (total, e) -> Integer.sum(total, e)));

在 Stream<Integer> 上调用 reduce 方法,并使用 Integer 的 sum 方法对流中的值求和。这个例子中的 lambda 表达式接受两个形参,它们作为实参(按完全相同的顺序)传递给 sum 方法。图 4 显示了这个 lambda 表达式的结构。

传递两个形参做为实参:

图片描述

也可以将这个拉姆表达式替换为方法引用:

.reduce(0, Integer::sum));

作为目标和实参传递

无需将所有形参作为实参传递给 static 方法,lambda 表达式可以使用一个形参作为实例方法调用的目标。如果第一个形参用作目标,则可以将 lambda 表达式替换为方法引用。

对使用形参作为目标的 lambda 表达式执行 reduce():

.reduce("", (result, letter) -> result.concat(letter)));

在这个例子中,在 Stream<String> 上调用 reduce 方法。该 lambda 表达式使用 String 的 concat 实例方法串联字符串。这个 lambda 表达式中的传递结构不同于您在上一个 reduce 示例中看到的结构:

图片描述

lambda 表达式的第一个形参用作实例方法调用的目标。第二个形参用作该方法的实参。根据此顺序,可以将该 lambda 表达式替换为方法引用.

.reduce("", String::concat));

请注意,尽管该 lambda 表达式调用了一个实例方法,但您再次使用了类名称。换句话说,无论您调用静态方法还是将第一个形参作为目标来调用实例方法,方法引用看起来都是一样的。只要不存在模糊性,就没有问题。

最好使用方法引用

要掌握传递 lambda 表达式的变形和结构,以及取代它们的方法引用,需要花费一定的时间和精力。在理解之后,就开始感受到使用方法引用取代传递表达式变得更加自然。

比 lambda 表达式更好的是,方法引用使得您的代码变得非常简洁和富于表达,这可以大大减少阅读代码的工作。我们看下面的示例。

使用 lambda 表达式的示例:

List<String> nonNullNamesInUpperCase =
    names.stream()
      .filter(name -> Objects.nonNull(name))
      .map(name -> name.toUpperCase())
      .collect(collectingAndThen(toList(), list -> Collections.unmodifiableList(list)));

给定一个 List<String> names,上面的代码删除列表中的所有 null 值,将每个名称转换为大写,并将结果收集到一个无法修改的列表中。

现在让我们使用方法引用重写上述代码。在本例中,每个 lambda 表达式都是一个传递表达式,无论是传递给静态方法还是实例方法。因此,我们将每个 lambda 表达式替换为一个方法引用:

List<String> nonNullNamesInUpperCase =
    names.stream()
      .filter(Objects::nonNull)
      .map(String::toUpperCase)
      .collect(collectingAndThen(toList(), Collections::unmodifiableList));

比较这两个清单,很容易看到使用方法引用的代码更加流畅且更容易阅读。它的意思很简单:给定名称,过滤非 null 值,映射到大写形式,然后收集到一个不可修改的列表中。

总结

只要看到一个 lambda 表达式的唯一目的是将形参传递给一个或多个其他函数,就需要考虑将该 lambda 表达式替换为方法引用是否更好。决定因素在于,lambda 表达式内没有完成任何实际工作。在这种情况下,lambda 表达式就是一个传递表达式,而且它的语法对于当前这个任务而言可能过于复杂了。

对于方法引用一旦熟悉了,您就会发现与使用 lambda 表达式的代码相比,使用方法引用会让同样的代码更流畅且富于表达。

感谢 Venkat Subramaniam 博士

点击查看更多内容
3人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
7047
获赞与收藏
756

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消