Java 8 Lambda,Function.identity()或t-> t


240

我对Function.identity()方法的使用有疑问。

想象以下代码:

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->str(反之亦然)。我认为第二个选项更具可读性(当然是个人喜好了)。但是,有没有“真正的”理由为什么应该优先考虑呢?


6
最终,不,这不会有所作为。
fge 2015年

50
都可以。选择您认为更易读的内容。(不用担心,要开心。)
Brian Goetz

3
我宁愿t -> t仅仅因为它更简洁。
戴维·康拉德

3
这个问题无关紧要,但是没有人知道为什么语言设计者使identity()返回Function的实例,而不是返回类型T的参数,然后将其返回,以便该方法可以与方法引用一起使用吗?
Kirill Rakhman 2015年

我会争辩说“身份”一词是有用的,因为它在函数式编程的其他领域具有重要意义。
orbfish

Answers:


312

从当前的JRE实现开始,Function.identity()将始终返回相同的实例,而每次出现identifier -> identifier不仅会创建自己的实例,甚至还会具有不同的实现类。有关更多详细信息,请参见此处

原因是编译器生成了一个合成方法,该方法保存了该lambda表达式的琐碎内容(对于x->x,相当于return identifier;),并告诉运行时创建调用此方法的功能接口的实现。因此,运行时只能看到不同的目标方法,当前的实现无法分析这些方法来确定某些方法是否等效。

因此,使用Function.identity()代替代替x -> x可能会节省一些内存,但是如果您真的认为它x -> x比更具可读性,那不会驱动您的决定Function.identity()

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


5
好答案。我对调试有些怀疑。它如何有用?很难获得涉及x -> x框架的异常堆栈跟踪。您是否建议将断点设置为此lambda?通常,将断点放入单表达式lambda(至少在Eclipse中)不是那么容易...
Tagir Valeev 2015年

14
@Tagir Valeev:您可以调试接收任意函数的代码,然后进入该函数的apply方法。然后,您可能会遇到lambda表达式的源代码。对于显式的lambda表达式,您将知道函数来自何处,并有机会认识到通过身份函数进行决策的位置。使用Function.identity()该信息时会丢失。然后,调用链可能会在简单的情况下有所帮助,但会想到例如多线程评估,其中原始启动程序不在堆栈跟踪中……
Holger 2015年


13
@Wim Deblauwe:有趣,但是我总是会反过来看:如果工厂方法未在其文档中明确声明它将在每次调用时返回一个新实例,则不能假设会这样做。因此,如果不这样做,也就不足为奇了。毕竟,这是使用工厂方法而非的主要原因newnew Foo(…)保证创建确切类型的新实例Foo,而,Foo.getInstance(…)可能返回(的子类型)的现有实例Foo……
Holger 2015年

93

在您的示例中,两者之间str -> str并没有太大的区别,Function.identity()因为在内部这只是简单的t->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()方法。


3
有关另一个示例的信息,请参见stackoverflow.com/q/38034982/14731,其中替换i -> iFunction.identity()会导致编译器错误。
吉利2016年

19
我更喜欢mapToInt(Integer::intValue)
shmosel '17

4
@shmosel可以,但是值得一提的是,由于mapToInt(i -> i)简化了,这两种解决方案都可以类似地工作mapToInt( (Integer i) -> i.intValue())。使用您认为更清晰的任何版本,对我来说,mapToInt(i -> i)更好地显示了此代码的意图。
Pshemo

1
我认为使用方法引用可能会带来性能上的好处,但这主要只是个人喜好。我发现它更具描述性,因为它i -> i看起来像一个身份函数,在这种情况下不是。
shmosel

@shmosel对于性能差异,我不能说太多,所以您可能是对的。但是如果性能不是问题,我将i -> i坚持下去,因为我的目标是将Integer映射到int(这mapToInt很好地暗示了),而不是显式调用intValue()方法。如何实现这种映射并不那么重要。因此,让我们同意不同意,但是感谢您指出可能的性能差异,有一天我需要仔细研究一下。
Pshemo

44

JDK来源

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

因此,只要语法正确,就不会。


8
我想知道这是否会使上述关于创建对象的lambda的答案无效-还是这是特定的实现。
orbfish

28
@orbfish:完全符合要求。t->t源代码中的每一次出现都可能创建一个对象,而实现Function.identity()仅是一次出现。因此,所有调用站点identity()将共享一个对象,而所有明确使用lambda表达式的站点t->t将创建自己的对象。该方法Function.identity()无论如何都不是特别的,只要创建一个封装了常用的lambda表达式的工厂方法并调用该方法而不是重复lambda表达式,就可以使用当前的实现节省一些内存。
Holger 2015年

我猜这是因为编译器在t->t每次调用该方法时都会优化一个新对象的创建,并在每次调用该方法时都回收同一对象?
丹尼尔·格雷

1
@DanielGray在运行时做出决定。编译器插入一条invokedynamic指令,该指令在第一次执行时就通过执行所谓的bootstrap方法被链接起来,对于lambda表达式,该方法位于中LambdaMetafactory。此实现决定将句柄返回给构造函数,工厂方法或始终返回同一对象的代码。它还可能决定返回到现有句柄的链接(当前不会发生)。
Holger

@Holger您确定不会对身份调用进行内联,然后将其单态化(并再次内联)吗?
JasonN
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.