유형 매개 변수가 메소드 매개 변수보다 더 강한 이유


12

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

더 엄격한

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

이것은 컴파일시 람다 리턴 유형이 점검되지 않는 이유 에 대한 후속 조치 입니다. 나는 withX()같은 방법을 사용하여 발견했다.

.withX(MyInterface::getLength, "I am not a Long")

원하는 컴파일 시간 오류를 생성합니다.

BuilderExample.MyInterface 유형의 getLength () 유형이 길어서 디스크립터의 리턴 유형과 호환되지 않습니다.

방법을 사용하는 동안 with()하지 않습니다.

전체 예 :

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return this;
    }

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

확장 된 예

다음 예는 공급 업체로 분류 된 방법 및 유형 매개 변수의 다른 동작을 보여줍니다. 또한 유형 매개 변수의 소비자 행동과의 차이점을 보여줍니다. 그리고 이는 메소드 매개 변수에 대해 소비자 또는 공급 업체간에 차이를 만들지 않음을 보여줍니다.

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}

1
후자와의 유추 때문에. 둘 다 유스 케이스를 기반으로하지만 구현해야합니다. 당신에게는 전자가 엄격하고 좋을 수도 있습니다. 유연성을 위해 다른 사람이 후자를 선호 할 수 있습니다.
Naman

이클립스에서 이것을 컴파일하려고합니까? 붙여 넣은 형식의 오류 문자열을 검색하면 이것이 Eclipse (ecj) 특정 오류임을 나타냅니다. 원시 javac또는 Gradle 또는 Maven과 같은 빌드 도구로 컴파일 할 때 동일한 문제가 발생 합니까?
user31601 년

@ user31601 javac 출력으로 전체 예제를 추가했습니다. 오류 메시지는 조금 다른이 형식의하지만 여전히 이클립스와 javac의 홀드 같은 문제가 있습니다
jukzi

답변:


12

이것은 정말 흥미로운 질문입니다. 대답은 복잡합니다.

tl; dr

차이점을 해결하려면 Java 유형 유추 사양대한 깊이있는 독서가 필요 하지만 기본적으로 다음과 같이 요약됩니다.

  • 다른 모든 것들이 동일하면 컴파일러는 가능한 가장 구체적인 유형을 유추합니다 .
  • 그러나 모든 요구 사항을 충족시키는 형식 매개 변수 대체를 찾을 수 있으면 컴파일이 성공하지만 대체는 모호 합니다.
  • 다음의 with모든 요구 사항을 충족시키는 (분명히 모호한) 대체물이 있습니다 R.Serializable
  • 를 위해 withX추가 유형 매개 변수를 도입 하면 제약 조건을 고려하지 않고 F컴파일러가 R먼저 해결 하도록합니다 F extends Function<T,R>. R(훨씬 더 구체적)으로 해석되어 실패 String추론을 의미 F합니다.

이 마지막 글 머리 기호가 가장 중요하지만 가장 손이 많이 듭니다. 더 간결한 표현 방법을 생각할 수 없으므로 자세한 내용을 보려면 아래의 전체 설명을 읽으십시오.

이것은 의도 된 행동입니까?

여기 사지에 나가서없고, 말할거야 .

나는 스펙에 버그가 있다고 제안하는 것이 아니다 withX. 언어 디자이너가 손을 들어 "타입 추론이 너무 어려워서 실패 할 것"이라고 말했다 . 컴파일러의 동작 withX이 원하는 것처럼 보이지만, 의도적으로 설계된 디자인 결정이 아니라 현재 사양의 부수적 인 부작용이라고 생각합니다.

이 문제,이 문제에 통지합니다 해야 내 응용 프로그램 디자인이 동작에 의존을?향후 버전의 언어가 계속 이런 식으로 작동한다고 보장 할 수 없기 때문에 사용하지 않아야한다고 주장합니다.

언어 디자이너가 사양 / 디자인 / 컴파일러를 업데이트 할 때 기존 응용 프로그램을 중단하지 않으려 고 노력하는 것은 사실이지만 문제는 의존하려는 동작이 현재 컴파일러 가 실패한 것 (즉 기존 응용 프로그램이 아님)이라는 것입니다. Langauge 업데이트는 항상 비 컴파일 코드를 컴파일 코드로 바꿉니다. 예를 들어, 다음의 코드가 될 수있는 보장 자바 7에서 컴파일하지,하지만 자바 8 컴파일 :

static Runnable x = () -> System.out.println();

사용 사례는 다르지 않습니다.

귀하의 withX방법을 사용할 때주의해야 할 또 다른 이유 는 F매개 변수 자체입니다. 일반적으로 메소드 의 일반 유형 매개 변수 (반환 유형에 표시되지 않음)는 서명의 여러 부분 유형을 함께 바인딩하기 위해 존재합니다. 그것은 말하고 있습니다 :

나는 무엇인지 상관하지 않지만 T사용하는 곳마다 T동일한 유형 인지 확인하고 싶습니다 .

논리적으로, 우리는 각 유형 매개 변수가 메소드 서명에서 적어도 두 번 나타날 것으로 예상합니다. 그렇지 않으면 "아무것도하지 않습니다". F당신에 withX전용하지 인라인 나에게 형식 매개 변수의 사용을 제안 서명, 한 번 나타납니다 의도 언어의 기능을.

대체 구현

좀 더 "의도 된 행동"으로 이것을 구현하는 한 가지 방법은 with메소드를 2 개의 체인으로 나누는 것 입니다.

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // TODO: Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

그런 다음 다음과 같이 사용할 수 있습니다.

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

여기에는 외부와 같은 유형의 매개 변수가 포함되어 있지 않습니다 withX. 이 방법을 두 가지 서명으로 나누면 형식 안전성 관점에서 수행하려는 작업의 의도를 더 잘 표현할 수 있습니다.

  • 첫 번째 방법은 다음 With정의 하는 클래스 ( )를 설정 합니다. 메소드 참조를 기반으로 유형 .
  • scond 메소드 ( of) 는 이전에 설정 한 것과 호환되도록 유형을 제한 합니다 value.

향후 버전의 언어에서이를 컴파일 할 수있는 유일한 방법은 구현 된 전체 오리 타이핑을하는 것입니다.

: 마지막으로 노트는이 모든 일이 무관 만들려면 내 생각 Mockito를 (특히 그 스터 빙 기능) 기본적으로 이미 당신이 당신의 "를 입력 안전 일반적인 빌더"를 달성하기 위해 노력하고 일을 할 수 있습니다. 어쩌면 그냥 대신 사용할 수 있습니까?

전체 설명

과에 대한 형식 유추 절차 를 진행하겠습니다 . 꽤 길어서 천천히 가져 가세요. 길지만, 나는 여전히 많은 세부 사항을 남겼습니다. 자세한 내용은 (링크를 따라) 사양을 참조하여 본인이 옳다는 것을 스스로에게 확신 시키십시오 (실수했을 수도 있음).withwithX

또한 약간 단순화하기 위해 더 적은 코드 샘플을 사용하겠습니다. 가장 큰 차이점은 스왑 아웃이다 Function위해 Supplier, 그래서 덜 유형과 놀이의 매개 변수가 있습니다. 다음은 설명 된 동작을 재현하는 전체 스 니펫입니다.

public class TypeInference {

    static long getLong() { return 1L; }

    static <R> void with(Supplier<R> supplier, R value) {}
    static <R, F extends Supplier<R>> void withX(F supplier, R value) {}

    public static void main(String[] args) {
        with(TypeInference::getLong, "Not a long");       // Compiles
        withX(TypeInference::getLong, "Also not a long"); // Does not compile
    }

}

각 메서드 호출에 대한 형식 적용 가능성 유추형식 유추 절차를 차례로 살펴 보겠습니다 .

with

우리는 :

with(TypeInference::getLong, "Not a long");

초기 경계 세트 B 0 은 다음과 같습니다.

  • R <: Object

모든 매개 변수 표현식은 적용 가능성관련이 있습니다.

따라서, 초기 제약 세트 적용 추론은 , C가 있다 :

  • TypeInference::getLong 와 호환 Supplier<R>
  • "Not a long" 와 호환 R

이것은 다음의 경계 세트 B 2줄어 듭니다 .

  • R <: Object( B 0 부터 )
  • Long <: R (첫 번째 제약에서)
  • String <: R (두 번째 제약 조건에서)

여기에는 바운드 ' false '가 포함되어 있지 않으며 성공 ( 주어진)의 해결 방법 이 있으므로 호출이 가능합니다.RSerializable

그래서 우리는 호출 타입 추론 으로 넘어갑니다. .

입력출력 변수 와 관련된 새로운 제약 조건 세트 C 는 다음과 같습니다.

  • TypeInference::getLong 와 호환 Supplier<R>
    • 입력 변수 : none
    • 출력 변수 : R

여기에는 입력 변수 와 출력 변수 간에 상호 종속성이 없으므로 한 단계 로 줄일 수 있으며 최종 바운드 세트 B 4B 2 와 같습니다 . 따라서 이전과 같이 해결이 성공하고 컴파일러는 한숨을 쉬게합니다!

withX

우리는 :

withX(TypeInference::getLong, "Also not a long");

초기 경계 세트 B 0 은 다음과 같습니다.

  • R <: Object
  • F <: Supplier<R>

두 번째 매개 변수 식만 적용 가능성과 관련이 있습니다. 첫 번째 ( TypeInference::getLong)는 다음 조건을 충족하므로 그렇지 않습니다.

경우 m일반적인 방법 및 메소드 호출은 명확한 형태 인수, 명시 적으로 입력 람다 식 또는 대응하는 타겟 타입 (의 특성에서 유래되는 정확한 방법 참조 발현 제공하지 않는다 m)의 입력 파라미터이다 m.

따라서, 초기 제약 세트 적용 추론은 , C가 있다 :

  • "Also not a long" 와 호환 R

이것은 다음의 경계 세트 B 2줄어 듭니다 .

  • R <: Object( B 0 부터 )
  • F <: Supplier<R>( B 0 부터 )
  • String <: R (제약에서)

이 바운드 '가 포함되지 않기 때문에 다시 거짓 '및 해상도R성공 (주는 String), 다음 호출을 적용한다.

호출 유형 추론 다시 한번 ...

이번에는 입력출력 변수 와 관련된 새로운 제약 조건 세트 C 가 다음과 같습니다.

  • TypeInference::getLong 와 호환 F
    • 입력 변수 : F
    • 출력 변수 : none

다시, 우리는 입력 변수 와 출력 변수 사이에 상호 의존성이 없습니다 . 그러나 이번에는 거기에 있다 입력 변수는 ( F우리가 있어야하므로), 해결 하기 전에이 감소 . 따라서 경계 세트 B 2로 시작 합니다.

  1. 우리는 V다음과 같이 부분 집합 을 결정 합니다.

    해석 할 추론 변수 세트가 주어지면 V이 세트와이 세트에서 하나 이상의 변수의 분해능이 의존하는 모든 변수를 합치십시오.

    결합에 의해 상기 제 B (2) 의 해상도 F에 의존 R하므로, V := {F, R}.

  2. 우리 V는 규칙 에 따라 하위 집합을 선택합니다 .

    하자 { α1, ..., αn }의는 uninstantiated 변수의 비어 있지 않은 부분 집합 V모두 같은 내가 그) i (1 ≤ i ≤ n)경우, αi변수의 해상도에 따라 β, 다음 중 하나를 β인스턴스화되어 있거나이 j그와 같은 β = αj; 그리고 ii) { α1, ..., αn }이 속성 에는 비어 있지 않은 적절한 하위 집합이 없습니다 .

    V이 속성 의 유일한 하위 집합은 입니다 {R}.

  3. 세 번째 바운드 ( String <: R)를 사용하여이를 인스턴스화 R = String하고 바운드 세트에 통합합니다. R이제 해결되고 두 번째 바운드는 효과적으로됩니다 F <: Supplier<String>.

  4. (수정 된) 두 번째 경계를 사용하여 인스턴스화 F = Supplier<String>합니다. F이제 해결되었습니다.

이제 F해결되었으므로 새로운 제약 조건을 사용하여 축소를 진행할 수 있습니다 .

  1. TypeInference::getLong 와 호환 Supplier<String>
  2. ... 와 호환이 줄어든다Long String
  3. ... 거짓으로 줄어든다

... 그리고 컴파일러 오류가 발생합니다!


'확장 예'에 대한 추가 참고 사항

질문 의 확장 예제 는 위의 작업에서 직접 다루지 않는 몇 가지 흥미로운 사례를 보여줍니다.

  • 값 유형이 메소드 리턴 유형 의 하위 유형 인 경우 ( Integer <: Number)
  • 기능적 인터페이스가 유추 된 유형에서 반 변형 인 경우 (즉, Consumer대신 Supplier)

특히, 주어진 호출 중 3 개는 설명에 설명 된 것과 다른 '다른'컴파일러 동작을 잠재적으로 제안하는 것으로 눈에 out니다.

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

이러한 3의 두 번째 정확히 같은 추론 과정을 통해 이동합니다 withX(바로 교체 위 LongNumberString함께 Integer). 이것은 클래스 디자인에 실패한 형식 유추 동작에 의존해서는 안되는 또 다른 이유를 보여줍니다. 여기서 컴파일 실패 는 바람직한 동작 이 아닐 수 있습니다.

다른 2 개 (실제로 Consumer작업하려는 사용자 와 관련된 다른 호출 )의 경우 위의 방법 중 하나에 대해 제시된 형식 유추 절차를 통해 작업하는 경우 (예 : with첫 번째 withX경우 제삼). 주의해야 할 작은 변경 사항이 하나 있습니다.

  • 첫 번째 매개 변수 (의 제약 t::setNumber 과 호환되는 Consumer<R> ) 것입니다 감소R <: Number대신 Number <: R은을 위해처럼 Supplier<R>. 이것은 축소에 대한 링크 된 문서에 설명되어 있습니다.

독자가 위의 절차 중 하나를 통해 추가 지식으로 무장 한 연습을 통해 특정 호출이 컴파일되거나 컴파일되지 않는 이유를 정확하게 설명합니다.


매우 심도 있고, 잘 연구되고 공식화되었습니다. 감사!
Zabuzard

@ user31601 공급 업체와 소비자의 차이가 어디에서 발생하는지 지적 할 수 있습니다. 원래 질문에 확장 예제를 추가했습니다. 공급자 / 소비자에 따라 다른 버전의 letBe (), letBeX () 및 let (). be ()에 대한 공변량, 반 변형 및 불변 동작을 보여줍니다.
jukzi

@jukzi 몇 가지 추가 메모를 추가했지만 이러한 새로운 예제를 직접 수행 할 수있는 충분한 정보가 있어야합니다.
user31601

18.2.1의 많은 특별한 경우. 내가 순진한 이해에서 전혀 특별한 경우를 기대하지 않은 람다 및 메소드 참조. 그리고 아마 평범한 개발자는 기대하지 않을 것입니다.
jukzi

글쎄, 그 이유는 람다와 메소드 참조로 컴파일러가 람다가 어떤 적절한 유형을 구현 해야하는지 결정해야하기 때문입니다. 선택해야합니다! 예를 들어, TypeInference::getLongimlement 수 Supplier<Long>또는 Supplier<Serializable>또는 Supplier<Number>등,하지만 결정적으로 그것은 단지 그들 (다른 모든 클래스 등) 중 하나를 구현할 수 있습니다! 이는 구현 된 유형이 모두 알려진 모든 다른 표현식과는 다르며 컴파일러는 이러한 유형 중 하나가 제약 조건 요구 사항을 충족하는지 여부 만 해결하면됩니다.
user31601
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.