除了保存代码行以外,lambda表达式还有什么用吗?


120

除了保存代码行以外,lambda表达式还有什么用吗?

lambdas是否提供任何特殊功能来解决难以解决的问题?我所看到的典型用法是代替编写此代码:

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

我们可以使用lambda表达式来缩短代码:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());

27
需要注意的是它可以更短: (o1, o2) -> o1.getName().compareTo(o2.getName())
托尔比约恩Ravn的安徒生

88
或更短一些:Comparator.comparing(Developer::getName)
ThomasKläger17年

31
我不会低估这种收益。当事情变得容易得多时,您更有可能以“正确”的方式进行事情。在编写长期可维护性更高的代码与在短期内更容易记起代码之间通常存在一些紧张关系。Lambda减轻了这种压力,因此可以帮助您编写更简洁的代码。
yshavit

5
Lambda是闭包,这可以节省更多空间,因为现在您不必在替换类中添加参数化的构造函数,字段,赋值代码等。
CodesInChaos

Answers:


116

Lambda表达式通常不会改变您可以使用Java解决的问题集,但是肯定会使解决某些问题变得更加容易,这是出于与我们不再使用汇编语言进行编程相同的原因。从程序员的工作中删除多余的任务可以使工作变得更轻松,并且可以执行您根本不会碰到的事情,而只是要产生大量的代码(手动)。

但是lambda表达式不仅节省了代码行。Lambda表达式使您可以定义函数,以前可以使用这些函数来使用匿名内部类作为变通方法,这就是为什么在这种情况下(但一般而言)不能替换匿名内部类的原因。

最值得注意的是,lambda表达式是独立于它们将要转换到的功能接口定义的,因此没有继承的成员可以访问,而且,它们不能访问实现该功能接口的类型的实例。在lambda表达式中,this并且super具有与周围环境相同的含义,另请参见此答案。同样,您不能创建新的局部变量来遮盖周围上下文的局部变量。对于定义函数的预期任务,这消除了许多错误源,但也意味着对于其他用例,可能存在匿名内部类,即使实现了功能接口,也无法将其转换为lambda表达式。

此外,该构造new Type() { … }保证产生一个新的独特实例(new一如既往)。如果在非static上下文中创建匿名内部类实例,则始终保留对其外部实例的引用。相反,lambda表达式仅this在需要时(即它们访问this或为非static成员时)捕获对的引用。它们会生成一个有意未指定身份的实例,这使实现可以在运行时决定是否重用现有实例(另请参见“ lambda表达式是否在每次执行时都会在堆上创建对象? ”)。

这些差异适用于您的示例。您的匿名内部类构造将始终产生一个新实例,它也可能捕获对外部实例的引用,而您(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())是一个不捕获的lambda表达式,在典型实现中其结果将为单例。此外,它不会.class在硬盘驱动器上生成文件。

考虑到语义和性能方面的差异,lambda表达式可能会改变程序员将来解决某些问题的方式,当然,这也是由于新的API包含了利用新语言功能的函数式编程思想。另请参见Java 8 lambda表达式和一等值


1
本质上,lambda表达式是一种函数式编程。鉴于可以使用函数式编程命令式编程来完成任何任务,因此除了可读性和可维护性之外,没有任何其他优势,而这些优势可能超过其他方面。
Draco18s不再信任SE SE

1
@Trilarion:您可以用function的定义填充整本书。您还必须决定是专注于目标还是Java的lambda表达式支持的方面。我的答案的最后一个链接(链接)已经在Java上下文中讨论了其中一些方面。但是要理解我的答案,就已经足够理解函数和类是一个不同的概念。有关更多详细信息,已有资源和问答集…
Holger

1
@Trilarion:函数都不允许解决更多问题,除非您认为其他解决方案所需的代码大小/复杂度或变通办法是不可接受的(如第一段所述),并且已经有一种定义函数的方法,如前所述,由于缺少更好的方法来定义函数,内部类可用作解决方法(但是内部类不是函数,因为它们可以用于更多目的)。它与OOP相同,它不允许您执行汇编中无法完成的任何事情,但是仍然可以对以汇编形式编写的Eclipse之类的应用程序进行映像吗?
Holger

3
@EricDuminil:对我来说,图灵完整性太高了。例如,无论Java是否已完成Turing,您都无法用Java编写Windows设备驱动程序。(或者在PostScript中,图灵也已经完成)在Java中添加允许这样做的功能将改变您可以使用它解决的一系列问题,但是那不会发生。所以我更喜欢集合点;由于您可以做的所有事情都是在可执行的CPU指令中实现的,因此在Assembly中没有什么可以支持每条CPU指令(比Turing机器形式主义更容易理解)。
Holger

2
@abelito在所有更高级别的编程结构中都是常见的,以减少对“实际发生的事情的可见性”,因为这就是问题所在,更少的细节,而是更多地关注实际的编程问题。您可以使用它来使代码更好或更糟。这只是一个工具。像投票功能一样;它是一种可以按预期方式使用的工具,可以对帖子的实际质量提供有根据的判断。或者您只是因为讨厌lambdas,而用它来拒绝详尽而合理的答案。
霍尔格

52

编程语言不适合机器执行。

它们供程序员考虑

语言是与编译器的对话,目的是将我们的思想转变为机器可以执行的东西。一个关于Java从谁走到它与其他语言(或人的主诉离开其他语言)曾经是,它迫使对程序员有一定的心理模型(即一切都是一个类)。

我不会权衡这是好是坏:一切都需要权衡。但是Java 8 lambda允许程序员从功能方面进行思考,这是您以前在Java中无法做到的。

与过程式程序员学习Java时要考虑的类一样,这是一回事:您会看到它们逐渐从具有美化结构的类中移出,并具有带有一堆静态方法的“辅助”类,并继续前进到更类似于理性的面向对象设计(mea culpa)。

如果您只是将它们视为表达匿名内部类的一种较短方法,那么您可能不会像上述过程程序员可能认为类没有什么大的改进那样发现它们给人留下深刻的印象。


5
不过要小心一点,我曾经以为OOP就是一切,然后我就与实体组件系统架构完全混淆(这意味着每种游戏对象都没有类?!和每个游戏对象)是不是一个OOP对象?!)
user253751 '17

3
换句话说,想 OOP,而不仅仅是思维*○ F *类作为另一种工具,Y限制我在一个糟糕的思维方式。
user253751 '17

2
@immibis:“以OOP进行思考”有很多不同的方法,因此,除了将“在OOP中进行思考”与“在类中进行思考”相混淆的常见错误外,还有许多以适得其反的方式进行思考的方法。不幸的是,这种情况从一开始就经常发生,因为即使是书籍和教程也要教那些次等的东西,在示例中显示反例并传播这种思想。所谓的“为每种事物都有一个类”的假设仅仅是一个例子,与抽象的概念相矛盾,同样,似乎存在“ OOP意味着编写尽可能多的样板”的神话,等等
Holger

@immibis尽管在那种情况下并没有帮助您,但轶事实际上确实支持这一点:语言和范式提供了一个框架,用于构造您的想法并将其转变为设计。在您的情况下,您碰到了框架的边缘,但是在另一种情况下,这样做可能帮助您从迷路成一个大的泥泞球并产生了难看的设计。因此,我认为,“一切都是权衡的”。确定后一种优势的同时,要保持后一种优势,同时避免前一种问题,这是一个关键问题。
Leushenko '17

@immibis就是为什么重要的是要学习一堆范例很重要:每个范例都会使您了解其他范例的不足之处。
杰瑞德·史密斯

36

如果使您能够以更短,更清晰的方式编写大量的逻辑块,那么节省代码行可以看作是一项新功能,从而使其他人阅读和理解的时间更少。

没有lambda表达式(和/或方法引用),Stream管道的可读性将大大降低。

例如,考虑一下,Stream如果用匿名类实例替换每个lambda表达式,那么下面的管道将是什么样子。

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

这将是:

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

这比带有lambda表达式的版本难写得多,而且容易出错。这也很难理解。

这是一条相对较短的管道。

为了使之在没有lambda表达式和方法引用的情况下也易于阅读,您必须定义一些变量,以容纳此处使用的各种功能接口实例,这些实例将分割流水线的逻辑,使其更难以理解。


17
我认为这是一个关键见解。可以说,如果语法和认知开销太大而无法使用,那么在编程语言中几乎不可能做任何事情,因此其他方法会更好。因此,通过减少句法和认知开销(就像lambda与匿名类相比,),诸如上述的管道成为可能。如果您编写一段代码使您被解雇,这可能会说是不可能的。
塞巴斯蒂安·雷德尔

Nitpick:实际上,您实际上不需要Comparatorfor,sorted因为它以自然顺序进行比较。也许将其更改为比较字符串或类似字符串的长度,以使示例更好?
tobias_k

@tobias_k没问题。我对其进行compareToIgnoreCase了更改,以使其行为与有所不同sorted()
伊兰

1
compareToIgnoreCase仍然是一个不好的例子,因为您可以简单地使用现有的String.CASE_INSENSITIVE_ORDER比较器。@tobias_k建议按长度进行比较(或任何其他没有内置比较器的属性)比较合适。从SO Q&A代码示例来看,lambda导致了很多不必要的比较器实现……
Holger

我是唯一认为第二个示例更容易理解的人吗?
克拉克之战”,

8

内部迭代

在迭代Java Collections时,大多数开发人员倾向于获取一个元素,然后对其进行处理。也就是说,取出该项目,然后使用它,或重新插入它,等等。对于Java的8之前版本,您可以实现一个内部类并执行类似的操作:

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

现在,使用Java 8,您可以通过以下操作实现更好而更少的冗长:

numbers.forEach((Integer value) -> System.out.println(value));

或更好

numbers.forEach(System.out::println);

行为作为论点

猜测以下情况:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

使用Java 8 谓词接口,您可以像这样做得更好:

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;

    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

像这样调用:

sumAll(numbers, n -> n % 2 == 0);

来源:DZone-为什么我们需要Java中的Lambda表达式


第一种情况可能是保存一些代码行的方法,但它使代码更易于阅读
alseether

4
内部迭代是lambda功能如何?实际上,简单的问题是技术上lambda不允许您执行Java-8之前无法完成的任何事情。这只是传递代码的一种更简洁的方法。
Ousmane D.

@青峰在这种情况下numbers.forEach((Integer value) -> System.out.println(value));λ表达式是由两个部分组成的一个在左边的箭头符号的( - >)列出它的参数和对所述一个包含其身体。编译器会自动找出lambda表达式具有与Consumer接口唯一未实现的方法相同的签名。
alseether

5
我没有问什么是lambda表达式,我只是说内部迭代与lambda表达式无关。
Ousmane D.

1
@青峰我明白你的意思,它没有与任何lambda表达式本身,但正如你指出的,它更像是,书写一个更清洁的方式。我认为就效率而言,它与8之前的版本一样好
参见

4

使用lambda代替内部类有很多好处,如下所示:

  • 使代码更紧凑,更富有表现力,而无需引入更多语言语法语义。您已经在问题中举了一个例子。

  • 通过使用lambda,您很乐意对元素流使用函数式操作进行编程,例如对集合进行map-reduce转换。请参阅java.util.functionjava.util.stream软件包文档。

  • 编译器没有为lambda生成物理类文件。因此,它使您交付的应用程序更小。内存如何分配给lambda?

  • 如果lambda不会访问超出其范围的变量,则编译器将优化lambda的创建,这意味着lambda实例仅由JVM创建一次。有关更多详细信息,请参见@Holger的问题答案。方法引用缓存在Java 8中是个好主意吗?

  • Lambda除了功能接口之外,还可以实现多标记接口,但是匿名内部类不能实现更多接口,例如:

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};


2

为了回答您的问题,事实上,lambdas 不允许您执行Java-8之前无法完成的任何工作,而是使您能够编写更简洁的代码。这样做的好处是您的代码将更清晰,更灵活。


显然,@ Holger消除了关于λ效率的任何疑问。这不仅是使代码更
简洁的

如果深入研究,每种语言功能都会有所不同。眼前的问题是一个高层次的问题,因此我写了一个高层次的答案。
Ousmane D.

2

我还没有提到的一件事是,lambda 允许您在使用的地方定义功能

因此,如果您具有一些简单的选择功能,则无需将其放在一堆样板中放在单独的位置,只需编写一个简洁且与本地相关的lambda。


1

是的,那里有很多优势。

  • 无需定义整个类,我们可以将其自身的功能实现作为参考传递。
    • 内部创建类将创建.class文件,而如果您使用lambda,则编译器将避免类创建,因为在lambda中,您传递的是函数实现而不是类。
  • 代码重用性比以前高
  • 正如您所说,代码比常规实现要短。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.