【问题标题】:Do lambda expressions have any use other than saving lines of code?除了节省代码行之外,lambda 表达式还有其他用途吗?
【发布时间】:2018-05-04 13:37:06
【问题描述】:

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());

【问题讨论】:

  • 请注意,它可以更短:(o1, o2) -&gt; o1.getName().compareTo(o2.getName())
  • 或更短:Comparator.comparing(Developer::getName)
  • 我不会低估这种好处。当事情变得容易得多时,您更有可能以“正确”的方式做事。编写从长远来看更易于维护的代码与在短期内更容易记下的代码之间通常存在一些紧张关系。 Lambda 可以减轻这种压力,从而帮助您编写更简洁的代码。
  • Lambdas 是闭包,这样可以节省更多空间,因为现在您不必在替换类中添加参数化构造函数、字段、赋值代码等。

标签: java lambda java-8


【解决方案1】:

Lambda 表达式一般不会改变您可以用 Java 解决的问题集,但肯定会使解决某些问题更容易,这与我们不再使用汇编语言编程的原因相同。从程序员的工作中删除多余的任务让生活变得更轻松,并允许做一些你甚至不会接触的事情,只是为了你必须(手动)生成的代码量。

但是 lambda 表达式不仅仅是节省代码行数。 Lambda 表达式允许您定义 函数,之前您可以使用匿名内部类作为解决方法,这就是为什么您可以在这些情况下替换匿名内部类,但不是一般情况。

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

此外,构造 new Type() { … } 保证产生一个新的不同实例(正如 new 总是这样做的)。如果在非static 上下文中创建匿名内部类实例,则始终保留对其外部实例的引用。相反,lambda 表达式仅在需要时捕获对this 的引用,即如果它们访问this 或非static 成员。它们会生成有意未指定身份的实例,这允许实现在运行时决定是否重用现有实例(另请参阅“Does a lambda expression create an object on the heap every time it's executed?”)。

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

鉴于语义和性能方面的差异,lambda 表达式可能会改变程序员未来解决某些问题的方式,当然,这也是由于新的 API 采用了利用新语言特性的函数式编程思想。另见Java 8 lambda expression and first-class values

【讨论】:

    【解决方案2】:

    编程语言不是供机器执行的。

    它们是供程序员思考的。

    语言是与编译器的对话,将我们的想法转化为机器可以执行的东西。使用其他语言(或离开其他语言)的人们对 Java 的主要抱怨之一是它强制 程序员的某种思维模式(即一切都是类)。

    我不会去衡量这是好是坏:一切都是取舍。但是 Java 8 lambda 允许程序员根据 函数思考,这是你以前在 Java 中无法做到的。

    这与程序程序员在学习 Java 时思考类是一回事:你会看到它们逐渐从那些美化结构的类转变为具有“辅助”类的一堆静态方法,然后转向更接近于理性 OO 设计的东西(mea culpa)。

    如果您只是将它们视为表达匿名内部类的一种更短的方式,那么您可能不会发现它们非常令人印象深刻,就像上面的程序程序员可能不认为类有任何重大改进一样。

    【讨论】:

    • 不过要小心,我曾经认为 OOP 就是一切,然后我对实体-组件-系统架构感到非常困惑(whaddaya 意味着您没有针对每种类型的类游戏对象?!每个游戏对象都不是 OOP 对象?!)
    • 换句话说,考虑 in OOP,而不是仅仅将 *of* 类视为另一种工具,它们限制了我的想法方式。
    • @immibis:“在 OOP 中思考”有很多不同的方式,所以除了将“在 OOP 中思考”与“在课堂上思考”混淆的常见错误之外,还有很多思考方式以适得其反的方式。不幸的是,这种情况从一开始就经常发生,因为即使是书籍和教程也会教低级的,在示例中显示反模式并传播这种思想。假设需要“为每种类型创建一个类”,这只是一个例子,与抽象概念相矛盾,同样,似乎存在“OOP 意味着编写尽可能多的样板”的神话,等等
    • @immibis 虽然在那种情况下它对您没有帮助,但该轶事确实支持了这一点:语言和范式为构建您的想法并将其转化为设计提供了一个框架。在您的情况下,您遇到了框架的边缘,但在另一种情况下,这样做可能会帮助您避免误入一个大泥球并产生丑陋的设计。因此,我假设,“一切都是权衡”。在决定语言的“大”程度时,在避免前一个问题的同时保持后一个好处是一个关键问题。
    • @immibis 这就是为什么学习一堆范式很重要的原因很好:每个范式都会告诉你其他范式的不足之处。
    【解决方案3】:

    节省代码行可以被视为一项新功能,如果它使您能够以更短、更清晰的方式编写大量逻辑,从而减少其他人阅读和理解的时间。

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

    例如,如果您将每个 lambda 表达式替换为匿名类实例,下面的 Stream 管道会是什么样子。

    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 表达式和方法引用的情况下使其可读,您必须定义变量来保存此处使用的各种功能接口实例,这会拆分管道的逻辑,使其更难理解。

    【讨论】:

    • 我认为这是一个关键的见解。可以说,如果语法和认知开销太大而无用,那么在编程语言中几乎不可能做某事,因此其他方法会更好。因此,通过减少句法和认知开销(就像 lambdas 与匿名类相比),上述管道成为可能。再开玩笑,如果写一段代码让你被解雇,那可以说是不可能的。
    • Nitpick:您实际上并不需要 Comparator 来代替 sorted,因为无论如何它都会按自然顺序进行比较。也许将其更改为比较字符串或类似的长度,以使示例更好?
    • @tobias_k 没问题。我将其更改为 compareToIgnoreCase 以使其行为与 sorted() 不同。
    • compareToIgnoreCase 仍然是一个不好的例子,因为您可以简单地使用现有的 String.CASE_INSENSITIVE_ORDER 比较器。 @tobias_k 按长度(或任何其他没有内置比较器的属性)比较的建议更适合。从 SO Q&A 代码示例来看,lambdas 导致了很多不必要的比较器实现……
    • 只有我认为第二个例子更容易理解吗?
    【解决方案4】:

    内部迭代

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

    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 Predicate interface 你可以做得更好:

    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 - Why We Need Lambda Expressions in Java

    【讨论】:

    • 可能第一种情况是节省一些代码行的一种方式,但它使代码更容易阅读
    • 内部迭代如何成为 lambda 功能?事实上,从技术上讲,lambda 不允许你做任何在 java-8 之前你不能做的事情。这只是传递代码的一种更简洁的方式。
    • @Aominè 在这种情况下numbers.forEach((Integer value) -&gt; System.out.println(value)); lambda 表达式由两部分组成,箭头符号 (->) 的 左侧 列出了它的 参数右侧 包含它的 body 的那个。编译器会自动计算出 lambda 表达式与 Consumer 接口唯一未实现的方法具有相同的签名。
    • 我没有问什么是lambda表达式,我只是说内部迭代与lambda表达式无关。
    • @Aominè 我明白你的意思,它与 lambdas per se 没有任何关系,但正如你所指出的,它更像是一种更简洁的写作方式。我认为在效率方面和 pre-8 版本一样好
    【解决方案5】:

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

    • 在不引入更多语言语法语义的情况下使代码更加紧凑和富有表现力。你已经在你的问题中举了一个例子。

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

    • 编译器没有为 lambda 生成物理类文件。因此,它使您交付的应用程序更小。 How Memory assigns to lambda?

    • 如果 lambda 不访问其范围之外的变量,编译器将优化 lambda 创建,这意味着 lambda 实例仅由 JVM 创建一次。有关更多详细信息,您可以查看@Holger 对问题Is method reference caching a good idea in Java 8? 的回答。

    • Lambdas除了函数式接口外还可以实现多标记接口,但是匿名内部类不能实现更多的接口,例如:

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

    【讨论】:

      【解决方案6】:

      Lambda 只是匿名类的语法糖。

      在 lambda 之前,可以使用匿名类来实现相同的目的。每个 lambda 表达式都可以转换为匿名类。

      如果您使用的是 IntelliJ IDEA,它可以为您进行转换:

      • 将光标放在 lambda 中
      • 按 alt/option + 输入

      【讨论】:

      【解决方案7】:

      为了回答你的问题,事实上 lambdas 不要让你做任何你在 java-8 之前不能做的事情,而是让你写更多的东西 简洁的代码。这样做的好处是,您的代码会更清晰、更灵活。

      【讨论】:

      • 显然@Holger 已经消除了对 lambda 效率的任何疑问。这不仅是一种让代码更干净的方法,而且如果 lambda 没有捕获任何值,它将是一个 Singleton 类(可重用)
      • 如果你深入挖掘,每一种语言特性都会有所不同。手头的问题处于高水平,因此我在高水平上写下了我的答案。
      【解决方案8】:

      我还没有看到提到的一件事是 lambda 允许您定义使用它的功能

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

      【讨论】:

        【解决方案9】:

        是的,有很多优点。

        • 无需定义整个类,我们可以将函数的实现传递给它自己作为参考。
          • 在内部创建类将创建 .class 文件,而如果您使用 lambda,则编译器会避免创建类,因为在 lambda 中您传递的是函数实现而不是类。
        • 代码可重用性比以前更高
        • 正如您所说,代码比正常实现要短。

        【讨论】:

          【解决方案10】:

          函数组合和高阶函数。

          Lambda 函数可用作构建“高阶函数”或执行“函数组合”的构建块。从这个意义上说,Lambda 函数可以被视为可重用的构建块。

          通过 lambda 的高阶函数示例:

          Function<IntUnaryOperator, IntUnaryOperator> twice = f -> f.andThen(f);
          IntUnaryOperator plusThree = i -> i + 3;
          var g = twice.apply(plusThree);
          System.out.println(g.applyAsInt(7))
          

          示例函数组合

          Predicate<String> startsWithA = (text) -> text.startsWith("A");
          Predicate<String> endsWithX   = (text) -> text.endsWith("x");
          
          Predicate<String> startsWithAAndEndsWithX =
                  (text) -> startsWithA.test(text) && endsWithX.test(text);
          
          String  input  = "A hardworking person must relax";
          boolean result = startsWithAAndEndsWithX.test(input);
          System.out.println(result);
          

          【讨论】:

            【解决方案11】:

            尚未提及的一个好处是我最喜欢的:lambdas 使延迟执行非常容易编写。

            例如,Log4j2 使用此方法,您现在可以传递一个 lambda 来计算该昂贵值,而不是有条件地将值传递给 log(一个可能计算成本很高的值)。不同之处在于,以前,无论是否使用,每次都会计算该值,而现在使用 lambdas,如果您的日志级别决定不记录该语句,则永远不会调用 lambda,并且永远不会发生昂贵的计算 - - 性能提升!

            可以在没有 lambda 的情况下完成吗?是的,通过使用 if() 检查包围每个日志语句,或者使用冗长的匿名类语法,但代价是可怕的代码噪音。

            类似的例子比比皆是。 Lambda 就像吃蛋糕一样吃:粗糙的多行优化代码的所有效率都被压缩成单行代码的视觉优雅。

            编辑:根据评论者的要求,举个例子:

            老方法,无论这条日志语句是否实际使用它,总是调用昂贵的计算():

            logger.trace("expensive value was {}", expensiveCalculation());
            

            新的高效 lambda 方法,除非启用跟踪日志级别,否则不会发生调用昂贵的计算():

            logger.trace("expensive value was {}", () -> expensiveCalculation());
            

            【讨论】:

            • 请用实际示例充实您的 Log4j2 示例。我想更清楚地看到你在说什么。
            • @alife 好主意,我编辑回答以包含一个示例,希望对您有所帮助
            • @Magnus:进一步澄清一下,您的意思是 .trace 方法将决定是否进行日志条目,而不参考昂贵的计算。在“高效 lambda 方式”中,仅当 .trace 决定这样做时才会调用开销计算。在“旧方式”中,调用者为每个 .trace 调用执行昂贵的计算,当 .trace 决定不记录时,这是浪费的。对吗?
            猜你喜欢
            • 1970-01-01
            • 2019-10-14
            • 1970-01-01
            • 2014-04-25
            • 2010-10-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多