람다를 비교하는 방법이 있습니까?


78

람다 식 (클로저)을 사용하여 정의 된 객체 목록이 있다고 가정 해 보겠습니다. 비교할 수 있도록 검사하는 방법이 있습니까?

내가 가장 관심있는 코드는

    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();

1
클로저 내에서 toString, equals 및 hashhCode 메소드를 정의 할 수 있습니다.
ANKIT Zalani

@AnkitZalani 컴파일하는 예제를 줄 수 있습니까?
피터 Lawrey

@PeterLawrey, toString에 정의되어 있으므로 인터페이스가 작동하기 위해 단일 메서드 요구 사항을 위반하지 않고 Object기본 구현을 제공하는 인터페이스를 정의 할 수 있다고 생각합니다 . 나는 이것을 확인하지 않았다. toString
Mike Samuel

6
@MikeSamuel 틀 렸습니다. 클래스는 인터페이스에 선언 된 기본 Object 메서드를 상속하지 않습니다. 설명 은 stackoverflow.com/questions/24016962/… 를 참조하십시오 .
Brian Goetz

@BrianGoetz, 포인터 주셔서 감사합니다.
Mike Samuel

답변:


82

이 질문은 사양 또는 구현과 관련하여 해석 될 수 있습니다. 분명히 구현이 변경 될 수 있지만 그럴 경우 코드를 다시 작성할 수 있으므로 두 가지 모두에 답할 것입니다.

또한 수행하려는 작업에 따라 다릅니다. 최적화를 원하십니까, 아니면 두 인스턴스가 동일한 기능을 수행하는지 (또는 동일하지 않은지) 확실한 보장을 찾고 있습니까? (후자의 경우, 두 함수가 같은 것을 계산하는지 여부를 묻는 것과 같은 간단한 문제조차도 결정 불가능하다는 점에서 계산 물리학과 충돌 할 것입니다.)

사양 관점에서 언어 사양은 람다 식을 평가 (호출하지 않음) 한 결과가 대상 기능 인터페이스를 구현하는 클래스의 인스턴스임을 약속합니다. 결과의 정체성이나 앨리어싱 정도에 대해 약속하지 않습니다. 이것은 더 나은 성능을 제공하기 위해 구현에 최대한의 유연성을 제공하기위한 것입니다 (이는 람다가 내부 클래스보다 빠를 수있는 방법입니다. 우리는 내부 클래스가 "must create unique instance"제약 조건에 묶여 있지 않습니다.)

따라서 기본적으로 사양은 참조 동일 (==) 인 두 개의 람다가 동일한 함수를 계산한다는 점을 제외하고는 많은 것을 제공하지 않습니다.

구현 관점에서 좀 더 결론을 내릴 수 있습니다. 람다를 구현하는 합성 클래스와 프로그램의 캡처 사이트 간에는 (현재 변경 될 수 있음) 1 : 1 관계가 있습니다. 따라서 "x-> x + 1"을 캡처하는 두 개의 개별 코드 비트는 서로 다른 클래스에 매핑 될 수 있습니다. 그러나 동일한 캡처 사이트에서 동일한 람다를 평가하고 해당 람다가 캡처되지 않는 경우 동일한 인스턴스를 얻게되며 이는 참조 동등성과 비교할 수 있습니다.

람다가 직렬화 가능하면 성능과 보안을 희생하는 대가로 상태를 더 쉽게 포기할 수 있습니다 (무료 점심 없음).

평등의 정의를 수정하는 것이 실용적 일 수있는 한 영역은 메서드 참조를 사용하는 것입니다. 리스너로 사용하고 적절하게 등록을 해제 할 수 있기 때문입니다. 이것은 고려 중입니다.

나는 당신이 얻고 자하는 것은 : 두 개의 람다가 동일한 기능적 인터페이스로 변환되고 동일한 동작 함수로 표현되고 동일한 캡처 인수를 갖는 경우 동일하다는 것입니다.

불행히도 이것은 (직렬화 할 수없는 람다의 경우, 그 구성 요소를 전혀 얻을 수없는 경우) 어렵고 충분하지 않습니다 (2 개의 개별적으로 컴파일 된 파일이 동일한 람다를 동일한 기능적 인터페이스 유형으로 변환 할 수 있기 때문에). 말할 수 없을 것입니다.)

EG는 이러한 판단을 내릴 수 있도록 충분한 정보를 노출해야하는지 여부와 람다가 더 선택적인 equals/hashCode 또는 더 설명적인 toString을 . 결론은 우리가이 정보를 호출자에게 제공하기 위해 성능 비용을 지불 할 의향이 없다는 것입니다 (나쁜 절충, .01 % 혜택을받는 것에 대해 사용자의 99.99 %를 처벌).

에 대한 결정적인 결론에 toString도달하지 못했지만 나중에 다시 검토 할 수 있도록 공개되었습니다. 그러나이 문제에 대해 양측에서 좋은 주장이있었습니다. 이것은 슬램 덩크가 아닙니다.


2
+1 ==평등을 지원 하는 것이 일반적으로 해결하기 어려운 문제라는 것을 이해하지만 , JVM이 this::a한 줄 this::a에서 다른 줄 과 동일 하다고 인식 할 수 없다면 컴파일러가 간단한 경우가있을 것이라고 생각했을 것 입니다. 실제로 모든 호출 사이트에 자체 구현을 제공함으로써 얻을 수있는 이점은 아직 명확하지 않습니다. 아마도 그들은 다르게 최적화 될 수 있지만 인라인이 이것을 할 수 있다고 생각했을 것입니다. ??
Peter Lawrey 2014-06-07

1
괜찮은 등가, hashCode 또는 toString을 얻을 수 없었기 때문에 배열에 대한 유틸리티 클래스를 좋아 Array하고 언젠가 유틸리티 클래스를 Arrays상상할 수 있습니다 Closures. 인쇄하고 배열하고 내용을 볼 수있는 언어가 있기 때문에 클로저를 인쇄하고 클로저가 수행하는 작업에 대한 통찰력을 얻을 수있는 언어가 있다고 생각합니다. (아마 코드의 문자열은 몇 가지 더 나은하지만 만족스럽지)
피터 Lawrey

6
프록시 클래스가 호출 사이트간에 공유되는 하나를 포함하여 가능한 여러 구현을 조사했습니다. 지금까지 함께했던 것 ( "metafactory"접근 방식의 큰 이점 중 하나는 사용자 클래스 파일을 다시 컴파일하지 않고도 변경할 수 있다는 것입니다)은 가장 단순하고 성능이 가장 뛰어났습니다. VM이 발전함에 따라 옵션 간의 상대적 성능을 계속 모니터링하고 다른 옵션 중 하나가 더 빠를 때 전환합니다.
Brian Goetz

2
참고로, MethodHandle기본 바이너리 인터페이스에서 사용되는 s 조차도 canonical이 보장되지는 않습니다 . 따라서 메타 팩토리는 참조를 비교하는 것만으로는 두 참조가 동일한 방법을 대상으로하고 있는지 여부를 알 수 없습니다. 동등한 메서드 참조에 대해 동일한 구현을 반환하려고하면 더 심층적 인 분석을 수행해야했습니다.
홀거

4
Java 9에 대한 변경 사항 없음
Brian Goetz

7

labmdas를 비교하기 위해 일반적으로 인터페이스를 확장 Serializable한 다음 직렬화 된 바이트를 비교합니다. 별로 좋지는 않지만 대부분의 경우 작동합니다.


람다의 hashCode에도 똑같이 적용되지 않습니까? 람다를 바이트 배열로 직렬화하고 (ByteArrayOutputStream 및 ObjectOutputStream의 도움으로) Arrays.hash (...)로 해싱하는 것을 의미합니다.
mmirwaldt

6

나는 폐쇄 자체에서 그러한 정보를 얻을 가능성을 보지 못합니다. 클로저는 상태를 제공하지 않습니다.

그러나 메소드를 검사하고 비교하려면 Java-Reflection을 사용할 수 있습니다. 물론 성능과 예외로 인해 그다지 아름다운 해결책은 아닙니다. 하지만 이렇게하면 메타 정보를 얻을 수 있습니다.


1
+1 반사를 사용하면 호출 된 방법을 비교할 수 thisarg$1없지만 얻을 수 있습니다 . 동일한 지 확인하기 위해 바이트 코드를 읽어야 할 수도 있습니다.
Peter Lawrey 2014-06-07
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.