【问题标题】:Benefit of using Java Function rather than normal method?使用 Java 函数而不是普通方法的好处?
【发布时间】:2021-08-11 03:35:36
【问题描述】:

函数接口是在 Java 8 中引入的,用于在 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));

那么使用函数有什么好处呢?正如它所指的函数式编程,Java 中的函数式编程究竟是什么,它可以带来什么好处?会不会像这样受益:

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

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

【问题讨论】:

  • 例如传递回调
  • 是的,这可能是一种用法,但也可以像旧方式一样转换。特别是回调将在与调用者不同的情况下处理。但有时我在使用它作为回调时必须来回理解代码。
  • this answer 中,我需要一个函数来传递给Comparator,这将依赖于额外的输入(因此排序顺序将动态依赖)。使用静态方法声明需要为额外的输入添加一个字段,这会很丑陋。定义一个Function 动态解决了它。
  • 唯一的原因是当您想将函数视为对象时。没有理由将函数用于像您的示例这样微不足道的事情。
  • 而实际上,在java中,这些分配给功能接口的maldas通过匿名类实现为对象。至少在几个版本前它是这样工作的;-)

标签: java java-8


【解决方案1】:

Function 的一个用法是在 Streams 中。这几天大家都在用map的方法,我相信:

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

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

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

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

...但这只是语法糖 - 当然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 方法的调用 - 它接受一个函数作为参数。

因此,归根结底,它只是另一种工具,可以帮助程序员表达他/她的意图。在适当的地方明智地使用它。

【讨论】:

    【解决方案2】:

    假设我想写一个applyTwice函数:

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

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

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

    【讨论】:

    • 是的,这可能是一个,非常类似于 C# 中的 Func Delegate。
    • 对我来说,这是创建和使用函数的主要原因。然后它们可以作为参数传递,就像变量一样。
    • 好像和 JS 闭包很像 ;-)
    【解决方案3】:

    几天前我不得不在工作场所使用的一个示例是,当我想根据条件懒惰地计算一条消息时。例如想象一个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 作为输入参数传递给函数?跨度>
    • @KevD。不不,我们不更换它。我们只是在内部将其移动到 lib 代码库,这样您就不必自己动手了。
    • 这个例子缺少实际的Function(它可能是也可能不是纯函数)。
    • @MartinZeitler 以及 OP 问题中的 exaclty 您在哪里看到提到纯? Function 是你选择做的任何事情,只要你的输出是一个字符串。你的评论毫无意义。无论如何,副作用通常是 java 函数的不变量,至少这是记录在案的。
    【解决方案4】:

    在 Java 中它通常被称为“纯函数”,它们的定义类似:

    • 函数的执行没有副作用。

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

    其他任何东西都应该是对象的方法。

    【讨论】:

    • 谢谢!我试图从 No.2 中理解一个“非纯函数”,一个“非纯”函数可能是指它使用某个未作为其输入参数传入的外部类执行的函数,例如,那些外部类可能在函数之外启动,这听起来正确吗?对于 No.1,这里的“副作用”指的是什么?有什么小例子吗?
    • @KevD。一个不受欢迎的“副作用”可能是,一个对象的属性正在发生变化 - 一个方法不一定需要任何输入参数,因为它可以在其执行范围内引用实例或静态方法和属性......而纯函数是 100% 独立的,不依赖其他任何东西,也不会改变其他任何东西,但只有输入参数和返回值(这些几乎不返回 void,因为它毫无意义)。
    • 无论如何都应该避免副作用,你不觉得吗?不仅仅是为了功能。
    • @PhilFreihofner 当一个方法改变一个对象时,这通常不被认为是“副作用”,但它可能是该方法应该做的实际意图。
    【解决方案5】:

    就功能接口而言,我相信它使“行为”更加敏捷,我的意思是在功能接口的帮助下,您可以轻松快速地向其他成员提供行为,而传统的情况并非如此方法。因此,基本上使用实例方法,您的行为会依附于对象,或者静态方法可以提供更好的范围,并且可以在所有类中访问,但它根本不是动态的。

    考虑下面的例子,

    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;
                                   };
    

    也许这不是最好的例子,但我想你明白我的意思。

    现在这种机制的好处是您不需要为不同的行为声明和管理方法,您可以动态地创建一个并将其用于特定目的。同时,值得一提的是,如果您确定行为对于所有使用它的人来说必须是通用的,那么我不建议强制执行功能接口,但如果它可以并且可能针对特定方法组而不是所有方法进行更改消费者那么肯定功能接口会有所帮助。

    底线是,无论是方法还是功能接口,在语言中都有自己的意义,即使我们可以互换使用它们,更好的选择是根据您的业务需求有意识地选择其中一个。

    【讨论】:

      【解决方案6】:

      传递函数-ality,而不是覆盖继承,即匿名实例。

      假设你创建了一个类,但必须提供一点计算。

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

      【讨论】:

        【解决方案7】:

        一般来说,函数式编程(lambdas、函数式接口)服务于最好的操作,比如转换和处理。相比之下,OOP 编程(使用方法)在您必须存储数据(内存中的 f.ex)、不时对其进行变异或在组件之间发送消息时效果最佳。 像往常一样,这取决于。对于所有的概括,你都有例外。

        【讨论】:

          【解决方案8】:

          基本上,Java 函数是内置的、无错误的、优化的、强大的函数,可以满足您的要求。 开发人员使用 Java 函数中的最佳算法来降低时间和空间复杂度。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-07-14
            • 1970-01-01
            • 2019-02-16
            相关资源
            最近更新 更多