Warum kann Java keinen Supertyp ableiten?

19

Wir alle wissen, dass Long sich ausdehnt Number. Warum wird dies nicht kompiliert?

Und wie definiert man die Methode withso, dass das Programm ohne manuelle Umwandlung kompiliert wird?

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

}
  • Typargument (e) für kann nicht abgeleitet werden <F, R> with(F, R)
  • Der Typ getNumber () vom Typ Builder.MyInterface ist Number. Dies ist nicht kompatibel mit dem Rückgabetyp des Deskriptors: Long

Anwendungsfall siehe: Warum wird der Lambda-Rückgabetyp beim Kompilieren nicht überprüft?

jukzi
quelle
Kannst du posten MyInterface?
Maurice Perry
Es ist bereits in der Klasse
Jukzi
Hmm, ich habe es versucht, <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)aber bekommen java.lang.Number cannot be converted to java.lang.Long), was überraschend ist, weil ich nicht sehe, woher der Compiler die Idee hat, dass der Rückgabewert von getterkonvertiert werden muss returnValue.
Jingx
@jukzi OK. Entschuldigung, das habe ich nicht verstanden.
Maurice Perry
Ändern Number getNumber()zu <A extends Number> A getNumber()macht Sachen funktionieren. Keine Ahnung, ob Sie das wollten. Wie andere gesagt haben, ist das Problem, dass MyInterface::getNumberdies eine Funktion sein könnte, die Doublezum Beispiel zurückkehrt und nicht Long. Ihre Deklaration erlaubt es dem Compiler nicht, den Rückgabetyp basierend auf anderen vorhandenen Informationen einzugrenzen. Durch die Verwendung eines generischen Rückgabetyps können Sie dies dem Compiler erlauben, daher funktioniert es.
Giacomo Alzetta

Antworten:

9

Dieser Ausdruck :

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

kann umgeschrieben werden als:

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

Unter Berücksichtigung der Methodensignatur:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R wird abgeleitet Long
  • F wird sein Function<MyInterface, Long>

und Sie übergeben eine Methodenreferenz, die als Function<MyInterface, Number>Dies ist der Schlüssel abgeleitet wird - wie sollte der Compiler vorhersagen, dass Sie tatsächlich Longvon einer Funktion mit einer solchen Signatur zurückkehren möchten ? Es wird das Downcasting nicht für Sie erledigen.

Da Numberes sich um eine Superklasse handelt Longund Numbernicht unbedingt eine Long(aus diesem Grund wird sie nicht kompiliert) - müssten Sie explizit selbst gießen:

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

macht Fzu sein Function<MyIinterface, Long>oder generische Argumente explizit während der Methodenaufruf übergeben , wie Sie getan haben:

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

und wissen Rwird als gesehen Numberund Code wird kompiliert.

michalk
quelle
Das ist eine interessante Typografie. Trotzdem suche ich nach einer Definition der Methode "with", mit der der Aufrufer ohne Cast kompiliert werden kann. Trotzdem danke für diese Idee.
Jukzi
@jukzi Du kannst nicht. Es spielt keine Rolle, wie Ihr withgeschrieben ist. Sie haben MJyInterface::getNumberden Typ Function<MyInterface, Number>so R=Numberund dann auch R=Longdas andere Argument (denken Sie daran, dass Java-Literale nicht polymorph sind!). An diesem Punkt stoppt der Compiler, da es nicht immer möglich ist, a Numberin a zu konvertieren Long. Die einzige Möglichkeit, dies zu beheben, besteht darin, MyInterfacedie Verwendung <A extends Number> Numberals Rückgabetyp zu ändern. Dies hat der Compiler R=Aund dann R=Longund da A extends Numberer ersetzen kannA=Long
Giacomo Alzetta
4

Der Schlüssel zu Ihrem Fehler liegt in der generischen Deklaration des Typs von F: F extends Function<T, R>. Die Aussage, die nicht funktioniert, lautet: new Builder<MyInterface>().with(MyInterface::getNumber, 4L);Erstens haben Sie eine neue Builder<MyInterface>. Die Deklaration der Klasse impliziert daher T = MyInterface. Wie pro Ihre Erklärung with, Fmuss eine sein Function<T, R>, die eine ist Function<MyInterface, R>in dieser Situation. Daher muss der Parameter gettereinen MyInterfaceas-Parameter (erfüllt durch die Methodenreferenzen MyInterface::getNumberund MyInterface::getLong) und return R, der vom selben Typ wie der zweite Parameter der Funktion sein muss with. Lassen Sie uns nun sehen, ob dies für alle Ihre Fälle gilt:

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

Sie können dieses Problem mit den folgenden Optionen "beheben":

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

Über diesen Punkt hinaus ist es meistens eine Entwurfsentscheidung, für welche Option die Codekomplexität für Ihre spezielle Anwendung reduziert wird. Wählen Sie also, was am besten zu Ihnen passt.

Der Grund, warum Sie dies nicht ohne Casting tun können, liegt im Folgenden aus der Java-Sprachspezifikation :

Bei der Boxkonvertierung werden Ausdrücke eines primitiven Typs als Ausdrücke eines entsprechenden Referenztyps behandelt. Insbesondere werden die folgenden neun Conversions als Box-Conversions bezeichnet :

  • Vom Typ Boolean zum Typ Boolean
  • Vom Typ Byte zum Typ Byte
  • Vom Typ kurz zum Typ kurz
  • Vom Typ char zum Typ Character
  • Vom Typ int zum Typ Integer
  • Von Typ Long zu Typ Long
  • Vom Typ float zum Typ Float
  • Vom Typ double zum Typ Double
  • Vom Nulltyp zum Nulltyp

Wie Sie deutlich sehen können, gibt es keine implizite Box-Konvertierung von Long zu Number, und die erweiterte Konvertierung von Long zu Number kann nur erfolgen, wenn der Compiler sicher ist, dass eine Number und keine Long erforderlich ist. Da es einen Konflikt zwischen der Methodenreferenz, für die eine Zahl erforderlich ist, und der 4L, für die ein Long bereitgestellt wird, gibt, kann der Compiler (aus irgendeinem Grund ???) den logischen Sprung, dass Long eine Zahl ist, nicht ausführen und daraus schließen, dass Fes sich um eine Zahl handelt Function<MyInterface, Number>.

Stattdessen konnte ich das Problem beheben, indem ich die Funktionssignatur leicht bearbeitete:

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

Nach dieser Änderung tritt Folgendes auf:

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

Bearbeiten:
Nachdem Sie mehr Zeit damit verbracht haben, ist es ärgerlich schwierig, die getterbasierte Typensicherheit durchzusetzen. Hier ist ein Arbeitsbeispiel, das Setter-Methoden verwendet, um die Typensicherheit eines Builders zu erzwingen:

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

Vorausgesetzt, es kann typsicher sein, ein Objekt zu erstellen, können wir hoffentlich irgendwann in der Zukunft ein unveränderliches Datenobjekt vom Builder zurückgeben (möglicherweise durch Hinzufügen einer toRecord()Methode zur Schnittstelle und Angabe des Builders als Builder<IntermediaryInterfaceType, RecordType>). Sie müssen sich also nicht einmal darum kümmern, dass das resultierende Objekt geändert wird. Ehrlich gesagt ist es eine absolute Schande, dass es so viel Aufwand erfordert, einen typsicheren, feldflexiblen Builder zu erhalten, aber es ist wahrscheinlich unmöglich ohne einige neue Funktionen, Codegenerierung oder eine nervige Menge an Reflexion.

Avi
quelle
Vielen Dank für all Ihre Arbeit, aber ich sehe keine Verbesserung in Bezug auf die Vermeidung der manuellen Typisierung. Mein naives Verständnis ist, dass ein Compiler in der Lage sein sollte, alles abzuleiten (dh zu typisieren), was ein Mensch kann.
Jukzi
@jukzi Mein naives Verständnis ist das gleiche, aber aus irgendeinem Grund funktioniert es nicht so. Ich habe mir eine Problemumgehung ausgedacht, mit der der gewünschte Effekt erzielt wird
Avi
Danke noch einmal. Ihr neuer Vorschlag ist jedoch zu umfangreich (siehe stackoverflow.com/questions/58337639 ), da er die Kompilierung von ".with (MyInterface :: getNumber," I AM NOT A NUMBER ")" ermöglicht.
Jukzi
Ihr Satz "Compiler kann nicht gleichzeitig auf die Art der Methodenreferenz und 4L schließen" ist cool. Aber ich will es einfach anders herum. Der Compiler sollte Number basierend auf dem ersten Parameter versuchen und eine Erweiterung auf Number des zweiten Parameters durchführen.
Jukzi
1
WHAAAAAAAAAAAT? Warum funktioniert BiConsumer wie vorgesehen, Function nicht? Ich verstehe die Idee nicht. Ich gebe zu, dass dies genau die Typensicherheit ist, die ich wollte, aber leider funktioniert es nicht mit Gettern. WARUM WARUM WARUM.
Jukzi
1

Es scheint, dass der Compiler den Wert 4L verwendet hat, um zu entscheiden, dass R Long ist, und getNumber () eine Zahl zurückgibt, die nicht unbedingt Long ist.

Aber ich bin mir nicht sicher, warum der Wert Vorrang vor der Methode hat ...

Rick
quelle
0

Der Java-Compiler ist im Allgemeinen nicht in der Lage, mehrere / verschachtelte generische Typen oder Platzhalter abzuleiten. Oft kann ich nichts kompilieren, ohne eine Hilfsfunktion zu verwenden, um einige der Typen zu erfassen oder daraus zu schließen.

Aber müssen Sie wirklich den genauen Typ von Functionas erfassen F? Wenn nicht, funktioniert möglicherweise das Folgende, und wie Sie sehen können, scheint es auch mit Subtypen von zu funktionieren 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
quelle
"with (MyInterface :: getNumber," NOT A NUMBER ")" sollte nicht kompiliert werden
jukzi
0

Der interessanteste Teil liegt im Unterschied zwischen diesen beiden Zeilen, denke ich:

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

Im ersten Fall ist das Texplizit Number, also 4Lauch ein Number, kein Problem. Im zweiten Fall 4List a Long, so Tist a Long, also ist Ihre Funktion nicht kompatibel und Java kann nicht wissen, ob Sie gemeint haben Numberoder Long.

njzk2
quelle
0

Mit folgender Unterschrift:

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

Alle Ihre Beispiele werden kompiliert, mit Ausnahme des dritten, für das die Methode explizit zwei Typvariablen haben muss.

Der Grund dafür, dass Ihre Version nicht funktioniert, liegt darin, dass Javas Methodenreferenzen keinen bestimmten Typ haben. Stattdessen haben sie den Typ, der im angegebenen Kontext erforderlich ist. In Ihrem Fall Rwird vermutet, dass dies Longan der liegt 4L, aber der Getter kann den Typ nicht haben, Function<MyInterface,Long>da in Java generische Typen in ihren Argumenten unveränderlich sind.

Hoopje
quelle
Ihr Code würde kompiliert, with( getNumber,"NO NUMBER")was nicht erwünscht ist. Es ist auch nicht wahr, dass Generika immer unveränderlich sind (siehe stackoverflow.com/a/58378661/9549750 für einen Beweis, dass sich Generika von Setzern anders verhalten als die von Gettern)
jukzi
@jukzi Ah, meine Lösung wurde bereits von Avi vorgeschlagen. Schade... :-). Übrigens können wir Thing<Cat>einer Thing<? extends Animal>Variablen a zuweisen , aber für eine echte Kovarianz würde ich erwarten, dass a a Thing<Cat>zugewiesen werden kann Thing<Animal>. Andere Sprachen wie Kotlin ermöglichen die Definition von Variablen vom Typ Co und Contravariant.
Hoopje