使用Java函数与普通方法相比有哪些好处?

21

Java 8 引入了函数接口(Function Interface)来在Java中实现函数式编程。它表示一个只有一个参数并且返回结果的函数。虽然易于实践和阅读,但我仍在尝试理解它的好处,除了使其看起来很酷之外。例如,

Function<Integer, Double> half = a -> a / 2.0;
Function<Double, Double> triple = b -> b * 3;
double result = half.andThen(triple).apply(8);

可以被转换成像标准方法一样的方式。

private Double half(int a) {
    return a / 2.0;
}
private Double triple (int b) {
    return b * 3;
}
double result = triple(half(8));

那么使用Function有什么好处呢?它提到了函数式编程,在Java中,函数式编程究竟是什么以及它能带来的好处是什么?这样做会像以下方式受益吗:

  1. 将函数链接起来执行(例如 andThen & compose)
  2. 在Java Stream中使用?
  3. 作为函数倾向于定义为私有而不是公共的访问修饰符,而方法可以是任意一种?

基本上,我想知道,在什么情况下我们更喜欢使用函数而不是普通方法?有无法或难以使用或转换为普通方法的用例吗?


8
传递回调函数(例如) - Raildex
是的,那可能是一种用法,但也可以像旧方式一样进行转换。特别是回调在不同情况下会由调用者处理。但有时我必须来回查看代码才能理解它作为回调时的使用。 - Kev D.
这个答案中,我需要一个函数传递给Comparator,该函数将依赖于其他输入(因此排序顺序将动态地取决于它)。使用静态方法声明将需要添加额外的输入字段,这将很丑陋。动态定义一个Function解决了这个问题。 - Ole V.V.
唯一的原因是当您想将函数视为对象时。对于像您的示例这样微不足道的事情,没有使用函数的理由。 - user253751
在Java中,实际上分配给函数式接口的这些lambda表达式是通过匿名类作为对象来实现的。至少在几个版本之前是这样工作的;-) - Marek Żylicz
8个回答

18

Function 的一个用途是在流上。现在每个人都使用 map 方法,我相信:

这个 map 方法接受 Function 作为参数。这允许编写相当优雅的代码 - 这是在 Java 8 之前无法实现的:

Stream.of("a", "b", "c")
   .map(s -> s.toUpperCase())
   .collect(Collectors.toList());
// List of A, B, C

现在,确实存在方法引用和函数式接口(其中之一是当然Function),这使您可以使用方法引用将上面的示例重写为:

现在有方法引用和函数式接口(其中之一是当然的 Function ),这让您可以使用方法引用将上面的示例重写为:

<code>Stream.of("a", "b", "c")
    .map(String::toUpperCase)
    .collect(Collectors.toList())
</code>

...但这只是一个语法糖 - map仍然接受Function作为参数。

另一个使用Java本身的Function的例子是StackWalker:下面是一个示例:

List<StackFrame> frames = StackWalker.getInstance().walk(s ->
    s.dropWhile(f -> f.getClassName().startsWith("com.foo."))
     .limit(10)
     .collect(Collectors.toList()));
}

注意调用walk方法 - 它接受一个函数作为参数。

总之,它只是另一个可以帮助程序员表达意图的工具。在适当的地方明智地使用它。


11

假设我想要编写一个applyTwice函数:

double applyTwice(double x, Function<Double, Double> f) {
  return f.apply(f.apply(x));
}

这需要将函数表示为一个对象。

当您想要在调用者提供的任意代码周围放置一些结构时,函数非常有用。


1
是的,这可能是一个相似的东西,就像C#中的Func<T,TResult>委托。 - Kev D.
2
对我来说,这是创建和使用函数的主要原因。然后它们可以像变量一样作为参数传递。 - Phil Freihofner
似乎类似于JS闭包;-) - Marek Żylicz

4

我在工作中不久前就遇到了一个例子,当时我想要根据条件惰性计算一条消息。例如,想象一下像这样使用 logger 的情况:

  logger.debug("my-heavy-computed-message-here");

现在想象一下,计算"my-heavy-computed-message-here"的过程确实很耗费资源;但你只想在启用了DEBUG记录器时才呈现它。人们通常的做法是:

if(logger.isDebugEnabled()) {
    logger.debug("my-heavy-computed-message-here");
}

这看起来有些丑陋。相反,我们使用一些代码作为输入,它需要一个 Function(或者 Supplier):

 logger.debug(Function<SomeObject, String> function)

在我们的日志记录器实现中,我们仅在需要时才以“惰性”的方式调用function::apply(从而计算出那个昂贵的字符串)。

嘿,这是一个有趣的例子,我很好奇你定义的function是如何替换if(logger.isDebugEnabled()的,我们该如何做到这一点,我们是否将logger作为输入参数传递给函数? - Kev D.
@KevD。不,我们不会替换它。我们只是将其内部移动到库代码库中,这样您就不必自己操作了。 - Eugene
这个例子缺少实际的“函数”(可能是纯函数,也可能不是)。 - Martin Zeitler
@MartinZeitler,你在OP问题中确切地看到了哪里提到了pure?而“Function”是你选择做任何事情的方式,只要你的输出是一个字符串。你的评论毫无意义。副作用通常是Java函数的不变量,至少这是有文档记录的。 - Eugene

3
在Java中通常称为“纯函数”,其定义如下:
  • 该函数的执行没有副作用。

  • 函数的返回值仅取决于传递给函数的输入参数。

其他任何功能都应该是对象的方法。


谢谢!我正在尝试理解第2个问题中的“非纯函数”,“非纯”函数可能指的是使用某些外部类执行的函数,而这些外部类并未作为其输入参数传递进来。例如,这些外部类可能在函数外部被初始化,这样说是否正确?对于第1个问题,“副作用”在这里指什么?有没有一个小例子可以说明? - Kev D.
@KevD。一个不期望的“副作用”可能是对象的属性被改变,而一个方法不一定需要任何输入参数,因为它可以引用其执行范围内的实例或静态方法和属性... 而纯函数是100%独立的,不依赖于其他任何东西并且也不改变其他任何东西,但只有输入参数和返回值(这些几乎不会返回void,因为这是无意义的)。 - Martin Zeitler
副作用应该被避免,不管怎样,你认为呢?不仅仅是针对函数。 - Phil Freihofner
当一个方法改变一个对象的状态时,这通常不被视为“副作用”,但这可能是该方法的实际意图。 - Martin Zeitler

1

与其使用具有覆盖的继承,即匿名实例,不如传递函数功能。

假设您创建了一个类,但必须提供一小部分计算。

class C {
    protected abstract int f(int x);
}

class Child1of99 extends C {
    @Override
    protected int f(int x) { return x / 42; }
}

或者

new C() {
    @Override
    protected int f(int x) { return x / 42; }
}

另外,您也可以这样做:

class C {
    private final IntOperation f;

    C(IntOperation f) {
        this.f = f;
    }
}

 new C(x -> x / 42);

1
就功能接口而言,我认为它使“行为”更加灵活。我的意思是,在功能接口的帮助下,您可以轻松快速地向其他成员提供行为,而传统方法则无法做到这一点。因此,基本上使用实例方法可以将您的行为固定在对象上,而静态方法可以提供更好的范围,并且可以在所有类中访问,但它并不具有动态性。
考虑以下示例:
class Math {

    int sum(int a, int b) {
        return a + b;
    }
}

现在我的 sum 方法已经修复并且不能改变,现在考虑以下与函数接口相关的示例。
interface Sum {
    int sum(int a, int b);
}

现在我可以拥有不同的行为。
Sum nocheckSum = (a, b) -> a + b;
Sum positiveNumSum = (a, b) -> {
                                    if(a < 0 || b < 0) throw new IllegalArgumentException("Only positive numbers are allowed!");
                                    return a + b;
                               };

也许这不是最好的例子,但我想你明白我的意思。
现在这种机制的好处是你不需要为不同的行为声明和管理方法,你可以动态创建并用于特定目的。与此同时,也很重要的是,如果你确定这种行为对所有消费者都是共性的,那么我不会建议强制使用函数接口,但如果它可以并且可能会因为一些方法消费者的具体情况而改变,那么函数接口肯定是有用的。
总之,方法或函数接口都在语言中具有各自的重要性,即使我们可以互换使用它们,更好的选择是根据你的业务需求明智地选择其中之一。

0
通常,函数式编程(lambda表达式、函数接口)最适合进行转换和处理等操作。与之形成对比的是,面向对象编程(使用方法)在需要存储数据(例如在内存中)、不时地改变它或在组件间发送消息时效果最佳。
但像往常一样,这取决于具体情况。所有概括都有例外。

0

基本上,Java函数是内置的、无误差的、优化的、强大的函数,能够满足您的需求。 开发人员在Java函数中使用最佳算法来减少时间和空间复杂度。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接