왜
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 :-)
}
}
답변
이것은 정말 흥미로운 질문입니다. 대답은 복잡합니다.
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 4 는 B 2 와 같습니다 . 따라서 이전과 같이 해결이 성공하고 컴파일러는 한숨을 쉬게합니다!
withX
우리는 :
withX(TypeInference::getLong, "Also not a long");
초기 경계 세트 B 0 은 다음과 같습니다.
R <: ObjectF <: 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로 시작 합니다.
-
우리는
V다음과 같이 부분 집합 을 결정 합니다.해석 할 추론 변수 세트가 주어지면
V이 세트와이 세트에서 하나 이상의 변수의 분해능이 의존하는 모든 변수를 합치십시오.결합에 의해 상기 제 B (2) 의 해상도
F에 의존R하므로,V := {F, R}. -
우리
V는 규칙 에 따라 하위 집합을 선택합니다 .하자
{ α1, ..., αn }의는 uninstantiated 변수의 비어 있지 않은 부분 집합V모두 같은 내가 그)i (1 ≤ i ≤ n)경우,αi변수의 해상도에 따라β, 다음 중 하나를β인스턴스화되어 있거나이j그와 같은β = αj; 그리고 ii){ α1, ..., αn }이 속성 에는 비어 있지 않은 적절한 하위 집합이 없습니다 .V이 속성 의 유일한 하위 집합은 입니다{R}. -
세 번째 바운드 (
String <: R)를 사용하여이를 인스턴스화R = String하고 바운드 세트에 통합합니다.R이제 해결되고 두 번째 바운드는 효과적으로됩니다F <: Supplier<String>. -
(수정 된) 두 번째 경계를 사용하여 인스턴스화
F = Supplier<String>합니다.F이제 해결되었습니다.
이제 F해결되었으므로 새로운 제약 조건을 사용하여 축소를 진행할 수 있습니다 .
TypeInference::getLong와 호환Supplier<String>- … 와 호환이 줄어든다
LongString - … 거짓으로 줄어든다
… 그리고 컴파일러 오류가 발생합니다!
‘확장 예’에 대한 추가 참고 사항
질문 의 확장 예제 는 위의 작업에서 직접 다루지 않는 몇 가지 흥미로운 사례를 보여줍니다.
- 값 유형이 메소드 리턴 유형 의 하위 유형 인 경우 (
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(바로 교체 위 Long로 Number와 String함께 Integer). 이것은 클래스 디자인에 실패한 형식 유추 동작에 의존해서는 안되는 또 다른 이유를 보여줍니다. 여기서 컴파일 실패 는 바람직한 동작 이 아닐 수 있습니다.
다른 2 개 (실제로 Consumer작업하려는 사용자 와 관련된 다른 호출 )의 경우 위의 방법 중 하나에 대해 제시된 형식 유추 절차를 통해 작업하는 경우 (예 : with첫 번째 withX경우 제삼). 주의해야 할 작은 변경 사항이 하나 있습니다.
- 첫 번째 매개 변수 (의 제약
t::setNumber과 호환되는Consumer<R>) 것입니다 감소 에R <: Number대신Number <: R은을 위해처럼Supplier<R>. 이것은 축소에 대한 링크 된 문서에 설명되어 있습니다.
독자가 위의 절차 중 하나를 통해 추가 지식으로 무장 한 연습을 통해 특정 호출이 컴파일되거나 컴파일되지 않는 이유를 정확하게 설명합니다.
