일치하는 여러 대상 유형이있는 람다 식에 대한 메서드 서명 선택


11

나는 질문 에 대답 하고 있었다 설명 할 수없는 시나리오에 부딪쳤다. 이 코드를 고려하십시오.

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

람다의 매개 변수를 명시 적으로 입력 (A a) -> aList.add(a)하여 코드를 컴파일 하는 이유를 이해하지 못합니다 . 또한 왜 과부하가 Iterable아닌 과부하로 연결됩니까?CustomIterable 됩니까?
이것에 대한 설명이나 사양의 관련 섹션에 대한 링크가 있습니까?

참고 : 확장 iterable.forEach((A a) -> aList.add(a));할 때만 컴파일됩니다 (메서드를 오버로드 하면 모호한 오류가 발생합니다)CustomIterable<T>Iterable<T>CustomIterable


둘 다에 이것을 얻는 것 :

  • openjdk 버전 "13.0.2"2020-01-14
    Eclipse 컴파일러
  • openjdk 버전 "1.8.0_232"
    Eclipse 컴파일러

편집 : Eclipse가 마지막 코드 줄을 컴파일하는 동안 maven으로 빌드 할 때 위의 코드가 컴파일되지 않습니다.


3
Java 8에서 세 가지 컴파일 중 어느 것도 컴파일하지 못했습니다. 이제 이것이 최신 버전에서 수정 된 버그인지 또는 도입 된 버그 / 기능인지 확실하지 않습니다. 아마도 Java 버전을 지정해야합니다.
Sweeper

@ 스위퍼 나는 처음에 jdk-13을 사용하여 이것을 얻었다. Java 8 (jdk8u232)의 후속 테스트에서 동일한 오류가 표시됩니다. 마지막 것이 왜 컴퓨터에서 컴파일되지 않는지 잘 모르겠습니다.
ernest_k

두 개의 온라인 컴파일러 ( 1 , 2 ) 에서 재생할 수 없습니다 . 내 컴퓨터에서 1.8.0_221을 사용하고 있습니다. 이것은 점점 더 이상 해지고있다…
스위퍼

1
@ernest_k Eclipse에는 자체 컴파일러 구현이 있습니다. 그것은 질문에 중요한 정보가 될 수 있습니다. 또한 깨끗한 maven 빌드 오류가 마지막 줄에도 오류 가 있음을 내 의견으로는 강조해야합니다. 반면, 관련 질문에 대해서는 코드를 재현 할 수 없기 때문에 OP가 Eclipse를 사용한다는 가정을 분명히 할 수 있습니다.
Naman

2
나는 질문의 지적 가치를 이해하지만 기능 인터페이스 만 다른 오버로드 된 메소드를 작성하지 않고 람다를 전달하는 것이 안전하다고 기대할 수 있습니다. 나는 람다 타입 추론과 오버로드의 조합이 일반 프로그래머가 이해하는 것에 가깝다는 것을 믿지 않습니다. 사용자가 제어하지 않는 변수가 많은 방정식입니다. 이것을 피하십시오 :)
Stephan Herrmann

답변:


8

TL; DR, 이것은 컴파일러 버그입니다.

상속 될 때 적용 가능한 특정 방법이나 기본 방법에 우선 순위를 부여하는 규칙은 없습니다. 흥미롭게도 코드를 다음과 같이 변경하면

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

그만큼 iterable.forEach((A a) -> aList.add(a)); 명령문은 Eclipse에서 오류를 생성합니다.

다른 오버로드를 선언 할 때 인터페이스 의 forEach(Consumer<? super T) c)메소드 특성이 Iterable<T>변경 되지 않았 으므로이 메소드를 선택하려는 Eclipse의 결정은 메소드의 특성을 기반으로 (일관 적으로) 수행 할 수 없습니다. 여전히 유일하게 상속 된 방법이며 여전히 유일합니다default 메소드, 유일하게 JDK 메소드 등입니다. 이러한 속성 중 어느 것도 어쨌든 분석법 선택에 영향을 미치지 않아야합니다.

선언을 다음으로 변경하십시오.

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

또한 "모호한"오류가 발생하므로 두 개의 후보 만있는 경우에도 적용 가능한 오버로드 된 방법의 수는 중요하지 않습니다. default 방법에 .

지금까지 문제는 두 가지 적용 가능한 방법이 있고 default방법과 상속 관계가 관련된 경우에 나타나는 것으로 보이지만 , 더 자세한 정보를 얻을 수있는 곳은 아닙니다.


그러나 예제의 구성은 컴파일러의 다른 구현 코드로 처리 될 수 있으며 하나는 버그를 나타내지 만 다른 하나는 버그를 나타내지 않습니다.
a -> aList.add(a)입니다 암시 적으로 입력 과부하 해결을 위해 사용할 수 없습니다 람다 표현식. 반대로 (A a) -> aList.add(a)이다 명시 입력 오버로드 된 메소드에서 일치하는 메소드를 선택하는 데 사용할 수 람다 식이지만 모든 메소드에는 정확히 동일한 기능 서명이있는 매개 변수 유형이 있으므로 여기에서는 도움이되지 않습니다 (여기에서는 도움이되지 않음) .

반례로서

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

함수형 서명은 다르며, 명시 적으로 형식화 된 람다 식을 사용하면 올바른 방법을 선택하는 데 실제로 도움이 될 수 있지만 내재적으로 형식화 된 람다 식은 도움이되지 않으므로 forEach(s -> s.isEmpty())컴파일러 오류가 발생합니다. 그리고 모든 Java 컴파일러는 이에 동의합니다.

참고 aList::add는 AS, 모호한 방법 참조입니다 add방법은 그것도 방법을 선택 도움이되지 수, 너무 과부하가 있지만, 방법을 참조 어쨌든 다른 코드에 의해 처리받을 수 있습니다. 명확하게 aList::contains변경하거나로 변경 List하여 명확하게 Collection하기 위해 addEclipse 설치의 결과가 변경되지 않았습니다 (사용했습니다 2019-06).


1
@howlger 귀하의 의견은 의미가 없습니다. 기본 메서드 상속 된 메서드이며 메서드가 오버로드됩니다. 다른 방법은 없습니다. 상속 된 메소드가 메소드라는 사실 default은 추가 포인트 일뿐입니다. 내 대답은 이미 Eclipse가 기본 메소드보다 우선 하지 않는 예를 보여줍니다 .
Holger

1
@howlger 우리 행동에는 근본적인 차이가 있습니다. default결과 변경 을 제거 하면 관찰 된 동작의 이유를 즉시 찾은 것으로 가정합니다. 당신은 그것에 대해 너무 확신하여, 심지어 모순되지 않는 사실에도 불구하고 다른 답변을 잘못이라고 부릅니다. 당신이 다른 것에 대해 당신 자신의 행동을 투영하고 있기 때문에 나는 상속이 그 이유라고 주장하지 않았습니다 . 나는 그렇지 않다는 것을 증명했다. Eclipse가 세 가지 과부하가 존재하는 한 시나리오에서는 특정 방법을 선택하지만 다른 시나리오에서는 선택하지 않기 때문에 동작이 일치하지 않음을 시연했습니다.
Holger

1
@howlger 그 외에도 이미이 의견 의 끝에 다른 시나리오의 이름을 지정하고 하나의 인터페이스, 상속 없음, 두 가지 방법, 하나 default는 다른 abstract두 개의 소비자 인수를 사용하여 시도해보십시오. Eclipse는 default방법 중 하나이지만 모호하다고 말합니다 . 분명히 상속은 여전히 ​​이클립스 버그와 관련이 있지만, 당신과 달리, 나는 버그를 완전히 분석하지 않았기 때문에 화를 내지 않고 다른 답변을 잘못 부르지 않습니다. 그것은 우리의 일이 아닙니다.
Holger

1
@ howlger 아니오, 요점은 이것이 버그라는 것입니다. 대부분의 독자는 세부 사항에 신경 쓰지 않을 것입니다. Eclipse는 메소드를 선택할 때마다 주사위를 굴릴 수 있지만 중요하지 않습니다. Eclipse는 모호 할 때 메소드를 선택하지 않아야하므로 왜 메소드를 선택하는지는 중요하지 않습니다. 이 답변은 동작이 일관성이 없다는 것을 증명합니다. 이는 이미 버그임을 강력하게 나타내는 데 충분합니다. Eclipse 소스 코드에서 문제가 발생하는 줄을 가리킬 필요는 없습니다. 이것이 Stackoverflow의 목적이 아닙니다. 아마도 Stackoverflow와 Eclipse의 버그 추적기를 혼동하고 있습니다.
Holger

1
@ howlger 당신은 다시 Eclipse가 왜 잘못된 선택을했는지에 대한 (잘못된) 진술을했다고 잘못 주장하고 있습니다. 다시, 나는 일식이 전혀 선택하지 않아야하기 때문에 나는하지 않았다. 그 방법은 모호하다. 포인트. "상 속됨 " 이라는 용어를 사용한 이유 는 동일한 이름의 메소드를 구별해야하기 때문입니다. 대신 로직을 변경하지 않고 " default method " 라고 말할 수있었습니다 . 보다 정확하게는“어떻게 든 Eclipse가 잘못 선택한 방법 ”이라는 문구를 사용해야했습니다 . 세 구 중 하나를 서로 바꾸어 사용할 수 있으며 논리는 바뀌지 않습니다.
Holger

2

이클립스 컴파일러는 올바르게에 해결 default방법 이가 있기 때문에, 가장 구체적인 방법 에 따라 Java 언어 사양 15.12.2.5은 :

최대로 구체적인 방법 중 하나가 구체적 일 경우 (즉, abstract기본값이 아닌 경우) 가장 구체적인 방법입니다.

javac(기본적으로 Maven 및 IntelliJ에서 사용) 메서드 호출이 모호하다는 것을 나타냅니다. 그러나 Java Language Specification에 따르면 두 가지 방법 중 하나가 가장 구체적인 방법이므로 모호하지 않습니다.

암시 적 으로 형식화 된 람다 식은 Java에서 명시 적으로 형식화 된 람다 식과 다르게 처리 됩니다. 명시 적으로 형식이 지정된 람다 식과 달리 암시 적으로 형식이 지정된 첫 번째 단계에서는 엄격한 호출 방법을 식별합니다 ( Java 언어 사양 jls-15.12.2.2 , 첫 번째 요점 참조). 따라서 여기에서 메소드 호출은 암시 적으로 유형이 지정된 람다 식에 대해 모호합니다 .

귀하의 경우이 버그 의 해결 방법 은 다음과 같이 명시 적으로 유형이 지정된 람다 식을 사용하는 대신 기능 인터페이스javac유형 을 지정하는 것입니다.

iterable.forEach((ConsumerOne<A>) aList::add);

또는

iterable.forEach((Consumer<A>) aList::add);

다음은 테스트를 위해 최소화 된 예입니다.

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}

3
인용문 바로 앞의 전제 조건을 놓쳤습니다.“ 최대한 특정 메소드가 모두 대체 동등 서명을 갖는 경우 ”물론, 인수가 완전히 관련되지 않은 인터페이스를 갖는 두 가지 메소드에는 대체 동등 서명이 없습니다. 게다가 default세 개의 후보 메소드가 있거나 두 메소드가 동일한 인터페이스에서 선언 된 경우 Eclipse가 메소드 선택을 중지하는 이유를 설명하지 않습니다 .
Holger

1
@Holger 귀하의 답변은 "해당되는 특정 방법이 상속 될 때 또는 기본 방법에 우선하는 규칙은 없습니다"라고 주장 합니다. 이 존재하지 않는 규칙에 대한 전제 조건이 여기에 적용되지 않는다고 말하는 것을 올바르게 이해하고 있습니까? 여기의 매개 변수는 기능 인터페이스입니다 (JLS 9.8 참조).
howlger

1
문맥에서 문장을 찢었습니다. 이 문장은 재정의에 상응하는 메소드의 선택, 즉 런타임에 동일한 메소드를 호출하는 선언 사이의 선택을 설명합니다. 구체적 클래스에는 구체적 메소드가 하나뿐이기 때문입니다. 이 독특한 같은 방법의 경우 무관하다 forEach(Consumer)forEach(Consumer2)같은 구현 방법에 끝낼 수 없다.
Holger

2
@StephanHerrmann JEP 또는 JSR을 모르지만 변경 사항이 "콘크리트"의 의미와 일치하도록 수정 된 것 같습니다. 즉 JLS§9.4 와 비교합니다 . " 기본 방법은 구체적인 방법과 다릅니다 (§8.4. 3.1) 클래스에 선언되어 있습니다. ”, 변경되지 않았습니다.
홀거

2
@StephanHerrmann 예, 재정의와 동등한 방법에서 후보를 선택하는 것이 더 복잡해졌으며 그 이유를 아는 것이 흥미로울 것이지만 당면한 질문과 관련이 없습니다. 변화와 동기를 설명하는 다른 문서가 있어야합니다. 과거에는이 "1 년 반마다 새로운 버전"정책으로 품질 유지가 불가능한 것 같습니다 ...
Holger

2

Eclipse가 JLS §15.12.2.5를 구현하는 코드는 명시 적으로 유형이 지정된 람다의 경우에도 다른 방법보다 더 구체적인 방법을 찾지 않습니다.

이상적으로 Eclipse는 여기서 멈추고 모호성을보고합니다. 불행히도 과부하 해결의 구현에는 JLS 구현 외에도 사소한 코드가 있습니다. 내가 이해 한 바에 따르면,이 코드 (Java 5가 처음 등장한 시점부터)는 JLS에 약간의 차이를 메워야합니다.

이것을 추적하기 위해 https://bugs.eclipse.org/562538 를 제출 했습니다.

이 특정 버그와는 별도로이 스타일 코드에 대해서만 강력하게 조언 할 수 있습니다. 오버로드는 람다 유형 추론으로 곱한 Java의 많은 놀라움에 좋습니다. 복잡함은 인식 된 이득에 비례하지 않습니다.


감사합니다. 이미 bugs.eclipse.org/bugs/show_bug.cgi?id=562507을 기록했다면 , 링크로 연결하거나 중복으로 닫을 수 있습니다 ...
ernest_k
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.