Perché Java non può dedurre un supertipo?


19

Sappiamo tutti che Long si estende Number. Quindi perché questo non si compila?

E come definire il metodo in withmodo che il programma venga compilato senza alcun cast manuale?

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

}
  • Impossibile dedurre argomenti di tipo per <F, R> with(F, R)
  • Il tipo di getNumber () dal tipo Builder.MyInterface è Number, questo è incompatibile con il tipo restituito dal descrittore: Long

Per il caso d'uso vedere: Perché il tipo restituito lambda non viene verificato al momento della compilazione


Puoi pubblicare MyInterface?
Maurice Perry,

è già dentro la classe
jukzi il

Hmm, ho provato <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)ma ottenuto java.lang.Number cannot be converted to java.lang.Long), il che è sorprendente perché non vedo da dove il compilatore abbia l'idea che il valore restituito getterdovrà essere convertito returnValue.
jingx,

@jukzi OK. Scusa, mi sono perso.
Maurice Perry,

Cambiare Number getNumber()per far <A extends Number> A getNumber()funzionare le cose. Non ho idea se questo è quello che volevi. Come altri hanno già detto, il problema è che MyInterface::getNumberpotrebbe essere una funzione che ritorna Doublead esempio e non Long. La tua dichiarazione non consente al compilatore di restringere il tipo di ritorno in base ad altre informazioni presenti. Usando un tipo di ritorno generico si consente al compilatore di farlo, quindi funziona.
Giacomo Alzetta,

Risposte:


9

Questa espressione:

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

può essere riscritto come:

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

Tenendo conto della firma del metodo:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R sarà inferito a Long
  • F sarà Function<MyInterface, Long>

e si passa un riferimento al metodo che verrà dedotto in quanto Function<MyInterface, Number>questa è la chiave: come dovrebbe prevedere il compilatore che si desidera effettivamente tornare Longda una funzione con tale firma? Non eseguirà il downcasting per te.

Poiché Numberè una superclasse di Longe Numbernon è necessariamente una Long(questo è il motivo per cui non si compila) - dovresti lanciare esplicitamente da solo:

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

facendo Fin modo di essere Function<MyIinterface, Long>o passare esplicitamente argomenti generici durante la chiamata del metodo come hai fatto:

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

e sapere Rsarà visto come Numbere il codice verrà compilato.


Questo è un typecast interessante. Ma sto ancora cercando una definizione del metodo "with" che farà compilare il chiamante senza alcun cast. Comunque grazie per quell'idea.
jukzi,

@jukzi Non puoi. Non importa come withè scritto. Hai MJyInterface::getNumberdigitato Function<MyInterface, Number>così R=Numbere poi hai anche R=Longdall'altro argomento (ricorda che i letterali di Java non sono polimorfici!). A questo punto il compilatore si interrompe perché non è sempre possibile convertire a Numberin Long. L'unico modo per risolvere questo problema è cambiare MyInterfaceper usare <A extends Number> Numbercome tipo di ritorno, questo fa sì che il compilatore abbia R=Ae quindi R=Longe poiché A extends Numberpuò sostituireA=Long
Giacomo Alzetta,

4

La chiave per il vostro errore è nella dichiarazione generica del tipo di F: F extends Function<T, R>. L'affermazione che non funziona è: in new Builder<MyInterface>().with(MyInterface::getNumber, 4L);primo luogo, hai una nuova Builder<MyInterface>. La dichiarazione della classe implica quindi T = MyInterface. Secondo la tua dichiarazione di with, Fdeve essere un Function<T, R>, che è un Function<MyInterface, R>in questa situazione. Pertanto, il parametro getterdeve assumere un MyInterfaceparametro as (soddisfatto dai riferimenti al metodo MyInterface::getNumbere MyInterface::getLong) e restituire R, che deve essere dello stesso tipo del secondo parametro della funzione with. Ora vediamo se questo vale per tutti i tuoi casi:

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

Puoi "risolvere" questo problema con le seguenti opzioni:

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

Oltre a questo punto, è principalmente una decisione di progettazione per la quale opzione riduce la complessità del codice per la tua particolare applicazione, quindi scegli quello che più ti si addice.

Il motivo per cui non è possibile eseguire questa operazione senza il cast risiede nel seguito, dalle specifiche del linguaggio Java :

La conversione del pugilato considera le espressioni di un tipo primitivo come espressioni di un tipo di riferimento corrispondente. In particolare, le seguenti nove conversioni sono chiamate conversioni di inscatolamento :

  • Dal tipo booleano al tipo booleano
  • Dal tipo byte al tipo byte
  • Dal tipo corto al tipo corto
  • Dal carattere char al tipo Character
  • Dal tipo int al tipo intero
  • Dal tipo long al tipo long
  • Dal tipo float al tipo float
  • Dal tipo doppio al tipo doppio
  • Dal tipo null al tipo null

Come puoi vedere chiaramente, non esiste una conversione di boxe implicita da long a Number e la conversione allargata da Long a Number può avvenire solo quando il compilatore è sicuro che richiede un numero e non un long. Poiché esiste un conflitto tra il riferimento al metodo che richiede un Numero e il 4L che fornisce un Long, il compilatore (per qualche motivo ???) non è in grado di fare il salto logico che Long è un numero e dedurre che Fè a Function<MyInterface, Number>.

Invece, sono riuscito a risolvere il problema modificando leggermente la firma della funzione:

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

Dopo questa modifica, si verifica quanto segue:

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

Modifica:
dopo aver trascorso un po 'più di tempo su di esso, è fastidiosamente difficile applicare la sicurezza di tipo getter-based. Ecco un esempio funzionante che utilizza i metodi setter per imporre la sicurezza del tipo di un costruttore:

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

Fornita la possibilità di costruire un oggetto in modo sicuro, speriamo che ad un certo punto in futuro saremo in grado di restituire un oggetto dati immutabile dal builder (magari aggiungendo un toRecord()metodo all'interfaccia e specificando il builder come a Builder<IntermediaryInterfaceType, RecordType>), quindi non devi nemmeno preoccuparti che l'oggetto risultante venga modificato. Onestamente, è un vero peccato che richiede così tanti sforzi per ottenere un costruttore flessibile e sicuro, ma è probabilmente impossibile senza alcune nuove funzionalità, generazione di codice o una fastidiosa riflessione.


Grazie per tutto il tuo lavoro, ma non vedo alcun miglioramento nell'evitare il typecast manuale. La mia ingenua comprensione è che un compilatore dovrebbe essere in grado di dedurre (cioè il typecast) tutto ciò che un umano può.
jukzi,

@jukzi La mia comprensione ingenua è la stessa, ma per qualsiasi motivo non funziona in questo modo. Ho escogitato una soluzione che praticamente raggiunge l'effetto desiderato, però
Avi

Grazie ancora. Ma la tua nuova proposta è troppo ampia (vedi stackoverflow.com/questions/58337639 ) poiché consente la compilazione di ".with (MyInterface :: getNumber," I AM NOT A NUMBER ")";
jukzi,

la tua frase "Il compilatore non può inferire il tipo di metodo di riferimento e 4L contemporaneamente" è interessante. Ma voglio solo il contrario. il compilatore dovrebbe provare Numero in base al primo parametro ed eseguire un ampliamento a Numero del secondo parametro.
jukzi,

1
WHAAAAAAAAAAAT? Perché BiConsumer funziona come previsto mentre Function non funziona? Non ho idea. Ammetto che questa è esattamente la sicurezza tipica che volevo, ma sfortunatamente non funziona con i getter. PERCHÉ PERCHÉ PERCHÉ.
jukzi,

1

Sembra che il compilatore abbia usato il valore 4L per decidere che R è Long e getNumber () restituisce un numero, che non è necessariamente un long.

Ma non sono sicuro del perché il valore abbia la precedenza sul metodo ...


0

Il compilatore Java non è in generale bravo a dedurre tipi generici / jolly multipli / nidificati. Spesso non riesco a ottenere qualcosa da compilare senza utilizzare una funzione di supporto per acquisire o dedurre alcuni dei tipi.

Ma hai davvero bisogno di catturare il tipo esatto di Functionas F? In caso contrario, forse i seguenti lavori, e come puoi vedere, sembrano funzionare anche con i sottotipi di 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");

    }
}

"with (MyInterface :: getNumber," NOT A NUMBER ")" non dovrebbe essere compilato
jukzi

0

La parte più interessante sta nella differenza tra quelle 2 linee, penso:

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

Nel primo caso, Tè esplicitamente Number, quindi 4Lè anche un Number, nessun problema. Nel secondo caso, 4Lè a Long, quindi Tè a Long, quindi la tua funzione non è compatibile e Java non può sapere se intendevi Numbero Long.


0

Con la seguente firma:

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

tutti i tuoi esempi vengono compilati, tranne il terzo, che richiede esplicitamente che il metodo abbia due variabili di tipo.

Il motivo per cui la tua versione non funziona è perché i riferimenti ai metodi Java non hanno un tipo specifico. Al contrario, hanno il tipo richiesto in un determinato contesto. Nel tuo caso, Rsi suppone che sia a Longcausa di 4L, ma il getter non può avere il tipo Function<MyInterface,Long>perché in Java, i tipi generici sono invarianti nei loro argomenti.


Il tuo codice verrà compilato with( getNumber,"NO NUMBER")che non è desiderato. Inoltre, non è vero che i generici siano sempre invarianti (vedi stackoverflow.com/a/58378661/9549750 per provare che i generici dei setter si comportano diversamente da quelli dei getter)
jukzi,

@jukzi Ah, la mia soluzione era già stata proposta da Avi. Peccato... :-). A proposito, è vero che possiamo assegnare Thing<Cat>a una Thing<? extends Animal>variabile, ma per la vera covarianza mi aspetterei che a Thing<Cat>possa essere assegnato a Thing<Animal>. Altre lingue, come Kotlin, consentono di definire variabili di tipo co-e controvarianti.
Hoopje,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.