有没有办法比较lambda?


78

说我有一个使用lambda表达式(闭包)定义的对象列表。有没有一种方法可以检查它们以便进行比较?

我最感兴趣的代码是

    List<Strategy> strategies = getStrategies();
    Strategy a = (Strategy) this::a;
    if (strategies.contains(a)) { // ...

完整的代码是

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";
    }
}

似乎唯一的解决方案是将每个lambda定义为一个字段,并且仅使用这些字段。如果要打印出称为的方法,最好使用Method。lambda表达式有更好的方法吗?

另外,是否可以打印lambda并获得人类可读的内容?如果您打印this::a而不是

ClosureEqualsMain$$Lambda$1/821270929@3f99bd52

得到像

ClosureEqualsMain.a()

甚至使用this.toString和方法。

my-ClosureEqualsMain.a();

1
您可以在闭包内定义toString,equals和hashhCode方法。
Ankit Zalani 2014年

@AnkitZalani您能举一个编译的例子吗?
彼得·劳瑞

@PeterLawrey,因为toString是在上定义的Object,所以我认为您可以定义一个提供默认实现的接口,toString而不会违反接口正常运行的单方法要求。我还没有检查。
Mike Samuel 2014年

6
@MikeSamuel那是不对的。类不继承在接口中声明的默认Object方法。请参阅stackoverflow.com/questions/24016962/…以获取解释。
Brian Goetz 2014年

@BrianGoetz,谢谢你的指导。
Mike Samuel

Answers:


82

可以相对于规范或实现来解释这个问题。显然,实现可能会发生变化,但是您可能会愿意在这种情况下重写代码,因此我将同时回答这两个问题。

这也取决于您要做什么。您是要优化,还是要确保两个实例具有(或不具有)相同的功能?(如果是后者,您将发现自己与计算物理学不符,即使是简单的问题,例如询问两个函数是否计算相同的事物,也无法确定。)

从规范的角度来看,语言规范仅保证评估(而不是调用)lambda表达式的结果是实现目标功能接口的类的实例。它不保证结果的身份或混淆程度。这是设计使然,可为实现提供最大的灵活性以提供更好的性能(这是lambda可以比内部类更快的方式;我们不依赖内部类所具有的“必须创建唯一实例”约束)。

因此,基本上,该规范不会给您带来太多好处,只是显然两个引用相等(==)的lambda将计算相同的函数。

从实现的角度来看,您可以得出更多结论。在实现lambda的综合类与程序中的捕获位置之间存在(当前可能会更改)1:1的关系。因此,捕获“ x-> x + 1”的两个单独的代码位可以很好地映射到不同的类。但是,如果您在相同的捕获站点上评估相同的lambda,并且该lambda没有捕获,则会得到相同的实例,可以将其与引用相等性进行比较。

如果您的Lambda可序列化,则它们会更轻松地放弃其状态,以换取牺牲一些性能和安全性(没有免费的午餐)。

调整相等性定义的一种实际可行方法是使用方法引用,因为这将使它们可以用作侦听器并正确地注销。这正在考虑中。

我认为您要尝试的是:如果将两个lambda转换为相同的功能接口,由相同的行为函数表示,并且具有相同的捕获arg,则它们是相同的

不幸的是,这既很难做到(对于不可序列化的lambda,您无法获得它的所有组件)而且还不够(因为两个单独编译的文件可以将同一lambda转换为相同的功能接口类型,并且您将无法分辨。)

EG讨论了是否公开足够的信息以做出这些判断,还讨论了lambda是否应实现更具选择性的equals/hashCode或更具描述性的toString。结论是,我们不愿意在性能成本上付出任何代价以使此信息可供调用者使用(权衡取舍,对99.99%的用户收取0.01%的好处而受到惩罚)。

toString尚未就得出明确的结论,但有待将来重新讨论。但是,双方在这个问题上都提出了一些好的论点。这不是灌篮。


2
+1虽然我理解支持==相等性通常很难解决,但我会认为在某些简单的情况下,如果不是JVM,编译器可以识别出this::a一行与this::a另一行相同。实际上,对于我来说,通过给每个呼叫站点自己的实现而获得的收益仍然不明显。也许可以对它们进行不同的优化,但是我认为内联可以做到这一点。
彼得·劳瑞

1
数组的likeArrayArrays实用程序类,因为它们无法获得像样的equals,hashCode或toString,我可以想象有Closures一天会创建一个实用程序类。由于可以在其中打印和排列并查看其内容的语言,我想可以在其中打印闭包并获得一些有关闭包功能的见解的语言。(可能是
一串

6
我们研究了许多可能的实现,包括其中一种在呼叫站点之间共享代理类的实现。我们现在使用的一种(最简单的方法)(“元处理”方法的一大好处是可以在不重新编译用户类文件的情况下进行更改)。随着VM的发展,我们将继续监视各个选项之间的相对性能,并且当其中一个较快时,我们将进行切换。
Brian Goetz 2014年

2
附带说明一下,即使MethodHandle在底层二进制接口使用,也不能保证是规范的。因此,meta工厂仅通过比较两个引用就无法判断两个引用是否针对同一方法。如果它试图为等效的方法引用返回相同的实现,则它必须执行更深入的分析。
Holger 2014年

4
不可更改的Java 9
布赖恩戈茨

7

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


lambda的hashCode也是如此,不是吗?我的意思是将lambda序列化为字节数组(借助于ByteArrayOutputStream和ObjectOutputStream),并通过Arrays.hash(...)将其哈希。
mmirwaldt,

6

我看不到有可能从闭包本身获取这些信息。闭包不提供状态。

但是,如果要检查和比较方法,可以使用Java-Reflection。当然,由于要捕获的性能和异常,这并不是一个非常好的解决方案。但是通过这种方式,您可以获得那些元信息。


1
+1反射可以让我thisarg$1,但不能比调用的方法。我可能需要阅读字节码以查看是否相同。
彼得·劳里
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.