【问题标题】:Java 8 lambdas, Function.identity() or t->tJava 8 lambda、Function.identity() 或 t->t
【发布时间】:2015-03-17 22:37:06
【问题描述】:

我对@9​​87654321@ 方法的用法有疑问。

想象一下下面的代码:

Arrays.asList("a", "b", "c")
          .stream()
          .map(Function.identity()) // <- This,
          .map(str -> str)          // <- is the same as this.
          .collect(Collectors.toMap(
                       Function.identity(), // <-- And this,
                       str -> str));        // <-- is the same as this.

您有什么理由应该使用Function.identity() 而不是str-&gt;str(反之亦然)。我认为第二个选项更具可读性(当然是口味问题)。但是,是否有任何“真正”的理由应该首选一个?

【问题讨论】:

  • 最终,不,这不会有什么不同。
  • 两者都可以。选择你认为更具可读性的那个。 (别担心,开心就好。)
  • 我更喜欢t -&gt; t,因为它更简洁。
  • 有点不相关的问题,但是有谁知道为什么语言设计者让 identity() 返回 Function 的实例而不是 T 类型的参数并返回它,以便该方法可以与方法引用一起使用?
  • 恒等函数是一个众所周知的数学术语;我们选择依靠现有的理解。

标签: java lambda java-8 java-stream


【解决方案1】:

在您的示例中,str -&gt; strFunction.identity() 之间没有太大区别,因为在内部它只是 t-&gt;t

但有时我们不能使用Function.identity,因为我们不能使用Function。看这里:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

这样可以正常编译

int[] arrayOK = list.stream().mapToInt(i -> i).toArray();

但是如果你尝试编译

int[] arrayProblem = list.stream().mapToInt(Function.identity()).toArray();

你会得到编译错误,因为mapToInt 需要ToIntFunction,这与Function 无关。 ToIntFunction 也没有 identity() 方法。

【讨论】:

  • 请参阅stackoverflow.com/q/38034982/14731 了解另一个将i -&gt; i 替换为Function.identity() 将导致编译器错误的示例。
  • 我更喜欢mapToInt(Integer::intValue)
  • @shmosel 没问题,但值得一提的是,两种解决方案的工作方式相似,因为mapToInt(i -&gt; i)mapToInt( (Integer i) -&gt; i.intValue()) 的简化。使用你认为更清晰的版本,对我来说mapToInt(i -&gt; i) 更好地显示了这段代码的意图。
  • 我认为使用方法引用可以带来性能优势,但这主要只是个人喜好。我觉得它更具描述性,因为i -&gt; i 看起来像一个身份函数,但在这种情况下它不是。
  • @shmosel 关于性能差异我不能说太多,所以您可能是对的。但是,如果性能不是问题,我将继续使用 i -&gt; i,因为我的目标是将 Integer 映射到 int(mapToInt 建议非常好)而不是显式调用 intValue() 方法。 如何实现这种映射并不那么重要。所以让我们同意不同意,但感谢您指出可能的性能差异,总有一天我需要仔细研究一下。
【解决方案2】:

来自JDK source

static <T> Function<T, T> identity() {
    return t -> t;
}

所以,不,只要它在语法上是正确的。

【讨论】:

  • 我想知道这是否会使上述与创建对象的 lambda 相关的答案无效——或者这是否是一个特定的实现。
  • @orbfish:这完全符合。源代码中每次出现t-&gt;t 都可能创建一个对象,而Function.identity() 的实现是一个 出现。因此,所有调用identity() 的呼叫站点都将共享该对象,而所有显式使用 lambda 表达式 t-&gt;t 的站点将创建自己的对象。 Function.identity() 方法在任何方面都没有什么特别之处,每当您创建一个封装常用 lambda 表达式的工厂方法并调用该方法而不是重复 lambda 表达式时,您可能会节省一些内存,鉴于当前的实现 i>.
  • @DanielGray 决定是在运行时做出的。编译器插入一条invokedynamic 指令,该指令在第一次执行时通过执行所谓的引导方法链接,在lambda 表达式的情况下,该方法位于LambdaMetafactory 中。此实现决定将句柄返回给构造函数、工厂方法或始终返回相同对象的代码。它还可能决定返回一个指向已经存在的句柄的链接(目前不会发生)。
  • @JasonN 该方法可能会被内联,但语义不会改变。但请记住,无论是否创建新实例,都被视为实现细节。所以我们在这里讨论一个特定的实现。这个特定的实现将重用创建的对象,即使该方法被内联。但是,在内联和应用各种优化之后,生成的代码可能根本不使用该对象。
  • @JasonN 优化不允许更改代码的语义。所以方法是否被内联并不重要。这里有两种观点。低级视图看到一个invokedynamic 指令,该指令不允许多次链接,并且不允许将其复制到多个调用站点来更改它,因此所有这些都必须链接到始终返回的相同代码在引导期间创建的单个实例。高级视图知道 lambda 表达式的对象身份是未指定的,但是,它不需要内联来知道这一点。
【解决方案3】:

在当前的 JRE 实现中,Function.identity() 将始终返回相同的实例,而 identifier -&gt; identifier 的每次出现不仅会创建自己的实例,甚至还会有不同的实现类。详情请见here

原因是编译器生成了一个合成方法来保存该 lambda 表达式的琐碎主体(在 x-&gt;x 的情况下,相当于 return identifier;)并告诉运行时创建调用此函数的函数接口的实现方法。所以运行时只看到不同的目标方法,当前的实现不会分析方法来找出某些方法是否等效。

因此,使用Function.identity() 而不是x -&gt; x 可能会节省一些内存,但如果您真的认为x -&gt; xFunction.identity() 更具可读性,那不应该推动您的决定。

您可能还认为,在启用调试信息的情况下进行编译时,合成方法将有一个 line debug 属性指向包含 lambda 表达式的源代码行,因此您有机会找到特定的源代码Function 调试时的实例。相比之下,在调试操作时遇到Function.identity()返回的实例,你将不知道是谁调用了该方法并将实例传递给了操作。

【讨论】:

  • 不错的答案。我对调试有一些疑问。它如何有用?获得涉及x -&gt; x 帧的异常堆栈跟踪的可能性很小。您是否建议将断点设置为此 lambda?通常将断点放入单表达式 lambda 中并不是那么容易(至少在 Eclipse 中)...
  • @Tagir Valeev:您可以调试接收任意函数的代码并进入该函数的应用方法。然后你可能会看到一个 lambda 表达式的源代码。在显式 lambda 表达式的情况下,您将知道函数来自何处,并有机会识别通过身份函数的决定是在哪个地方做出的。使用Function.identity() 时,该信息会丢失。然后,调用链可能会在简单的情况下有所帮助,但请考虑,例如原始发起者不在堆栈跟踪中的多线程评估......
  • @Wim Deblauwe:很有趣,但我总是反过来看:如果工厂方法没有在其文档中明确声明它将在每次调用时返回一个新实例,您可以不要假设它会。因此,如果没有,这不足为奇。毕竟,这是使用工厂方法而不是 new 的一大原因。 new Foo(…) 保证创建精确类型 Foo 的新实例,而 Foo.getInstance(…) 可能返回 Foo 的(子类型)的现有实例…
猜你喜欢
  • 2016-10-28
  • 1970-01-01
  • 2023-04-02
  • 2015-08-27
  • 2014-05-06
  • 1970-01-01
  • 2014-04-14
  • 1970-01-01
相关资源
最近更新 更多