Dlaczego Java nie może wywnioskować nadtypu?

19

Wszyscy wiemy, że Long rozciąga się Number. Dlaczego więc się nie kompiluje?

Jak zdefiniować metodę, withaby program kompilował się bez ręcznego rzutowania?

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;
  }

}
  • Nie można wnioskować o argumentach typu dla <F, R> with(F, R)
  • Typ getNumber () z typu Builder.MyInterface to Number, jest to niezgodne z typem zwracanym przez deskryptor: Long

Przykład użycia patrz: Dlaczego nie jest sprawdzany typ zwracany przez lambda w czasie kompilacji

jukzi
źródło
Czy możesz pisać MyInterface?
Maurice Perry
jest już w klasie
jukzi
Hmm próbowałem, <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)ale dostałem java.lang.Number cannot be converted to java.lang.Long), co jest zaskakujące, ponieważ nie widzę, skąd kompilator ma pomysł, na getterktóry trzeba będzie przekonwertować wartość zwracaną returnValue.
jingx
@jukzi OK. Przepraszam, tęskniłem za tym.
Maurice Perry
Zmiana Number getNumber()na <A extends Number> A getNumber()sprawia, że ​​rzeczy działają. Nie mam pojęcia, czy tego właśnie chciałeś. Jak powiedzieli inni, problemem jest to, że MyInterface::getNumbermoże to być funkcja, która Doublena przykład zwraca, a nie zwraca Long. Deklaracja nie pozwala kompilatorowi zawęzić typu zwracanego na podstawie innych obecnych informacji. Używając ogólnego typu zwracanego, pozwalasz na to kompilatorowi, dlatego działa.
Giacomo Alzetta

Odpowiedzi:

9

To wyrażenie:

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

może być przepisany jako:

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

Biorąc pod uwagę podpis metody:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R zostaną wywnioskowane z Long
  • F będzie Function<MyInterface, Long>

i przekazujesz referencję do metody, która zostanie wprowadzona jako Function<MyInterface, Number>To jest klucz - w jaki sposób kompilator powinien przewidzieć, że rzeczywiście chcesz powrócić Longz funkcji z taką sygnaturą? Nie zrobi to za Ciebie.

Ponieważ Numberjest nadklasą Longi Numbernie jest necessairly Long(dlatego nie skompilować) - trzeba by rzucić jawnie na własną rękę:

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

czyni Fsię Function<MyIinterface, Long>lub przechodzą generyczne argumenty wyraźnie podczas wywołania metody jak ty:

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

i know Rbędzie postrzegane jako, Numbera kod się skompiluje.

Michalk
źródło
To interesujący typecast. Ale wciąż szukam definicji metody „za pomocą”, która sprawi, że program wywołujący skompiluje się bez użycia rzutowania. W każdym razie dzięki za ten pomysł.
jukzi
@jukzi Nie możesz. Nie ma znaczenia, jak withjest napisane. Masz MJyInterface::getNumbertyp Function<MyInterface, Number>tak, R=Numbera następnie masz również R=Longz drugiego argumentu (pamiętaj, że literały Java nie są polimorficzne!). W tym momencie kompilator zatrzymuje się, ponieważ nie zawsze można przekonwertować a Numberna a Long. Jedynym sposobem, aby to naprawić, jest zmiana MyInterfacena <A extends Number> Numbertyp zwracany, co sprawia, że ​​kompilator ma, R=Aa następnie, R=Longa ponieważ A extends Numbermoże zastąpićA=Long
Giacomo Alzetta
4

Kluczem do błędu jest w ogólnym deklaracji typu F: F extends Function<T, R>. Oświadczenie, które nie działa: new Builder<MyInterface>().with(MyInterface::getNumber, 4L);Po pierwsze, masz nowy Builder<MyInterface>. Deklaracja klasy implikuje zatem T = MyInterface. Zgodnie z deklaracją with, Fmusi być Function<T, R>, co jest Function<MyInterface, R>w tej sytuacji. Dlatego parametr gettermusi przyjąć MyInterfaceparametr as (spełniony przez odwołania do metod MyInterface::getNumberi MyInterface::getLong) i zwrócić R, który musi być tego samego typu co drugi parametr funkcji with. Zobaczmy teraz, czy dotyczy to wszystkich twoich przypadków:

// 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);

Możesz „naprawić” ten problem za pomocą następujących opcji:

// 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);

Poza tym jest to głównie decyzja projektowa, dla której opcja zmniejsza złożoność kodu dla konkretnej aplikacji, więc wybierz to, co najbardziej ci odpowiada.

Powód, dla którego nie można tego zrobić bez przesyłania, jest następujący: ze specyfikacji języka Java :

Konwersja boksu traktuje wyrażenia typu pierwotnego jako wyrażenia odpowiedniego typu odwołania. W szczególności następujące dziewięć konwersji nazywane są konwersjami bokserskimi :

  • Od typu logicznego do typu logicznego
  • Od typu bajtu do typu Byte
  • Od typu krótkiego do typu krótkiego
  • Od typu char do typu Character
  • Od typu int do typu Integer
  • Od typu długiego do typu Długiego
  • Od typu float do type Float
  • Od typu double do typu double
  • Od typu zerowego do typu zerowego

Jak wyraźnie widać, nie istnieje niejawna konwersja boksu z długiej na liczbę, a konwersja rozszerzająca z długiej na liczbę może wystąpić tylko wtedy, gdy kompilator jest pewien, że wymaga liczby, a nie długiej. Ponieważ istnieje konflikt między odwołaniem do metody wymagającym liczby a wartością 4L zapewniającą wartość Long, kompilator (z jakiegoś powodu ???) nie może wykonać logicznego skoku, że Long jest liczbą i wydedukować, że Fjest to Function<MyInterface, Number>.

Zamiast tego udało mi się rozwiązać problem, lekko edytując podpis funkcji:

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

Po tej zmianie następują:

// 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);

Edycja:
po spędzeniu nad tym trochę czasu, irytująco trudno jest egzekwować bezpieczeństwo oparte na getterach. Oto działający przykład, który wykorzystuje metody ustawiające w celu wymuszenia bezpieczeństwa typu konstruktora:

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");
  }
}

Pod warunkiem, że jest to bezpieczna dla typu konstrukcja obiektu, mam nadzieję, że w pewnym momencie w przyszłości będziemy mogli zwrócić niezmienny obiekt danych z konstruktora (może dodając toRecord()metodę do interfejsu i określając konstruktor jako a Builder<IntermediaryInterfaceType, RecordType>), więc nie musisz się nawet martwić modyfikacją powstałego obiektu. Szczerze mówiąc, to absolutny wstyd, że wymaga tyle wysiłku, aby uzyskać bezpieczną dla typu, elastycznego konstruktora, ale prawdopodobnie jest to niemożliwe bez nowych funkcji, generowania kodu lub irytującej refleksji.

Avi
źródło
Dzięki za całą pracę, ale nie widzę żadnej poprawy w kierunku unikania ręcznego rzutowania. Naiwnie rozumiem, że kompilator powinien mieć możliwość wnioskowania (tj. Rzutowania) wszystkiego, co człowiek może.
jukzi
@jukzi Moje naiwne rozumienie jest takie samo, ale z jakiegokolwiek powodu nie działa w ten sposób. Wymyśliłem obejście, które prawie osiąga pożądany efekt
Avi
Dzięki jeszcze raz. Ale twoja nowa propozycja jest zbyt szeroka (patrz stackoverflow.com/questions/58337639 ), ponieważ umożliwia kompilację „.with (MyInterface :: getNumber,„ I AM NOT A NUMBER ”)”;
jukzi
zdanie „Kompilator nie może wnioskować o typie metody i 4L jednocześnie” jest fajne. Ale chcę, żeby było inaczej. kompilator powinien wypróbować liczbę na podstawie pierwszego parametru i wykonać rozszerzenie do liczby drugiego parametru.
jukzi
1
WHAAAAAAAAAAAT? Dlaczego BiConsumer działa zgodnie z przeznaczeniem, a funkcja nie? Nie mam pomysłu. Przyznaję, że dokładnie tego chciałem, ale niestety nie działa z getterami. DLACZEGO DLACZEGO DLACZEGO.
jukzi
1

Wydaje się, że kompilator użył wartości 4L, aby zdecydować, że R jest Długi, a getNumber () zwraca liczbę, która niekoniecznie jest Długa.

Ale nie jestem pewien, dlaczego wartość ma pierwszeństwo przed metodą ...

Stóg
źródło
0

Kompilator Java na ogół nie jest dobry do wnioskowania o wielu / zagnieżdżonych typach ogólnych lub symbolach wieloznacznych. Często nie mogę uzyskać czegoś do skompilowania bez użycia funkcji pomocnika do przechwytywania lub wnioskowania o niektórych typach.

Ale czy naprawdę musisz uchwycić dokładny typ Functionjako F? Jeśli nie, być może poniższe prace, a jak widać, również działają z podtypami 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");

    }
}
machfour
źródło
„with (MyInterface :: getNumber,„ NOT A NUMBER ”)” nie powinno się kompilować
jukzi
0

Myślę, że najciekawsza część polega na różnicy między tymi dwoma liniami:

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

W pierwszym przypadku, Tto wyraźnie Number, więc 4Lto również Numbernie ma problemu. W drugim przypadku 4Ljest to Long, a więc Tjest Long, więc twoja funkcja nie jest kompatybilna, a Java nie może wiedzieć, czy masz na myśli Numberczy Long.

njzk2
źródło
0

Z następującym podpisem:

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

wszystkie twoje przykłady kompilują się, z wyjątkiem trzeciego, który wyraźnie wymaga, aby metoda miała dwie zmienne typu.

Przyczyną tego, że Twoja wersja nie działa, jest to, że odwołania do metod Java nie mają jednego określonego typu. Zamiast tego mają typ wymagany w danym kontekście. W twoim przypadku Rzakłada się , że dzieje się Longtak z powodu 4L, ale moduł pobierający nie może mieć typu, Function<MyInterface,Long>ponieważ w Javie typy ogólne są niezmienne w swoich argumentach.

Hoopje
źródło
Twój kod skompiluje się, with( getNumber,"NO NUMBER")co nie jest pożądane. Nie jest też prawdą, że generyczne są zawsze niezmienne (patrz stackoverflow.com/a/58378661/9549750, aby udowodnić, że generyczne setery zachowują się inaczej niż te
getterów
@ jukzi Ah, moje rozwiązanie zostało już zaproponowane przez Avi. Szkoda ... :-). Nawiasem mówiąc, to prawda, że możemy przypisać Thing<Cat>do Thing<? extends Animal>zmiennej, ale na prawdziwe kowariancji Spodziewam się, że Thing<Cat>może być przypisany do Thing<Animal>. Inne języki, takie jak Kotlin, pozwalają definiować zmienne typu ko- i kontrowariantnego.
Hoopje