【问题标题】:In Java why is Function.identity() a static method instead of something else?在 Java 中,为什么 Function.identity() 是静态方法而不是其他方法?
【发布时间】:2020-05-04 20:39:28
【问题描述】:

Java 8 添加了函数式编程结构,包括 Function 类及其关联的 identity() 方法。

这是该方法的当前结构:

// Current implementation of this function in the [JDK source][1]
static <T> Function<T, T> identity() {
    return t -> t;
}

// Can be used like this
List<T> sameList = list.stream().map(Function.identity()).collect(Collectors.toList());

但是,还有另一种结构方式:

// Alternative implementation of the method
static <T> T identity(T in) {
    return in;
}

// Can be used like this
List<T> sameList = list.stream().map(Function::identity).collect(Collectors.toList());

甚至还有第三种结构方式:

// Third implementation
static final Function<T, T> IDENTITY_FUNCTION = t -> t;

// Can be used like this
List<T> sameList = list.stream().map(Function.IDENTITY_FUNCTION).collect(Collectors.toList());

在这三种方法中,JDK 中实际使用的第一种方法看起来内存效率较低,因为它似乎在每次使用时都会创建一个新对象 (lambda),而第二种和第三种实现则没有。根据this SO answer 的说法,实际情况并非如此,因此最终所有三种方法在性能方面似乎都相当。

使用第二种方法允许将方法用作方法引用,这类似于在函数构造中使用了许多其他标准库方法。例如。 stream.map(Math::abs)stream.map(String::toLowerCase)

总的来说,为什么要使用第一种方法,它看起来(尽管最终不是)性能较差并且与其他示例不同?

【问题讨论】:

  • @AndyTurner 我不认为他们是说那些是相同的,而是对于给定的实现(身份作为具有签名T(T) 的函数),这将是使用它。
  • 您的标题与您的问题不符。
  • 尝试您的“第三次实施”了吗?
  • static int Math.abs(int)String String::toLowerCase() 早于 lambdas,因此无法更改,此外这些方法也可以直接调用,而不仅仅是作为方法引用。相比之下,&lt;T&gt; T Function::identity(T in) 是新的,没有人会直接调用它(有什么意义?)。
  • 您提到了用于流的样式,因此值得指出的是,尽管没有正式指定任何地方,但我们鼓励 的方法使用静态导入提供功能接口的实现。所以我们可以用toList()代替Collectors.toList(),而不是Comparator.compare(..)compare(...)。这允许像.map(String::length).collect(groupingBy(identity(), counting())); 一样编写流。在该示例中,IMO 简单 identity() 比您提到的 Function::identity 简单。

标签: java java-8 functional-programming functional-interface


【解决方案1】:

TL;DR 使用Function.identity() 只创建一个对象,因此非常节省内存。


第三个实现无法编译,因为 T 未定义,所以这不是一个选项。

在第二个实现中,每次编写Function::identity 时都会创建一个 对象实例。

在第一个实现中,每当您调用Function.identity() 时,都会返回一个指向相同 lambda 对象的实例。

你自己看看很简单。首先在同一个类中创建两个 identity 方法,因此将它们重命名为 identity1identity2 以使它们分别可识别。

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

static <T> T identity2(T in) {
    return in;
}

编写一个接受Functiontest 方法并打印对象,这样我们就可以看到它的唯一身份,正如哈希码所反映的那样。

static <A, B> void test(Function<A, B> func) {
    System.out.println(func);
}

反复调用test方法,看看是否每一个都得到一个新的对象实例(我的代码在一个名为Test的类中)

test(Test.identity1());
test(Test.identity1());
test(Test.identity1());
test(Test::identity2);
test(Test::identity2);
for (int i = 0; i < 3; i++)
    test(Test::identity2);

输出

Test$$Lambda$1/0x0000000800ba0840@7adf9f5f
Test$$Lambda$1/0x0000000800ba0840@7adf9f5f
Test$$Lambda$1/0x0000000800ba0840@7adf9f5f
Test$$Lambda$2/0x0000000800ba1040@5674cd4d
Test$$Lambda$3/0x0000000800ba1440@65b54208
Test$$Lambda$4/0x0000000800ba1840@6b884d57
Test$$Lambda$4/0x0000000800ba1840@6b884d57
Test$$Lambda$4/0x0000000800ba1840@6b884d57

如您所见,调用Test.identity1() 的多个语句 都得到相同的对象,但使用Test::identity2 的多个语句 都得到不同的对象。

same 语句的 repeated 执行确实得到了相同的对象(如循环的结果所示),但这与从 不同的语句。

结论:使用Test.identity1()只创建一个对象,因此它比使用Test::identity2更节省内存。

【讨论】:

  • 谢谢!你知道为什么方法引用每次都会创建一个新对象吗?这是语言规范所要求的,还是 Oracle 的 JVM(或您正在测试的任何 JVM)的产物?
  • @Mark 我认为规范中没有任何内容。特定的书面方法引用在首次使用时创建单个可重用的 lambda 对象这一事实是 JVM 实现决策。 Oracles API 中Function.identity() 的实现仅依赖于此。其他实现可能没有 JVM 执行此操作,在这种情况下,它们可能会实现 Function.identity() 来自己执行此类记忆。
  • 这在Does a lambda expression create an object on the heap every time it's executed? 中有解释,但正如Is method reference caching a good idea in Java 8? 中所说,如果特定实现不重用为无状态 lambda 生成的对象,那么它一定是有原因的,我们不应该抵消,换句话说,一个 Function.identity() 做记忆本身是有问题的。
  • 有趣的是,Objects.isNull(Object) 不遵循Function.identity() 的模式,而是打算用作Object::isNull。它已在同一版本中添加,其文档甚至明确表示它打算与方法引用一起使用,例如 OP 的第二个变体。
猜你喜欢
  • 2011-12-08
  • 2011-12-11
  • 2010-11-23
  • 2011-10-11
  • 2018-10-27
  • 1970-01-01
  • 2011-02-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多