Warum kann Java keinen Supertyp ableiten




type-inference (4)

Der Java-Compiler kann im Allgemeinen nicht auf mehrere / verschachtelte generische Typen oder Platzhalter schließen. Oft kann ich nichts kompilieren, ohne eine Hilfsfunktion zum Erfassen oder Ableiten einiger Typen zu verwenden.

Aber müssen Sie wirklich die genaue Art der Function als F erfassen? Wenn nicht, funktioniert vielleicht das Folgende, und wie Sie sehen können, scheint es auch mit Subtypen von 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");

    }
}

Wir alle wissen, dass sich die Anzahl verlängert. Warum kompiliert das nicht? Und wie man die Methode "mit" definiert, damit das Programm ohne manuelle Umwandlung kompiliert.


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

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

Weitere Informationen finden Sie unter: Warum wird der Lambda-Rückgabetyp beim Kompilieren nicht überprüft ?


Der Schlüssel zu Ihrem Fehler liegt in der generischen Deklaration des Typs F : F extends Function<T, R> . Die Anweisung, die nicht funktioniert, lautet: new Builder<MyInterface>().with(MyInterface::getNumber, 4L); Zunächst haben Sie einen neuen Builder<MyInterface> . Die Deklaration der Klasse impliziert daher T = MyInterface . Function<MyInterface, R> Ihrer Deklaration von with muss F eine Function<T, R> , die in dieser Situation eine Function<MyInterface, R> . Daher muss der Parameter getter ein MyInterface als Parameter MyInterface (erfüllt durch die Methodenreferenzen MyInterface::getNumber und MyInterface::getLong ) und R , das derselbe Typ sein muss wie der zweite Parameter für die Funktion with . Nun wollen wir 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);

Darüber hinaus ist es meistens eine Entwurfsentscheidung, für welche Option die Codekomplexität für Ihre bestimmte Anwendung verringert wird. Wählen Sie also, was für Sie am besten geeignet ist.

Der Grund, warum Sie dies nicht ohne Casting tun können, ergibt sich aus der Java-Sprachspezifikation :

Bei der Boxing-Konvertierung werden Ausdrücke eines primitiven Typs als Ausdrücke eines entsprechenden Referenztyps behandelt. Im Einzelnen werden die folgenden neun Conversions als Boxing-Conversions bezeichnet :

  • Vom Typ Boolean zum Typ Boolean
  • Vom Typ Byte zum Typ Byte
  • Vom Typ kurz bis zum Typ kurz
  • Vom Typ char bis zum Typ Character
  • Vom Typ int bis zum Typ Integer
  • Vom Typ Long zum Typ Long
  • Vom Typ float zum Typ Float
  • Vom Typ Double zum Typ Double
  • Vom NULL-Typ zum NULL-Typ

Wie Sie deutlich sehen können, gibt es keine implizite Boxing-Konvertierung von Long in Number, und die erweiterte Konvertierung von Long in 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 F eine 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 geschieht Folgendes:

// 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 einige Zeit damit verbracht haben, ist es ärgerlich schwierig, die getterbasierte Typensicherheit durchzusetzen. Hier ist ein funktionierendes Beispiel, 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, dass ein Objekt typsicher erstellt werden kann, können wir hoffentlich zu einem späteren Zeitpunkt ein unveränderliches Datenobjekt vom Builder zurückgeben (möglicherweise durch Hinzufügen einer toRecord() -Methode zur Schnittstelle und Angeben des Builders als einen Builder<IntermediaryInterfaceType, RecordType> ), sodass Sie sich nicht einmal Gedanken darüber machen müssen Builder<IntermediaryInterfaceType, RecordType> das resultierende Objekt geändert wird. Ehrlich gesagt ist es eine Schande, dass es so viel Mühe erfordert, einen typsicheren feldflexiblen Builder zu erhalten, aber es ist wahrscheinlich unmöglich, ohne einige neue Funktionen, Code-Generierung oder eine nervige Menge an Überlegungen.


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 zu Long gefolgert
  • F ist die Function<MyInterface, Long>

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

Da Number eine übergeordnete Klasse von Long und Number nicht unbedingt ein Long (aus diesem Grund wird es nicht kompiliert), müssten Sie das Casting explizit selbst durchführen:

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

mache F zur Function<MyIinterface, Long> oder übergebe generische Argumente explizit während des Methodenaufrufs wie du es getan hast:

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

und wissen, dass R als Number und der Code kompiliert wird.


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

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





type-inference