有没有办法比较lambdas?

Pet*_*rey 76 java lambda java-8

假设我有一个使用lambda表达式(闭包)定义的对象列表.有没有办法检查它们,以便可以进行比较?

我最感兴趣的代码是

    List<Strategy> strategies = getStrategies();
    Strategy a = (Strategy) this::a;
    if (strategies.contains(a)) { // ...
Run Code Online (Sandbox Code Playgroud)

完整的代码是

import java.util.Arrays;
import java.util.List;

public class ClosureEqualsMain {
    interface Strategy {
        void invoke(/*args*/);
        default boolean equals(Object o) { // doesn't compile
            return Closures.equals(this, o);
        }
    }

    public void a() { }
    public void b() { }
    public void c() { }

    public List<Strategy> getStrategies() {
        return Arrays.asList(this::a, this::b, this::c);
    }

    private void testStrategies() {
        List<Strategy> strategies = getStrategies();
        System.out.println(strategies);
        Strategy a = (Strategy) this::a;
        // prints false
        System.out.println("strategies.contains(this::a) is " + strategies.contains(a));
    }

    public static void main(String... ignored) {
        new ClosureEqualsMain().testStrategies();
    }

    enum Closures {;
        public static <Closure> boolean equals(Closure c1, Closure c2) {
            // This doesn't compare the contents 
            // like others immutables e.g. String
            return c1.equals(c2);
        }

        public static <Closure> int hashCode(Closure c) {
            return // a hashCode which can detect duplicates for a Set<Strategy>
        }

        public static <Closure> String asString(Closure c) {
            return // something better than Object.toString();
        }
    }    

    public String toString() {
        return "my-ClosureEqualsMain";
    }
}
Run Code Online (Sandbox Code Playgroud)

看起来唯一的解决方案是将每个lambda定义为一个字段并仅使用这些字段.如果要打印出所调用的方法,最好使用Method.lambda表达式有更好的方法吗?

此外,是否可以打印lambda并获得人类可读的东西?如果你打印this::a而不是

ClosureEqualsMain$$Lambda$1/821270929@3f99bd52
Run Code Online (Sandbox Code Playgroud)

得到类似的东西

ClosureEqualsMain.a()
Run Code Online (Sandbox Code Playgroud)

甚至使用this.toString和方法.

my-ClosureEqualsMain.a();
Run Code Online (Sandbox Code Playgroud)

Bri*_*etz 77

这个问题可以相对于规范或实现来解释.显然,实现可能会改变,但是你可能愿意在发生这种情况时重写你的代码,所以我会在两者上回答.

这也取决于你想做什么.您是在寻求优化,还是在寻找两个实例(或不是)相同功能的铁定保证?(如果是后者,你会发现自己与计算物理学不一致,因为即使是问两个函数是否计算相同的事情这样简单的问题也是不可判定的.)

从规范的角度来看,语言规范只承诺评估(不调用)lambda表达式的结果是实现目标功能接口的类的实例.它没有对结果的身份或别名程度做出任何承诺.这是设计上的,为实现提供最大的灵活性以提供更好的性能(这就是lambdas如何比内部类更快;我们不依赖于内部类的"必须创建唯一实例"约束.)

所以基本上,规范并没有给你太多,除了显然两个引用相等的lambda(==)将计算相同的函数.

从实现的角度来看,您可以得出更多结论.实现lambdas的合成类与程序中的捕获站点之间存在(当前可能会发生变化)1:1的关系.因此,捕获"x - > x + 1"的两个独立代码位可能会映射到不同的类.但是,如果您在同一个捕获站点评估相同的lambda,并且该lambda是非捕获的,那么您将获得相同的实例,可以将其与引用相等性进行比较.

如果你的lambdas是可序列化的,他们会更容易放弃他们的状态,以换取牺牲一些性能和安全性(没有免费的午餐.)

调整相等定义可能是切实可行的一个方面是使用方法引用,因为这将使它们能够用作侦听器并正确地取消注册.这是在考虑中.

我想你要做的是:如果两个lambda被转换为相同的功能接口,由相同的行为函数表示,并且具有相同的捕获args,它们是相同的

不幸的是,这很难做到(对于不可序列化的lambda,你不能得到它的所有组件)而且还不够(因为两个单独编译的文件可以将相同的lambda转换为相同的功能接口类型,你不会不能告诉.)

EG讨论了是否要公开足够的信息以便做出这些判断,以及讨论lambdas是否应该实现更具选择性的equals/hashCode或更具描述性的toString.结论是,我们不愿意支付任何性能成本,以便向呼叫者提供这些信息(糟糕的权衡,惩罚99.99%的用户获得的利益为.01%).

没有达到关于toString的明确结论,但未来还有待重新审视.但是,双方在这个问题上都提出了一些很好的论点; 这不是扣篮.

  • 我们研究了许多可能的实现,包括一个代理类在调用之间共享的实现.我们现在使用的那个("metafactory"方法的一大好处是可以在不重新编译用户类文件的情况下进行更改)是最简单和最佳的.随着VM的发展,我们将继续监控选项之间的相对性能,当其中一个更快时,我们将切换. (6认同)
  • Java 9没有变化. (4认同)
  • @GeoffreyDeSmet 我确信这是出于良好的意图,但这种情况经常发生,以至于开发人员认为当性能与安全性或可维护性发生冲突时他们“别无选择”。正如您所建议的,这种情况可能会因为安全问题没有得到很好的理解而被放大(尽管,当涉及性能时,有时很难让人们倾听其他问题。) (3认同)
  • @GeoffreyDeSmet 缺少验证数据的可能性是一个问题,但由于 lambda 表达式和方法引用主要封装*行为*,所以更大的问题是序列化允许任意(不受信任的代码)访问该行为。[这个问题](/sf/ask/1781055881/)包含一个实际的例子。 (3认同)
  • +1虽然我理解支持`==`相等性通常很难解决,但我会认为在某些简单的情况下,如果不是JVM可以识别出`this :: a`的编译器,编译器就会与另一行上的this :: a相同。实际上,对于我来说,通过给每个呼叫站点自己的实现而获得的收益仍然不明显。也许可以对它们进行不同的优化,但是我认为内联可以做到这一点。 (2认同)
  • 附带说明一下,即使底层二进制接口中使用的“ MethodHandle”也不能保证是规范的(http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5。 html#jvms-5.4.3.5-400)。因此,meta工厂仅通过比较两个引用就无法确定两个引用是否针对同一方法。如果试图为等效的方法引用返回相同的实现,则它必须执行更深入的分析。 (2认同)
  • @bodrin 我假设你的 `getTargetObject()` 方法会返回接收器以获得绑定引用吗?您有没有想过这会是一个重大的安全漏洞?当您捕获绑定的方法引用并将其传递给方法时,用户会非常惊讶地发现他们也共享了未绑定的接收器。 (2认同)
  • @GeoffreyDeSmet 不仅仅是取消封装行为。如果 lambda 捕获本地,则对于不可序列化的 lambda,本地是封装的,但对于可序列化的 lambda,它实际上是公共的。所以想想`s -&gt; s.equals(theSecretPassword)`。 (2认同)
  • @MikeNakis,这是没有存储侦听器引用以取消注册的人的错误。它的工作方式与引入 lambda 表达式之前没有什么不同,例如使用匿名内部类时。 (2认同)
  • @MikeNakis因为,不管分散注意力的长名称如何,“myAnonymousInnerClassInstanceImplementingMyInterface”是*一个包含对实例的引用的变量。实例的创建方式无关,您需要这样一个变量来保存引用,以便能够使用它两次,用于注册和取消注册。您不能将 `register(new MyListener() { … })` 与 `unregister(new MyListener() { … })` 一起使用,也不能将 `register(this::name)` 与 `unregister(this ::名字)`。什么也没有变。除了变量名称之外,如果您确实使用示例中的名称...... (2认同)
  • @MikeNakis 无论您使用“new”还是通过 lambda 表达式实例化对象,都无关紧要。重要的是,*您必须将结果存储在变量中*才能使用它两次,即注册和取消注册。当然,创建匿名内部类时还有关键字new。请随意解释一下如果没有它,您将如何使用匿名内部类...... (2认同)

KIC*_*KIC 7

为了比较labmdas,我通常让接口扩展Serializable,然后比较序列化的字节.不是很好,但适用于大多数情况.


F. *_*ler 6

我没有看到从封闭本身获取这些信息的可能性.关闭不提供状态.

但是如果要检查和比较方法,可以使用Java-Reflection.当然,这不是一个非常漂亮的解决方案,因为它的性能和例外是可以捕获的.但是这样你就可以得到那些元信息.