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
with
c'è una (certamente vago) sostituzione che soddisfa tutti i requisiti di R
:Serializable
- Infatti
withX
, l'introduzione del parametro di tipo aggiuntivo F
forza la risoluzione del compilatore per R
primo, senza considerare il vincolo F extends Function<T,R>
. R
si risolve nel (molto più specifico) String
che significa quindi l'inferenza del F
fallimento.
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 withX
sembra 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 withX
metodo è il F
parametro 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 T
sia, ma voglio essere sicuro che ovunque io usi T
sia 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". F
nel tuo withX
appare 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 with
metodo 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 value
essere 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 with
e 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 Function
per 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 , è:
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 .
Determiniamo un sottoinsieme V
come segue:
Dato un insieme di variabili di inferenza da risolvere, V
sia 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 F
dipende da R
, quindi V := {F, R}
.
Scegliamo un sottoinsieme di V
secondo la regola:
lascia che { α1, ..., αn }
sia un sottoinsieme non vuoto di variabili non incluse in modo V
tale che i) per tuttii (1 ≤ i ≤ n)
, seαi
dipende dalla risoluzione di una variabile β
, allora o β
ha un'istanza o c'è qualche j
tale che β = αj
; e ii) non esiste un sottoinsieme proprio non vuoto di { α1, ..., αn }
con questa proprietà.
L'unico sottoinsieme di V
ciò soddisfa questa proprietà è {R}
.
Utilizzando il terzo limite (String <: R
) istanziamo R = String
e incorporiamo questo nel nostro set associato. R
viene ora risolto e il secondo limite diventa effettivamente F <: Supplier<String>
.
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:
TypeInference::getLong
è compatibile con Supplier<String>
- ... riduce a
Long
è compatibile con String
- ... 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è
Consumer
piuttosto 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 Long
con Number
e String
con 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 Consumer
che desideri elaborare), il comportamento dovrebbe essere evidente se lavori attraverso la procedura di inferenza del tipo stabilita per uno dei metodi sopra (ovvero with
per la prima, withX
per 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 <: Number
anziché Number <: R
come 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.