38

lambda 표현식을 사용할 때 타입 추론이 작동하지 않는 별난 시나리오가 있습니다. 내 실제 시나리오의 근사치는 다음과 같습니다.

static class Value<T> {
}

@FunctionalInterface
interface Bar<T> {
  T apply(Value<T> value); // Change here resolves error
}

static class Foo {
  public static <T> T foo(Bar<T> callback) {
  }
}

void test() {
  Foo.foo(value -> true).booleanValue(); // Compile error here
}

마지막 줄에서 두 번째로 오는 컴파일 오류는 다음과 같습니다.

메소드 booleanValue ()가 Object 유형에 대해 정의되지 않았습니다.

내가 람다를에 던지면Bar<Boolean>:

Foo.foo((Bar<Boolean>)value -> true).booleanValue();

또는 메소드 서명을 변경하면Bar.apply원시 형식을 사용하는 방법 :

T apply(Value value);

그러면 문제는 사라집니다. 이것이 내가 기대할 수있는 방법은 다음과 같습니다.

  • Foo.foo호출은 다음과 같은 반환 유형을 추론해야합니다.boolean
  • value람다에서Value<Boolean>.

이 추론이 예상대로 작동하지 않는 이유는 무엇이며 어떻게이 API를 변경하여 예상대로 작동하게 할 수 있습니까?


  • 어떤 Java 버전을 사용합니까? - NwDev
  • @NwDx Java 1.8.0_25 - Josh Stone
  • Boolean이 Value < Boolean >이 아니기 때문에 유형 불일치가 생깁니다. 컴파일러는 T가 ValueT가되어야 함을 알 수 있어야합니다. 물론, Object라고 가정하는 방법을 알지 못합니다. 을보세요기능인터페이스 적용 방법. 값은 바 인터페이스의 컨텍스트에서 구체적인 유형입니다. - NwDev
  • @NwDx 당신이 묘사 한 것이 @ fukanchik의 대답의 기초라고 생각합니다. 컴파일러가 유추하는 문제는Value<Object>대신에Value<Boolean>그게 내가 한 일이야. 그것이 작동하도록하는 방법에 대한 아이디어를 자유롭게 공유하십시오. - Josh Stone
  • 당신이 Foo.foo의 리턴 타입을 버리지 않았다면 컴파일러는 타겟 타입을 사용하여 T에 대한 경계를 도출 할 수 있습니다. - Brian Goetz

6 답변


30

후드

일부 숨김 사용하기javac기능을 사용하면 어떤 일이 일어나고 있는지에 대한 자세한 정보를 얻을 수 있습니다.

$ javac -XDverboseResolution=deferred-inference,success,applicable LambdaInference.java 
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: error: cannot find symbol
    Foo.foo(value -> true).booleanValue(); // Compile error here
                          ^
  symbol:   method booleanValue()
  location: class Object
1 error

이것은 많은 정보입니다.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

단계:방법 적용 단계

실제 : 전달 된 실제 인수

type-args : 명시 적 형식 인수

후보자 :잠재적으로 적용 가능한 방법

현재는<none>암묵적으로 입력 된 람다가적용 가능성과 관련있다..

컴파일러는 당신의 호출을 해결합니다.foo명명 된 유일한 방법으로foo...에서Foo. 그것은 부분적으로 인스턴스화되었습니다.Foo.<Object> foo(실제 또는 형식 인수가 없었으므로) 지연 추론 단계에서 변경 될 수 있습니다.

LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

인스턴스화 된 서명 :의 완전히 인스턴스화 된 서명foo. 이 단계의 결과입니다 (이 시점에서 더 이상의 형식 유추는foo).

target-type : 호출이 수행되는 컨텍스트입니다. 메소드 호출이 할당의 일부이면 왼쪽이됩니다. 메소드 호출 자체가 메소드 호출의 일부이면 매개 변수 유형이됩니다.

메소드 호출이 매달 기 때문에 대상 유형이 없습니다. 목표 유형이 없으므로 추측을 더 이상 수행 할 수 없습니다.fooT될 것으로 추측된다.Object.


분석

컴파일러는 추측 중에 암시 적으로 형식화 된 람다를 사용하지 않습니다. 어느 정도까지는 이것이 합리적입니다. 일반적으로 주어진param -> BODY, 당신은 컴파일 할 수 없을 것입니다.BODY당신이 타입을 가질 때까지param. 타입을 추측하려고 시도했다면param...에서BODY, 닭고기와 계란 타입의 문제를 일으킬 수 있습니다. Java의 향후 릴리스에서이를 개선 할 가능성이 있습니다.


솔루션

Foo.<Boolean> foo(value -> true)

이 솔루션은에 대한 명시적인 형식 인수를 제공합니다.foo(with type-args섹션 참조). 이렇게하면 메소드 서명의 부분 인스턴스화가 다음으로 변경됩니다.(Bar<Boolean>)Boolean, 그것은 당신이 원하는 것입니다.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: Boolean
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
                                    ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Value<Boolean> value) -> true)

이 솔루션은 람다를 명시 적으로 입력하므로 적용 가능성과 관련이 있습니다 (참고with actuals이하). 이렇게하면 메소드 서명의 부분 인스턴스화가 다음으로 변경됩니다.(Bar<Boolean>)Boolean, 그것은 당신이 원하는 것입니다.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
                                           ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Bar<Boolean>) value -> true)

위와 같지만 약간 다른 맛이납니다.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
                                         ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Boolean b = Foo.foo(value -> true)

이 솔루션은 메서드 호출에 대한 명시적인 대상을 제공합니다 (target-type이하). 이것은 연기 된 인스턴스화가 타입 매개 변수가Boolean대신에Object(만나다instantiated signature이하).

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Boolean b = Foo.foo(value -> true);
                   ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Boolean b = Foo.foo(value -> true);
                       ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: Boolean
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

기권

이것은 발생하는 동작입니다. 이것이 JLS에 지정되어 있는지 여부는 알 수 없습니다. 이 동작을 지정하는 정확한 섹션을 찾을 수 있는지 살펴볼 수는 있지만형식 유추표기법이 나에게 두통을줍니다.

이것은 또한 왜 변화하는지 완전히 설명하지 못한다.Bar익지 않는 것을 사용하는Value이 문제를 해결할 것입니다 :

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue();
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue();
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo(value -> true).booleanValue();
                          ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

어떤 이유로 원시를 사용하도록 변경Value연기 된 인스턴스 생성을 통해T~이다.Boolean. 만약 내가 추측해야만한다면, 나는 컴파일러가 람다를Bar<T>, 그것은 그것이T~이다.Boolean람다의 몸을보고. 이것은 내가 이전에 분석 한 내용이 정확하지 않다는 것을 의미합니다. 컴파일러양철통람다 본문에 형식 유추를 수행하지만 형식 변수에 대해서만 수행합니다.반환 유형에 나타납니다.


5

람다 매개 변수 유형에 대한 추론은 람다 본문에 의존 할 수 없습니다.

컴파일러는 암시적인 람다 식을 이해하려고하는 어려운 작업에 직면합니다.

    foo( value -> GIBBERISH )

유형valueGIBBERISH가 컴파일되기 전에 먼저 추측되어야합니다. GIBBERISH의 해석은 일반적으로value.

(당신의 특별한 경우에, GIBBERISH는value.)

Javac은 유추해야 함Value<T>매개 변수에 대한 첫 번째value; 상황에 제약이 없습니다.T=Object. 그런 다음, λ 본체true부울로 컴파일되고 인식됩니다.T.

함수 인터페이스를 변경 한 후에는 람다 매개 변수 유형에 유추가 필요하지 않습니다. T는 유익하지 않다. 다음으로 람다 본문이 컴파일되고 반환 형식이 부울로 나타나며이 부울은T.


문제를 보여주는 또 다른 예

<T> void foo(T v, Function<T,T> f) { ... }

foo("", v->42);  // Error. why can't javac infer T=Object ?

T는 유추 된 것으로 추측된다.String; 람다의 몸은 추론에 참여하지 않았다.

이 예제에서 javac의 동작은 매우 합리적인 것처럼 보입니다. 프로그래밍 오류를 막을 가능성이 높습니다. 추론이 너무 강력하지 않기를 바랄뿐입니다. 우리가 작성한 모든 내용이 어떻게 든 컴파일되면 컴파일러가 오류를 찾는 데 대한 자신감을 잃어 버리게됩니다.


람다 바디가 확실한 제약 조건을 제공하는 것처럼 보이지만 컴파일러는 그 정보를 사용할 수없는 다른 예가 있습니다. Java에서 람다 매개 변수 유형은 먼저 확인 되어야만 본문을 볼 수 있습니다. 이는 신중한 결정입니다. 대조적으로, C #은 다른 매개 변수 유형을 시도하여 코드를 컴파일하게 만듭니다. Java는 너무 위험하다고 생각합니다.

어쨌든 암묵적 람다가 실패 할 때, 잦은 일이 발생하면 람다 매개 변수에 명시적인 유형을 제공하십시오. 너의 경우에,(Value<Boolean> value)->true


4

이것을 고치는 쉬운 방법은 메소드 호출에 대한 타입 선언이다.foo:

Foo.<Boolean>foo(value -> true).booleanValue();

편집하다:왜 이것이 필요한지에 대한 구체적인 문서를 찾을 수 없습니다. 다른 모든 사람들과 거의 같습니다. 나는 그것이 원시적 인 유형들로 인한 것 같다고 생각했지만 그것은 옳지 않았다. 그럼에도 불구하고이 구문은 a를 사용하여 호출됩니다.대상 유형. 또한람다의 타겟 유형. 그 이유는 나를 도피하지만, 왜이 특별한 사용 사례가 필요한지에 대한 문서를 찾을 수 없습니다.

편집 2 :나는이 관련된 질문을 발견 :

일반 유형 유추가 메소드 체인과 작동하지 않습니까?

여기에 방법을 연결하고 있기 때문에 그 것처럼 보입니다. 거기에 수용된 대답에서 참조 된 JSR 주석에 따르면, 컴파일러는 유추 된 일반 형식 정보를 양방향으로 연결된 메서드 호출간에 전달하는 방법이 없기 때문에 고의적 인 기능 생략이있었습니다. 결과적으로 전체 유형이 지워지고 시간이 지남에 따라booleanValue. 대상 유형을 추가하면 컴파일러가 아래에 요약 된 규칙을 사용하여 결정을 내리는 대신 수동으로 제약 조건을 제공하여이 동작을 제거합니다.JLS §18, 이것은 전혀 언급하지 않는 것 같습니다. 이것은 내가 생각해 낼 수있는 유일한 정보입니다. 누구든지 더 나은 것을 발견하면 나는 그것을보고 싶다.


  • JLS 참조가 필요한지 잘 모르겠습니다. 일반적으로 추론이 여기에서 효과적이지 않은 이유가 궁금하고 API를 변경할 수있는 경우에 대해 궁금합니다. 작동하게 만들다. 이상적으로는 사용자가 여기에 형식 인수를 수동으로 제공하도록 요구하지 않으려합니다. - Josh Stone
  • 알고리즘과 프리미티브를 사용하여 유추하는 방식에 관한 것입니다. 그냥 true 대신에 Boolean.TRUE를 반환하려고 했습니까? IDE가 아니기 때문에 지금은 테스트 할 수 없습니다. - Brian
  • String이나 Boolean.TRUE와 같은 객체를 반환하면 같은 종류의 문제가 발생합니다. - Josh Stone
  • 연결 방법에 문제가있는 경우 서명 변경이 필요하지 않습니다.T apply(Value value);어떤 긍정적 인 효과를 가지지 만, 그렇다. - sstan
  • @ 브라이언 그 문법은 타겟 타이핑이라고하지 않습니다. 타겟 타이핑은Boolean b = Foo.foo(value -> true);설명하는 구문 (Foo.<Boolean> foo(value->true))는목격하다. - Jeffrey

2

다른 답변과 마찬가지로 나는 똑똑한 사람이 지적 할 수 있기를 바라고 있습니다.컴파일러는 그것을 추론 할 수 없다.T~이다.Boolean.

컴파일러를 도와주는 한 가지 방법은 기존 클래스 / 인터페이스 디자인을 변경하지 않고도 람다 식의 형식 매개 변수 형식을 명시 적으로 선언하는 것입니다. 따라서이 경우 명시 적으로value매개 변수는Value<Boolean>.

void test() {
  Foo.foo((Value<Boolean> value) -> true).booleanValue();
}


  • 클래스 / 인터페이스 디자인을 조금만 변경할 수 있습니다.Foo.foo호출은 boolean의 반환 유형을 유추합니다.value람다에서Value<Boolean>. 이 API는 공개 API의 일부이므로 전송하고 싶지 않으며 캐스트를 요구하면 사용성 문제가 발생합니다. - Josh Stone
  • 그냥 명확히하기 위해, 내 예제는 어떤 캐스팅도 수행하지 않습니다. 타입을 추론하기보다는 람다 표현식의 형식 매개 변수 유형에 대해 명시 적으로 설명하는 것입니다. 하지만 네,이 경우 정확한 유형을 추론 할 수없는 이유를 알고 싶습니다. - sstan

1

왜 그런지는 모르지만 별도의 반환 유형을 추가해야합니다.

public class HelloWorld{
static class Value<T> {
}

@FunctionalInterface
interface Bar<T,R> {
      R apply(Value<T> value); // Return type added
}

static class Foo {
  public static <T,R> R foo(Bar<T,R> callback) {
      return callback.apply(new Value<T>());
  }
}

void test() {
  System.out.println( Foo.foo(value -> true).booleanValue() ); // No compile error here
}
     public static void main(String []args){
         new HelloWorld().test();
     }
}

어떤 똑똑한 사람이 아마 그것을 설명 할 수 있습니다.


  • 그게 우리를 좀 더 가깝게 만들지 만value람다 표현식에서Value<Object>대신에Value<Boolean>여전히 람다 식 몸체에 더 많은 코드가 포함되어있는 실제 시나리오에서 문제를 일으킬 것입니다 ... - Josh Stone
  • 당신이 무슨 뜻인지 확실하지. 내 생각 엔, T에서 메소드를 호출하려면 와일드 카드가 필요하다.static class Value<T extends Boolean>, interface Bar<T extends Boolean,R>, public static <T extends Boolean,R> R foo(Bar<T,R> callback)등 ... - fukanchik
  • Boolean 클래스를 클래스로 바꾸십시오. - fukanchik
  • 그런 것 같은 것은 작동 할 수있는 것처럼 보이지만, 람다의 리턴 타입은 아무 것도 될 수 없다 (사용자에 의해 제어 됨). 부울은 단지 예제 일뿐입니다 ... - Josh Stone

1

문제

값은 유형을 추측합니다.Value<Object>왜냐하면 당신은 람다를 잘못 해석했기 때문입니다. 당신이 람다 (lambda)를 직접 호출하는 것처럼 적용 방법을 직접 생각해보십시오. 그래서 당신이하는 일은 :

Boolean apply(Value value);

이것은 정확하게 다음과 같이 추론됩니다.

Boolean apply(Value<Object> value);

Value에 대한 유형을 지정하지 않았기 때문입니다.

간단한 솔루션

올바른 방법으로 람다에게 전화하십시오 :

Foo.foo((Value<Boolean> value) -> true).booleanValue();

이것은 유추 될 것입니다 :

Boolean apply(Value<Boolean> value);

(내) 권장 솔루션

당신의 솔루션은 좀 더 명확해야합니다. 콜백을 원한다면 리턴 될 타입 값이 필요하다.

제네릭 콜백 인터페이스, 일반 Value 클래스 및 UsingClass를 사용하여이를 사용하는 방법을 보여 줬습니다.

콜백 인터페이스

/**
 *
 * @param <P> The parameter to call
 * @param <R> The return value you get
 */
@FunctionalInterface
public interface Callback<P, R> {

  public R call(P param);
}

가치 클래스

public class Value<T> {

  private final T field;

  public Value(T field) {
    this.field = field;
  }

  public T getField() {
    return field;
  }
}

UsingClass 클래스

public class UsingClass<T> {

  public T foo(Callback<Value<T>, T> callback, Value<T> value) {
    return callback.call(value);
  }
}

기본 TestApp

public class TestApp {

  public static void main(String[] args) {
    Value<Boolean> boolVal = new Value<>(false);
    Value<String> stringVal = new Value<>("false");

    Callback<Value<Boolean>, Boolean> boolCb = (v) -> v.getField();
    Callback<Value<String>, String> stringCb = (v) -> v.getField();

    UsingClass<Boolean> usingClass = new UsingClass<>();
    boolean val = usingClass.foo(boolCb, boolVal);
    System.out.println("Boolean value: " + val);

    UsingClass<String> usingClass1 = new UsingClass<>();
    String val1 = usingClass1.foo(stringCb, stringVal);
    System.out.println("String value: " + val1);

    // this will give you a clear and understandable compiler error
    //boolean val = usingClass.foo(boolCb, stringVal);
  }
}


  • 이 대답은 예제 시나리오에서 조금 벗어난 것으로 보이는데 이는 실제 API를 단순화 한 것입니다. 두 가지 : foo 메소드는 정적이어야하며 단일 인수를 가져야합니다. - Josh Stone
  • @ JoshStone 당신이 말하는 API. 필자가 한 것은 JavaFX에서 테이블 셀 값 팩토리 콜백과 약간 비슷합니다. 당신이 정적 인 것을 원한다는 것은 내가 추측하는 문제가 아닙니다. 그러나 단 하나의 가치? 죄송하지만, 아마 당신은술부함수 나 콜백이 아닙니다. - NwDev
  • 다시 언급 할 가치가 있습니다. 공개 API의 일부이므로 사용하기 간단해야합니다. 사용자는 기본 람다를 제공 할 수 있어야합니다.value -> true어떤 가치가 유추 될지Value<Boolean>그리고 람다를 받아들이는 메소드는 리턴 할 것이다.Boolean. 다른 모든 변경 사항은 괜찮지 만 API를 사용할 수 있도록 유지해야합니다. - Josh Stone
  • 흠, 저에게 API는 유연하고 추상적이어야합니다. 바에서 수행 한 작업이 구체적인 유형으로 작업 중이므로 전혀 유연하지 않습니다. - NwDev
  • 문제를 설명하기위한 예입니다. 실제 API를 사용하는 목표는 유연성이 아니며 간결함을 요구하기 때문에foo이 일을하기 위해 하나의 주장을 받아들이십시오. - Josh Stone

연결된 질문


관련된 질문

최근 질문