Java 8 lambdas, Function.identity() or t->t
我对Function.identity()方法的用法有疑问。
想象一下以下代码:
1 2 3 4 5 6 7
| 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(反之亦然)。 我认为第二种选择更具可读性(当然是品味的问题)。 但是,有没有"真正的"理由为什么应该首选?
-
最终,不,这不会有所作为。
-
要么没事。 选择您认为更具可读性的产品。 (别担心,快乐。)
-
我更喜欢t -> t因为它更简洁。
-
稍微不相关的问题,但有没有人知道为什么语言设计者使identity()返回一个Function的实例,而不是具有类型为T的参数并返回它,以便该方法可以与方法引用一起使用?
-
我认为熟悉"身份"一词是有用的,因为它在函数式编程的其他领域具有重要意义。
从当前的JRE实现开始,Function.identity()将始终返回相同的实例,而每次出现的identifier -> identifier不仅会创建自己的实例,甚至还会有一个不同的实现类。有关详细信息,请参阅此处。
原因是编译器生成一个合成方法,该方法保存该lambda表达式的普通主体(在x->x的情况下,相当于return identifier;),并告诉运行时创建调用此方法的功能接口的实现。因此,运行时只能看到不同的目标方法,并且当前实现不会分析方法以确定某些方法是否相同。
因此,使用Function.identity()而不是x -> x可能会节省一些内存,但如果你真的认为x -> x比Function.identity()更具可读性,则不应该推动你的决定。
您还可以考虑在启用调试信息进行编译时,合成方法将具有指向包含lambda表达式的源代码行的行调试属性,因此您有可能找到特定Function的源代码。调试时的实例。相反,在调试操作期间遇到Function.identity()返回的实例时,您将不知道谁调用了该方法并将实例传递给操作。
-
很好的答案。我对调试有些怀疑。它有用吗?它不太可能获得涉及x -> x帧的异常堆栈跟踪。你建议把断点设置为这个lambda吗?通常将断点放入单表达式lambda(至少在Eclipse中)并不容易......
-
@Tagir Valeev:您可以调试接收任意函数的代码并进入该函数的apply方法。然后你可能会得到一个lambda表达式的源代码。在显式lambda表达式的情况下,您将知道函数的来源,并有机会识别通过身份函数传递决策的位置。使用Function.identity()时,信息丢失。然后,调用链可能在简单的情况下有所帮助,但可以考虑,例如,多线程评估,其中原始启动器不在堆栈跟踪中...
-
有趣的是这个背景:blog.codefx.org/java/instances-non-capturing-lambdas
-
@Wim Deblauwe:有意思,但我总是会反过来看:如果工厂方法没有在其文档中明确声明它将在每次调用时返回一个新实例,那么你不能认为它会。 所以如果没有,那就不足为奇了。 毕竟,这是使用工厂方法而不是new的一个重要原因。 new Foo(…)保证创建精确类型Foo的新实例,而Foo.getInstance(…)可以返回(子类型)Foo的现有实例...
在您的示例中,str -> str和Function.identity()之间没有太大区别,因为内部只是t->t。
但有时我们不能使用Function.identity,因为我们不能使用Function。看看这里:
1 2 3
| List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2); |
这将编译好
1
| int[] arrayOK = list.stream().mapToInt(i -> i).toArray(); |
但如果你试图编译
1
| int[] arrayProblem = list.stream().mapToInt(Function.identity()).toArray(); |
你会得到编译错误,因为mapToInt需要ToIntFunction,这与Function无关。 ToIntFunction也没有identity()方法。
-
有关使用Function.identity()替换i -> i将导致编译器错误的另一个示例,请参阅stackoverflow.com/q/38034982/14731。
-
我更喜欢mapToInt(Integer::intValue)。
-
@shmosel没关系,但值得一提的是,两个解决方案的工作方式类似,因为mapToInt(i -> i)是mapToInt( (Integer i) -> i.intValue())的简化。使用您认为更清楚的版本,对我来说mapToInt(i -> i)更好地显示了此代码的意图。
-
我认为使用方法引用可以带来性能优势,但它主要只是个人偏好。我发现它更具描述性,因为i -> i看起来像一个身份函数,在这种情况下它不是。
-
@shmosel我不能说性能差异,所以你可能是对的。但是如果性能不是问题,我将继续使用i -> i,因为我的目标是将Integer映射到int(mapToInt非常好地表示),而不是显式调用intValue()方法。如何实现这种映射并不是那么重要。因此,我们只是同意不同意,但感谢指出可能的性能差异,我将需要在某一天仔细研究一下。
-
由于JIT编译,假设Function.identity()应该稍快一些:由于被更频繁地调用(因为它在别处使用),lambda的主体将被更快地编译,导致与解释模式相比的加速。但这里的差异微不足道。
来自JDK来源:
1 2 3
| static < T > Function<T, T> identity() {
return t -> t;
} |
所以,不,只要它在语法上是正确的。
-
我想知道这是否会使上述与创建对象的lambda相关的答案无效 - 或者这是否是一个特定的实现。
-
@orbfish:这完全符合要求。源代码中每次出现的t->t都可能创建一个对象,并且Function.identity()的实现是一次出现。因此,所有调用identity()的调用站点将共享该一个对象,而所有明确使用lambda表达式t->t的站点将创建自己的对象。方法Function.identity()并不特别,无论何时创建封装常用lambda表达式的工厂方法并调用该方法而不是重复lambda表达式,在给定当前实现的情况下,可以节省一些内存。
-
我猜这是因为每次调用方法时编译器都会优化掉新的t->t对象的创建,并且每当调用方法时都会循环使用相同的对象?