在Java 8中,使用方法引用表达式或返回功能接口实现的方法在样式上是否更好?


11

Java 8添加了功能接口的概念,以及许多旨在采用功能接口的新方法。这些接口的实例可以使用方法引用表达式(例如SomeClass::someMethod)和lambda表达式(例如(x, y) -> x + y)来简洁地创建。

我和一位同事对什么时候最好使用一种或另一种形式(在这种情况下,“最佳”实际上可以归结为“最易读”和“最符合标准做法”)有不同的看法,因为他们基本上是相反的当量)。具体而言,这涉及以下所有情况:

  • 所涉及的函数不在单个范围内使用
  • 给实例起个名字有助于提高可读性(与之相反,例如,逻辑足够简单以一目了然地看到正在发生的事情)
  • 没有其他编程原因可以解释为什么一种形式优于另一种形式。

我目前对此事的看法是,添加私有方法并通过方法引用进行引用是更好的方法。感觉这就是设计功能使用方式的方式,并且似乎更容易通过方法名称和签名传达正在发生的事情(例如,“ boolean isResultInFuture(Result result)”显然是在说要返回布尔值)。如果将来对类的增强希望使用相同的检查,但不需要功能接口包装器,则这也可使private方法可重用。

我的同事希望使用一种方法来返回接口的实例(例如“ Predicate resultInFuture()”)。对我而言,这感觉不完全是该功能的预期用途,感觉有点笨拙,而且似乎很难通过命名来真正传达意图。

为了使这个示例具体,下面是使用不同样式编写的相同代码:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

是否有任何正式或半官方的文件或评论,说明是更偏爱一种方法,更符合语言设计者的意图还是更具可读性?除非有官方消息来源,是否有任何明确的理由说明为什么会有更好的方法?


1
Lambda被添加到语言中是有原因的,但这并不是使Java变得更糟。

@Snowman不用说。因此,我认为您的评论和我的问题之间存在某种隐含的联系,我不太了解。您要说明的重点是什么?
M. Justin

2
我认为他的意思是,如果lambda不能改善现状,他们就不会费心介绍它们。
罗伯特·哈维

2
@RobertHarvey当然。但是到那时,lambda和方法引用都同时添加了,所以以前都没有。即使没有这种特殊情况,有时方法引用仍然会更好。例如,现有的公共方法(例如Object::toString)。因此,我的问题更多是关于我在这里布置的这种特定类型的实例是否比某个实例比另一个实例更好,反之亦然。
M. Justin

Answers:


8

在函数式编程方面,您和您的同事正在讨论的是无点样式,更具体地说是eta减少。请考虑以下两个任务:

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

这些在操作上是等效的,第一种称为式,而第二种称为式。术语“点”是指命名函数自变量(来自拓扑),在后一种情况下会丢失。

更一般而言,对于所有函数F,包装器lambda接受给定的所有参数并将其不变地传递给F,然后返回F不变的结果与F本身相同。删除lambda并直接使用F(从上面的有指向性的样式到上面的无点样式)称为eta减少,该术语源于lambda演算

有一些不是由eta reduce生成的无点表达式,其中最经典的例子是function composition。给定一个从类型A到类型B的函数以及从类型B到类型C的函数,我们可以将它们组合成一个从类型A到类型C的函数:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

现在假设我们有一个方法Result deriveResult(Foo foo)。由于谓词是一个函数,我们可以构造一个新的谓词,该谓词首先调用deriveResult然后测试派生结果:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

尽管使用lambda来实现compose针对性的样式,但是使用 compose进行定义isFoosResultInFuture是没有指向性的,因为不需要提及任何参数。

无点编程也称为默认编程,它可以通过从函数定义中删除无意义的(双关语意)细节来增强可读性。Java 8几乎不像Haskell这样的功能性语言那样完全支持无点编程,但是一个很好的经验法则是始终执行eta约简。不需要使用与它们包装的函数没有不同行为的lambda。


1
我认为我和我的同事正在讨论的更多是这两个任务:Predicate<Result> pred = this::isResultInFuture;以及Predicate<Result> pred = resultInFuture();resultInFuture()返回a的地方Predicate<Result>。因此,尽管这很好地解释了何时使用方法引用和lambda表达式,但并不能完全涵盖第三种情况。
M. Justin

4
@ M.Justin:该resultInFuture();赋值与我介绍的lambda赋值相同,除了在我的情况下,您的resultInFuture()方法是内联的。这样的做法有2个间接层(均未做任何事情的) -拉姆达-构建方法和拉姆达本身。甚至更糟!
杰克

5

当前的答案均未真正解决问题的核心,即该类应具有private boolean isResultInFuture(Result result)方法还是private Predicate<Result> resultInFuture()方法。尽管它们在很大程度上是等效的,但每个都有优点和缺点。

如果除了创建方法引用之外直接调用该方法,则第一种方法更自然。例如,如果对该方法进行单元测试,则使用第一种方法可能会更容易。第一种方法的另一个优点是该方法不必在意如何使用,只需将其与调用站点分离即可。如果您以后更改类以便直接调用该方法,则这种去耦将以很小的方式得到回报。

如果您可能想使此方法适应不同的功能接口或不同的接口联合,则第一种方法也比较优越。例如,您可能希望方法引用的类型为Predicate<Result> & Serializable,第二种方法没有为您提供实现的灵活性。

另一方面,如果您打算对谓词执行更高阶的操作,则第二种方法更为自然。谓词上有几种方法,不能直接在方法引用上调用。如果打算使用这些方法,则第二种方法更好。

最终,决定是主观的。但是,如果您不打算在Predicate上调用方法,则我倾向于使用第一种方法。


通过转换方法引用,仍可以对谓词进行更高阶的操作:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack Jack

4

我不再编写Java代码,而是以功能性语言为生,并且还支持其他学习功能性编程的团队。Lambda具有易读性的精致之处。通常公认的样式是在可以内联它们时使用它们,但是如果您觉得有必要将其分配给变量以供以后使用,则可能应该只定义一个方法。

是的,人们一直在将lambda分配给教程中的变量。这些只是教程,可以帮助您全面了解它们的工作方式。实际上,实际上并没有以这种方式使用它们,除非在相对罕见的情况下(例如使用if表达式在两个lambda之间进行选择)。

这意味着,如果您的lambda足够短,可以在内联后易于阅读,例如@JimmyJames的注释中的示例,则可以这样做:

people = sort(people, (a,b) -> b.age() - a.age());

尝试内联示例lambda时,将其与可读性进行比较:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

对于您的特定示例,我个人会在请求请求中对其进行标记,并因其长度而要求您将其提取到命名方法中。Java是一种烦人的冗长的语言,因此也许到时候它自己的特定于语言的实践将不同于其他语言,但是在此之前,请确保您的lambda简短而内联。


2
回复:冗长-尽管新的功能添加本身并没有减少很多冗长,但它们允许这样做。例如,您可以定义:public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)具有明显的实现,然后可以编写如下代码: people = sort(people, (a,b) -> b.age() - a.age());这是IMO的一大改进。
JimmyJames

这就是我在说的@JimmyJames的一个很好的例子。当lambda很短并且可以内联时,它们是一个很大的改进。当它们过长而您想将它们分开并给它们命名时,最好定义一个方法。感谢您帮助我理解我的答案被误解了。
Karl Bielefeldt

我认为您的回答很好。不知道为什么将其否决。
JimmyJames
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.