Perché un parametro di tipo è più forte di un parametro di metodo


12

Perché è

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

più severo allora

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

Questo è un follow-up su Perché il tipo di ritorno lambda non viene verificato al momento della compilazione . Ho trovato usando il metodo withX()come

.withX(MyInterface::getLength, "I am not a Long")

produce l'errore di tempo di compilazione desiderato:

Il tipo di getLength () dal tipo BuilderExample.MyInterface è lungo, questo è incompatibile con il tipo restituito dal descrittore: String

mentre usando il metodo with()no.

esempio completo:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

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

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

Esempio esteso

L'esempio seguente mostra il diverso comportamento del metodo e del parametro di tipo ridotto a un fornitore. Inoltre, mostra la differenza rispetto a un comportamento del consumatore per un parametro di tipo. E mostra che non fa differenza se è un consumatore o un fornitore per un parametro del metodo.

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}

1
A causa dell'inferenza con quest'ultimo. Sebbene entrambi siano basati sul caso d'uso, è necessario implementarlo. Per il tuo, il primo potrebbe essere rigoroso e buono. Per flessibilità, qualcun altro può preferire quest'ultimo.
Naman,

Stai tentando di compilare questo in Eclipse? La ricerca di stringhe di errore nel formato incollato suggerisce che si tratta di un errore specifico di Eclipse (ecj). Si riscontra lo stesso problema durante la compilazione con raw javaco uno strumento di compilazione come Gradle o Maven?
user31601,

@ user31601 ho aggiunto un esempio completo con l'output javac. I messaggi di errore sono leggermente diversi ma con eclissi e javac mantengono lo stesso comportamento
jukzi

Risposte:


12

Questa è una domanda davvero interessante. La risposta, temo, è complicata.

tl; dr

Risolvere la differenza implica una lettura abbastanza approfondita della specifica di inferenza del tipo di Java , ma sostanzialmente si riduce a questo:

  • A parità di altre condizioni, il compilatore deduce il tipo più specifico possibile.
  • Tuttavia, se riesce a trovare una sostituzione per un parametro di tipo che soddisfa tutti i requisiti, la compilazione avrà esito positivo, per quanto vaga la sostituzione risulti essere.
  • Per withc'è una (certamente vago) sostituzione che soddisfa tutti i requisiti di R:Serializable
  • Infatti withX, l'introduzione del parametro di tipo aggiuntivo Fforza la risoluzione del compilatore per Rprimo, senza considerare il vincolo F extends Function<T,R>. Rsi risolve nel (molto più specifico) Stringche significa quindi l'inferenza del Ffallimento.

Quest'ultimo punto è il più importante, ma anche il più ondulato della mano. Non riesco a pensare a un modo più conciso di formularlo, quindi se vuoi maggiori dettagli, ti suggerisco di leggere la spiegazione completa di seguito.

Questo comportamento è previsto?

Vado fuori su un arto qui, e dire di no .

Non sto suggerendo che ci sia un bug nelle specifiche, più che (nel caso di withX) i progettisti del linguaggio hanno alzato le mani e hanno detto "ci sono alcune situazioni in cui l'inferenza del tipo diventa troppo difficile, quindi falliremo" . Anche se il comportamento del compilatore rispetto a quello che withXsembra sembra essere ciò che desideri, considererei questo come un effetto collaterale accidentale delle specifiche attuali, piuttosto che una decisione di progettazione intesa positivamente.

Questo è importante perché informa la domanda. Dovrei fare affidamento su questo comportamento nella progettazione della mia applicazione? Direi che non dovresti, perché non puoi garantire che le versioni future della lingua continueranno a comportarsi in questo modo.

Sebbene sia vero che i progettisti del linguaggio si sforzano molto di non rompere le applicazioni esistenti quando aggiornano le loro specifiche / design / compilatore, il problema è che il comportamento su cui si desidera fare affidamento è quello in cui il compilatore attualmente fallisce (cioè non un'applicazione esistente ). Gli aggiornamenti di Langauge trasformano continuamente il codice non di compilazione in codice di compilazione. Ad esempio, il codice seguente può essere garantita non compilare in Java 7, ma sarebbe la compilazione in Java 8:

static Runnable x = () -> System.out.println();

Il tuo caso d'uso non è diverso.

Un altro motivo per cui sarei cauto nell'usare il tuo withXmetodo è il Fparametro stesso. Generalmente, esiste un parametro di tipo generico su un metodo (che non appare nel tipo restituito) per associare insieme i tipi di più parti della firma. Sta dicendo:

Non mi importa cosa Tsia, ma voglio essere sicuro che ovunque io usi Tsia dello stesso tipo.

Logicamente, quindi, ci aspetteremmo che ogni parametro di tipo appaia almeno due volte nella firma di un metodo, altrimenti "non sta facendo nulla". Fnel tuo withXappare solo una volta nella firma, il che mi suggerisce un uso di un parametro di tipo non in linea con l' intento di questa caratteristica del linguaggio.

Un'implementazione alternativa

Un modo per implementarlo in modo leggermente più "intenzionale" sarebbe quello di suddividere il withmetodo in una catena di 2:

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // TODO: Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

Questo può quindi essere usato come segue:

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

Questo non include un parametro di tipo estraneo come il tuo withX. Suddividendo il metodo in due firme, esprime anche meglio l'intento di ciò che stai cercando di fare, dal punto di vista della sicurezza del tipo:

  • Il primo metodo imposta una classe ( With) che definisce il tipo in base al riferimento del metodo.
  • Il metodo scond ( of) vincola il tipo di valueessere compatibile con ciò che hai impostato in precedenza.

L'unico modo in cui una versione futura del linguaggio sarebbe in grado di compilarlo è se l'implementazione completa della tipizzazione anatra, che sembra improbabile.

Un'ultima nota per rendere irrilevante l'intera faccenda: penso che Mockito (e in particolare la sua funzionalità di stub) potrebbe sostanzialmente già fare quello che stai cercando di ottenere con il tuo "costruttore generico sicuro". Forse potresti semplicemente usarlo invece?

La spiegazione completa (ish)

Esaminerò la procedura di inferenza del tipo per entrambi withe withX. Questo è piuttosto lungo, quindi prendilo lentamente. Nonostante sia lungo, ho ancora lasciato fuori molti dettagli. Potresti voler fare riferimento alla specifica per maggiori dettagli (segui i link) per convincerti che ho ragione (potrei aver fatto un errore).

Inoltre, per semplificare un po 'le cose, userò un esempio di codice più minimale. La differenza principale è che scambia fuori Functionper Supplier, quindi ci sono meno tipi e parametri in gioco. Ecco uno snippet completo che riproduce il comportamento che hai descritto:

public class TypeInference {

    static long getLong() { return 1L; }

    static <R> void with(Supplier<R> supplier, R value) {}
    static <R, F extends Supplier<R>> void withX(F supplier, R value) {}

    public static void main(String[] args) {
        with(TypeInference::getLong, "Not a long");       // Compiles
        withX(TypeInference::getLong, "Also not a long"); // Does not compile
    }

}

Esaminiamo l' inferenza dell'applicabilità del tipo e la procedura dell'inferenza del tipo per ogni invocazione del metodo a turno:

with

Abbiamo:

with(TypeInference::getLong, "Not a long");

Il set di limiti iniziale, B 0 , è:

  • R <: Object

Tutte le espressioni dei parametri sono pertinenti all'applicabilità .

Quindi, il vincolo iniziale impostato per l' inferenza di applicabilità , C , è:

  • TypeInference::getLong è compatibile con Supplier<R>
  • "Not a long" è compatibile con R

Ciò si riduce all'insieme associato B 2 di:

  • R <: Object(da B 0 )
  • Long <: R (dal primo vincolo)
  • String <: R (dal secondo vincolo)

Poiché questo non contiene il limite " falso " e (presumo) la risoluzione dei successi R(dare Serializable), allora l'invocazione è applicabile.

Quindi, passiamo all'inferenza del tipo di invocazione .

Il nuovo set di vincoli, C , con variabili di input e output associate , è:

  • TypeInference::getLong è compatibile con Supplier<R>
    • Variabili di input: nessuna
    • Variabili di output: R

Questo non contiene interdipendenze tra le variabili di input e output , quindi può essere ridotto in un singolo passaggio e il set di limiti finale, B 4 , è uguale a B 2 . Quindi, la risoluzione ha successo come prima, e il compilatore emette un sospiro di sollievo!

withX

Abbiamo:

withX(TypeInference::getLong, "Also not a long");

Il set di limiti iniziale, B 0 , è:

  • R <: Object
  • F <: Supplier<R>

Solo la seconda espressione di parametro è pertinente all'applicabilità . Il primo ( TypeInference::getLong) non lo è, perché soddisfa la seguente condizione:

Se mè un metodo generico e l'invocazione del metodo non fornisce argomenti di tipo esplicito, un'espressione lambda tipizzata in modo esplicito o un'espressione di riferimento del metodo esatta per cui il tipo di destinazione corrispondente (derivato dalla firma di m) è un parametro di tipo di m.

Quindi, il vincolo iniziale impostato per l' inferenza di applicabilità , C , è:

  • "Also not a long" è compatibile con R

Ciò si riduce all'insieme associato B 2 di:

  • R <: Object(da B 0 )
  • F <: Supplier<R>(da B 0 )
  • String <: R (dal vincolo)

Ancora una volta, poiché questo non contiene il limite " falso " e la risoluzione dei successi R(dare String), allora l'invocazione è applicabile.

Inferenza del tipo di invito ancora una volta ...

Questa volta, il nuovo set di vincoli, C , con variabili di input e output associate , è:

  • TypeInference::getLong è compatibile con F
    • Variabili di input: F
    • Variabili di output: nessuna

Ancora una volta, non abbiamo interdipendenze tra variabili di input e output . Tuttavia questa volta, non v'è una variabile di input ( F), quindi è necessario risolverlo prima di tentare la riduzione . Quindi, iniziamo con il nostro set associato B 2 .

  1. Determiniamo un sottoinsieme Vcome segue:

    Dato un insieme di variabili di inferenza da risolvere, Vsia l'unione di questo insieme e tutte le variabili dalle quali dipende la risoluzione di almeno una variabile in questo insieme.

    Dal secondo legato B 2 , la risoluzione di Fdipende da R, quindi V := {F, R}.

  2. Scegliamo un sottoinsieme di Vsecondo la regola:

    lascia che { α1, ..., αn }sia un sottoinsieme non vuoto di variabili non incluse in modo Vtale che i) per tuttii (1 ≤ i ≤ n) , seαi dipende dalla risoluzione di una variabile β, allora o βha un'istanza o c'è qualche jtale che β = αj; e ii) non esiste un sottoinsieme proprio non vuoto di { α1, ..., αn }con questa proprietà.

    L'unico sottoinsieme di Vciò soddisfa questa proprietà è {R}.

  3. Utilizzando il terzo limite (String <: R ) istanziamo R = Stringe incorporiamo questo nel nostro set associato. Rviene ora risolto e il secondo limite diventa effettivamente F <: Supplier<String>.

  4. Usando il secondo limite (rivisto), istanziamo F = Supplier<String>. Fè ora risolto.

Ora che Fè stato risolto, possiamo procedere con riduzione , usando il nuovo vincolo:

  1. TypeInference::getLong è compatibile con Supplier<String>
  2. ... riduce a Long è compatibile con String
  3. ... che si riduce a falso

... e otteniamo un errore del compilatore!


Note aggiuntive sull '"Esempio esteso"

L' esempio esteso nella domanda esamina alcuni casi interessanti che non sono direttamente coperti dal funzionamento sopra:

  • Dove il tipo di valore è un sottotipo del tipo restituito dal metodo (Integer <: Number )
  • Dove l'interfaccia funzionale è contraddittoria nel tipo inferito (cioè Consumerpiuttosto che Supplier)

In particolare, 3 delle invocazioni indicate si distinguono come potenzialmente suggerendo un comportamento del compilatore "diverso" rispetto a quello descritto nelle spiegazioni:

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

Il secondo di questi 3 attraverserà esattamente lo stesso processo di inferenza di withX sopra (basta sostituire Longcon Numbere Stringcon Integer). Questo illustra ancora un altro motivo per cui non si dovrebbe fare affidamento su questo comportamento inferenza di tipo non riuscito per il vostro disegno di classe, come il fallimento di compilare qui è probabile che non è un comportamento desiderabile.

Per gli altri 2 (e in effetti una qualsiasi delle altre invocazioni che coinvolgono una persona Consumerche desideri elaborare), il comportamento dovrebbe essere evidente se lavori attraverso la procedura di inferenza del tipo stabilita per uno dei metodi sopra (ovvero withper la prima, withXper la terzo). C'è solo una piccola modifica che devi prendere in considerazione:

  • Il vincolo sul primo parametro ( t::setNumber è compatibile con Consumer<R> ) si ridurrà a R <: Numberanziché Number <: Rcome fa per Supplier<R>. Questo è descritto nella documentazione collegata sulla riduzione.

Lascio che sia un esercizio per il lettore lavorare con cura attraverso una delle procedure di cui sopra, armato con questo pezzo di conoscenza aggiuntiva, per dimostrare a se stessi esattamente perché una particolare invocazione compila o non si compila.


Molto approfondito, ben studiato e formulato. Grazie!
Zabuzard,

@ user31601 Puoi indicare dove entra in gioco la differenza tra fornitore e consumatore. Ho aggiunto un esempio esteso nella domanda originale per questo. Mostra il comportamento covariante, contravariante e invariante per le diverse versioni di letBe (), letBeX () e let (). Be () a seconda del fornitore / consumatore.
jukzi,

@jukzi Ho aggiunto alcune note aggiuntive, ma dovresti avere abbastanza informazioni per elaborare tu stesso questi nuovi esempi.
user31601

Questo è interessante: tanti casi speciali nel 18.2.1. per lambda e riferimenti a metodi in cui non mi sarei aspettato nessun caso speciale per loro dalla mia comprensione ingenua. E probabilmente nessuno sviluppatore normale si aspetterebbe.
jukzi,

Bene, immagino che il motivo sia che con lambda e riferimenti ai metodi, il compilatore deve decidere quale tipo corretto deve essere implementato dal lambda - deve fare una scelta! Ad esempio, TypeInference::getLongpotrebbe implementare Supplier<Long>o Supplier<Serializable>o Supplier<Number>etc, ma soprattutto può implementarne solo uno (proprio come qualsiasi altra classe)! Questo è diverso da tutte le altre espressioni, in cui i tipi implementati sono tutti noti in anticipo e il compilatore deve solo capire se uno di essi soddisfa i requisiti del vincolo.
user31601
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.