Java가 왜 수퍼 타입을 유추 할 수 없습니까?


19

우리는 모두 Long extends를 알고 Number있습니다. 왜 이것이 컴파일되지 않습니까?

그리고 with프로그램이 수동 캐스트없이 컴파일되도록 메소드를 정의하는 방법 은 무엇입니까?

import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}
  • 에 대한 유형 인수를 유추 할 수 없습니다 <F, R> with(F, R)
  • Builder.MyInterface 유형의 getNumber () 유형은 Number입니다. 설명 자의 리턴 유형과 호환되지 않습니다. Long

사용 사례 는 컴파일 타임에 람다 리턴 유형이 확인되지 않는 이유를 참조하십시오.


당신은 게시 할 수 있습니까 MyInterface?
모리스 페리

이미 클래스 내부
jukzi

흠 시도 <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)했지만 얻었습니다 java.lang.Number cannot be converted to java.lang.Long). 컴파일러가 반환 값 getter을로 변환해야 한다는 아이디어를 어디서 얻지 못하기 때문에 놀랍 습니다 returnValue.
jingx

@jukzi OK. 죄송합니다.
Maurice Perry

변경 Number getNumber()<A extends Number> A getNumber()차종 물건 작동합니다. 이것이 당신이 원하는 것인지 전혀 모른다. 다른 사람들이 말했듯이 문제는 예를 들어 MyInterface::getNumber반환 Double하지 않는 함수 일 수 있다는 것 Long입니다. 선언하면 컴파일러가 존재하는 다른 정보를 기반으로 반환 유형을 좁힐 수 없습니다. 일반 반환 유형을 사용하면 컴파일러에서 그렇게 할 수 있으므로 작동합니다.
Giacomo Alzetta

답변:


9

이 표현 :

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

다음과 같이 다시 작성할 수 있습니다.

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

메소드 서명 고려 :

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R ~에 추론 될 것이다 Long
  • F 될거야 Function<MyInterface, Long>

그리고 다음과 같이 추론 될 메소드 참조를 전달합니다. Function<MyInterface, Number>이것이 핵심입니다- 컴파일러는 실제로 Long그러한 서명을 가진 함수에서 리턴하려고한다고 어떻게 예측해야 합니까? 다운 캐스팅을하지 않습니다.

이후 Number의 슈퍼 클래스입니다 LongNumbernecessairly없는 Long(이 컴파일되지 않는 이유입니다) - 당신은 당신의 자신에 명시 적으로 캐스팅해야합니다 :

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

만들 FFunction<MyIinterface, Long>또는 당신이했던 것처럼 메소드 호출시 명시 적으로 일반적인 인수를 전달 :

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

및 노하우가 R로 간주됩니다 Number코드를 컴파일합니다.


흥미로운 캐스트입니다. 그러나 여전히 나는 발신자가 캐스트없이 컴파일 할 수있는 "with"메서드의 정의를 찾고 있습니다. 어쨌든 그 아이디어에 감사드립니다.
jukzi

@jukzi 당신은 할 수 없습니다. 어떻게 with작성 되는지 는 중요하지 않습니다 . 당신은이 MJyInterface::getNumber유형이 Function<MyInterface, Number>있으므로 R=Number다음 당신은 또한이 R=Long다른 인수에서 (자바 리터럴 다형성 아니라는 것을 기억!). 이 시점에서 a Number를 a 로 변환 할 수는 없기 때문에 컴파일러가 중지됩니다 Long. 이 문제를 해결하는 유일한 방법은 변경할 수 있습니다 MyInterface사용하는 <A extends Number> Number반환 형식으로,이 컴파일러가 만들어 R=A다음 R=Long과 이후로 A extends Number그것을 대체 할 수 있습니다A=Long
자코모 Alzetta

4

당신의 오류의 핵심은 유형의 일반 선언에 F: F extends Function<T, R>. 작동하지 않는 진술은 다음과 같습니다. new Builder<MyInterface>().with(MyInterface::getNumber, 4L);첫째, 새로운 Builder<MyInterface>. 따라서이 클래스의 선언은 다음을 의미합니다 T = MyInterface. 당신의 선언에 따라 with, F해야 Function<T, R>을 인 Function<MyInterface, R>이 상황에서. 따라서 매개 변수 getter는 매개 변수 MyInterface로 메소드 ( MyInterface::getNumber및 메소드 참조에 의해 만족 됨)를 가져와 MyInterface::getLong리턴 R해야합니다 with. 이는 함수에 대한 두 번째 매개 변수와 동일한 유형이어야합니다 . 이제 이것이 모든 경우에 해당되는지 확인하십시오.

// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

다음 옵션으로이 문제를 "수정"할 수 있습니다.

// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

이 시점을 넘어 서면 대부분의 옵션이 특정 응용 프로그램의 코드 복잡성을 줄이는 옵션으로 결정되므로 가장 적합한 것을 선택하십시오.

캐스트하지 않고이 작업을 수행 할 수없는 이유 는 Java 언어 사양에서 다음과 같습니다 .

복싱 변환은 기본 유형의 표현을 해당 참조 유형의 표현으로 처리합니다. 특히 다음 9 가지 변환을 권투 변환 이라고합니다 .

  • 부울 형식에서 부울 형식으로
  • 바이트 유형에서 바이트 유형으로
  • 짧은 유형에서 짧은 유형으로
  • char 타입에서 Character 타입으로
  • int 유형에서 Integer 유형으로
  • 긴 유형에서 긴 유형으로
  • float 유형에서 Float 유형으로
  • 더블 타입에서 더블 타입으로
  • 널 유형에서 널 유형으로

분명히 알 수 있듯이 long에서 Number 로의 암시 적 boxing 변환은 없으며 Long에서 Number 로의 확장 변환은 컴파일러가 Long이 아닌 Number가 필요하다고 확신하는 경우에만 발생할 수 있습니다. 긴-있는 번호와를 추론하는 논리적 도약 할 수없는 (어떤 이유로 ???) 숫자와 롱을 제공 4L, 컴파일러를 필요로하는 방법 참조 사이에 충돌이이기 때문에 FA는가 Function<MyInterface, Number>.

대신 함수 서명을 약간 편집하여 문제를 해결했습니다.

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

이 변경 후 다음이 발생합니다.

// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

편집 :
그것에 더 많은 시간을 보낸 후 게터 기반 유형 안전을 시행하는 것은 성가신 일입니다. 다음은 setter 메소드를 사용하여 빌더의 유형 안전성을 적용하는 실제 예제입니다.

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

미래에 어느 시점에서 객체를 생성하는 형식 안전 기능을 제공하면 빌더에서 불변 데이터 객체 를 반환 할 수 있습니다 ( toRecord()인터페이스에 메소드를 추가 하고 빌더를로 지정 Builder<IntermediaryInterfaceType, RecordType>). 결과 객체가 수정되는 것에 대해 걱정할 필요조차 없습니다. 솔직히 말해서, 유형 안전 필드 유연한 빌더를 구현하는 데 많은 노력이 필요한 것은 절대적인 수치이지만 새로운 기능, 코드 생성 또는 성가신 성찰이 없다면 불가능할 것입니다.


모든 작업에 감사하지만 수동 타입 캐스트를 피하는 것에 대한 개선 사항은 없습니다. 저의 순진한 이해는 컴파일러가 인간이 할 수있는 모든 것을 유추 할 수 있어야한다는 것입니다.
jukzi

@jukzi 내 순진한 이해는 동일하지만 어떤 이유로 든 그렇게 작동하지 않습니다. 그래도 원하는 효과를 거의 얻을 수있는 해결 방법을 생각해 냈습니다.
Avi

다시 감사합니다. 그러나 새 제안이 너무 넓습니다 ( stackoverflow.com/questions/58337639 참조 ) ".with (MyInterface :: getNumber,"I AM NOT A NUMBER ")";
jukzi

"컴파일러는 동시에 메소드 참조 유형과 4L을 유추 할 수 없습니다"라는 문장이 좋습니다. 그러나 나는 단지 다른 방법으로 arround를 원합니다. 컴파일러는 첫 번째 매개 변수를 기반으로 숫자를 시도하고 두 번째 매개 변수의 숫자를 넓 힙니다.
jukzi

1
WHAAAAAAAAAAAT? 기능이 작동하지 않는데 왜 BiConsumer가 의도 한대로 작동합니까? 나는 아이디어를 얻지 못했다. 나는 이것이 내가 원하는 typesafety임을 인정하지만 불행히도 getter와는 작동하지 않습니다. 왜 왜 왜.
jukzi

1

컴파일러가 R을 Long으로 결정하기 위해 값 4L을 사용하고 getNumber ()가 반드시 Long이 아닌 Number를 리턴하는 것으로 보입니다.

그러나 왜 값이 메소드보다 우선하는지 확실하지 않습니다 ...


0

Java 컴파일러는 일반적으로 다중 / 중첩 된 일반 유형 또는 와일드 카드를 유추하는 데 능숙하지 않습니다. 종종 도우미 함수를 사용하여 일부 유형을 캡처하거나 유추하지 않으면 컴파일 할 무언가를 얻을 수 없습니다.

그러나, 당신은 정말의 정확한 유형 캡처 할 필요합니까 Function등을 F? 그렇지 않은 경우 다음과 같이 작동하며 아시다시피 하위 유형에서도 작동하는 것 같습니다 Function.

import java.util.function.Function;
import java.util.function.UnaryOperator;

public class Builder<T> {
    public interface MyInterface {
        Number getNumber();
        Long getLong();
    }

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

    // example subclass of Function
    private static UnaryOperator<String> stringFunc = (s) -> (s + ".");

    public static void main(String[] args) {
        // works
        new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
        // works
        new Builder<String>().with(stringFunc, "s");

    }
}

"(숫자해야하지 컴파일하는 MyInterface :: getNumber,") "와"
jukzi

0

가장 흥미로운 부분은 두 줄의 차이점에 있습니다.

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

첫 번째 경우는 T명시 적이다 Number, 그래서 4L도없는 Number, 아무 문제. 두 번째 경우, 4LA는 Long, 그래서 TA는 Long함수가 호환되지 않습니다, 그래서, 당신이 의미하는 경우 자바는 알 수 없다 NumberLong.


0

다음과 같은 서명으로 :

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

세 번째를 제외한 모든 예제는 컴파일됩니다.이 메서드에는 두 가지 유형 변수가 있어야합니다.

버전이 작동하지 않는 이유는 Java의 메소드 참조에 특정 유형이 없기 때문입니다. 대신, 주어진 컨텍스트에서 필요한 유형을 갖습니다. 귀하의 경우 R는로 Long인해 추론 4L되지만 Function<MyInterface,Long>Java에서는 제네릭 형식이 인수에서 변하지 않기 때문에 getter가 형식을 가질 수 없습니다 .


귀하의 코드 with( getNumber,"NO NUMBER")는 원하지 않는 컴파일 됩니다. 또한 제네릭이 항상 변하지 않는 것은 사실이 아닙니다 ( 세터의 제네릭이 게터의 제네릭과 다른 행동을한다는 ​​증거는 stackoverflow.com/a/58378661/9549750 참조 )
jukzi

@ jukzi Ah, 내 솔루션은 이미 Avi에서 제안했습니다. 너무 나쁘다 ... :-). 그건 그렇고, 변수에 a Thing<Cat>를 할당 할 수는 Thing<? extends Animal>있지만, 실제 공분산의 경우 a에 a Thing<Cat>를 할당 할 수 있을 것으로 기대합니다 Thing<Animal>. Kotlin과 같은 다른 언어에서는 공변량 및 반 변형 유형 변수를 정의 할 수 있습니다.
Hoopje
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.