1. ホーム
  2. java

[解決済み] ラムダを比較する方法はありますか?

2023-05-21 01:03:37

質問

ラムダ式(クロージャ)を使って定義されたオブジェクトのリストがあるとします。 それらを比較できるように検査する方法はありますか?

私が最も興味を持っているコードは

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

各ラムダをフィールドとして定義し、そのフィールドのみを使用することが唯一の解決策であると思われます。もし、呼び出されたメソッドを出力したい場合は Method . ラムダ式でより良い方法はありますか?

また、ラムダを印刷して、人間が読めるものを得ることは可能でしょうか? もしあなたが this::a の代わりに

ClosureEqualsMain$$Lambda$1/821270929@3f99bd52

のようなものを取得します。

ClosureEqualsMain.a()

を使うか、あるいは this.toString とメソッドで指定します。

my-ClosureEqualsMain.a();

どのように解決するのですか?

この質問は、仕様や実装に関連して解釈される可能性があります。 明らかに実装は変わる可能性がありますが、そうなったときにコードを書き換えることを望んでいるかもしれないので、両方で回答することにします。

また、あなたが何をしたいのかにもよります。 最適化したいのか、それとも 2 つのインスタンスが同じ関数である (またはそうでない) という鉄壁の保証を求めているのか。 (後者の場合、2つの関数が同じものを計算するかどうかというような単純な問題でさえ、決定不可能であるという点で、計算物理学と対立していることに気づくでしょう)。

仕様の観点から、言語仕様はラムダ式の評価(呼び出しではない)の結果がターゲット関数インターフェースを実装するクラスのインスタンスであることだけを約束しています。 結果の同一性、またはエイリアシングの程度については何も約束されていません。 これは、実装に最大限の柔軟性を持たせ、より良いパフォーマンスを提供するための設計です (これがラムダが内部クラスよりも高速である理由です。内部クラスのように、quot; must create unique instance" という制約に縛られません。) 。

つまり、基本的には、参照等しい(==)2つのラムダが同じ関数を計算することを除いて、仕様はあまり教えてくれません。

実装の観点からは、もう少し結論を出すことができます。 ラムダを実装する合成クラスと、プログラム内のキャプチャサイトの間には、1対1の関係があります(現在、変更される可能性があります)。 つまり、"x -> x + 1"をキャプチャする2つの別々のコードのビットは、異なるクラスにマッピングされる可能性が十分にあるのです。 しかし、同じキャプチャサイトで同じラムダを評価し、そのラムダが非キャプチャであれば、同じインスタンスを取得し、参照等価で比較することができます。

ラムダがシリアライザブルであれば、パフォーマンスとセキュリティを多少犠牲にする代わりに、より簡単に状態を放棄することができます(無料の昼食はありません)。

等号の定義を微調整することが実用的であるかもしれない 1 つの領域は、メソッド参照です。 これは現在検討中です。

あなたが得ようとしていることは、2つのラムダが同じ機能インターフェイスに変換され、同じ動作関数によって表され、同一のキャプチャされた引数を持つ場合、それらは同じである、ということだと思います。

残念ながら、これは難しいし(シリアライズ不可能なラムダでは、そのすべての構成要素を得ることはできません)、十分ではありません(2つの別々にコンパイルしたファイルが同じラムダを同じ関数インターフェイス型に変換しても、見分けがつかないからです)。

EG では、これらの判断を可能にするために十分な情報を公開するかどうか、またラムダがより選択的に実装するべきかどうかを議論しました。 equals / hashCode またはもっと説明的なtoStringを使用します。 結論は、呼び出し元がこの情報を利用できるようにするためにパフォーマンス コストで何かを支払う気はないということでした (悪いトレードオフ、0.01% に利益をもたらすもののために 99.99% のユーザーを罰すること)。

に関する決定的な結論は toString は成立しませんでしたが、将来的に再検討される可能性は残されています。 しかし、この問題については、両側からいくつかの良い議論がなされましたので、これはスラムダンクではありません。