2016-07-07 102 views
10

ho due interfacce che assomigliano a questo:Java Tipo di ritorno automatico di covarianza con sottoclassi generico

interface Parent<T extends Number> { 
    T foo(); 
} 

interface Child<T extends Integer> extends Parent<T> { 
} 

Se ho un Parent oggetto grezzo, chiamando foo() default di tornare un Number poiché non v'è alcun tipo di parametro.

Parent parent = getRawParent(); 
Number result = parent.foo(); // the compiler knows this returns a Number 

Questo ha senso.

Se si dispone di un oggetto Child grezzo, mi aspetto che chiamando foo() restituisca un Integer con la stessa logica. Tuttavia, il compilatore afferma che restituisce un valore Number.

Child child = getRawChild(); 
Integer result = child.foo(); // compiler error; foo() returns a Number, not an Integer 

posso ignorare Parent.foo() in Child per risolvere questo problema, in questo modo:

interface Child<T extends Integer> extends Parent<T> { 
    @Override 
    T foo(); // compiler would now default to returning an Integer 
} 

Perché accade questo? C'è un modo per avere il valore predefinito Child.foo() per la restituzione di Integer senza sovrascrivere Parent.foo()?

MODIFICA: Fingere Integer non è definitivo. Ho appena scelto Number e Integer come esempi, ma ovviamente non erano la scelta migliore. : S

+3

Penso che sia perché senza l'override del bambino, il figlio eredita il metodo 'foo' del genitore, e, secondo il genitore,' T' è un 'Numero'. Per 'foo' nel parent per restituire un' Integer', dovrebbe conoscere le informazioni sulle sue classi figlio, quale tipo di interruzione della gerarchia ... –

+0

try 'interfaccia Child estende Parent < T estende Intero> ' – fukanchik

+5

@fukanchik che non è nemmeno Java valido. –

risposta

4
  1. Questo è basato su idee di @AdamGent.
  2. Sfortunatamente non sono abbastanza fluente con JLS quanto basta per dimostrare quanto segue dalla specifica.

Immaginate public interface Parent<T extends Number> è stato definito in un diversa unità di compilazione - in un file separato Parent.java.

Quindi, durante la compilazione di Child e main, il compilatore vedrebbe il metodo foo come Number foo().Proof:

import java.lang.reflect.Method; 
interface Parent<T extends Number> { 
    T foo(); 
} 

interface Child<R extends Integer> extends Parent<R> { 
} 

public class Test { 
    public static void main(String[] args) throws Exception { 
     System.out.println(Child.class.getMethod("foo").getReturnType()); 
    } 
} 

stampe:

class java.lang.Number 

Questa uscita è ragionevole java fa cancellazione di tipo e non è in grado di trattenere T extends nel risultato .class file di inclusa perché il metodo foo() è definito solo in Parent . Per modificare il tipo di risultato nel compilatore figlio, è necessario inserire un codice stubInteger foo() nel bytecode Child.class. Questo perché non rimangono informazioni sui tipi generici dopo la compilazione.

Ora, se si modifica il bambino ad essere:

interface Child<R extends Integer> extends Parent<R> { 
    @Override R foo(); 
} 

esempio aggiungi il proprio foo() allo Child il compilatore creerà la copia del metodo nel .class con un prototipo diverso, ma ancora compatibile, Integer foo(). Ora output è:

class java.lang.Integer 

Questo è fonte di confusione, naturalmente, perché la gente si aspetta "visibilità lessicale" invece di "visibilità bytecode".

alternativa è quando compilatore compilare questo diverso in due casi: interfaccia nella stessa "scope lessicale" dove compilatore può vedere il codice sorgente e l'interfaccia in un'unità di compilazione differente quando compilatore può vedere solo bytecode. Non penso che questa sia una buona alternativa.

+0

Ho cancellato la mia risposta a favore della vostra. Non sono sicuro del motivo per cui il mio stava scendendo (probabilmente perché ero confuso nel pensare che l'OP volesse sapere perché Java ha scelto di implementare i generici come fa). Sarebbe stato utile se il downtoter avesse appena aggiunto un commento. Ma la tua risposta e @Scottb sono migliori. –

1

Gli T s non sono esattamente gli stessi. Immaginiamo che le interfacce sono state definite come questo, invece:

interface Parent<T1 extends Number> { 
    T1 foo(); 
} 

interface Child<T2 extends Integer> extends Parent<T2> { 
} 

L'interfaccia Child estende l'interfaccia Parent, così possiamo "sostituire" il parametro di tipo T1 formale con il "reale" parametro tipo che possiamo dire è "T2 extends Integer":

interface Parent<<T2 extends Integer> extends Number> 

questo è consentito solo perché Integer è un sottotipo di Numero. Pertanto, la firma foo() nell'interfaccia Parent (dopo essere esteso nell'interfaccia Child) è semplificata:

interface Parent<T2 extends Number> { 
    T2 foo(); 
} 

In altre parole, la firma non viene modificata. Il metodo foo() come dichiarato nell'interfaccia Parent continua a restituire Number come tipo non elaborato.

+2

La firma non cambia ma ci sono linguaggi che eseguiranno un'espansione generica (ovvero copierà/creerà metodi in sottoclassi). Deve fare come Java ha implementato i generici. –

+1

Ci sono sicuramente alcune peculiarità nel modo in cui i tipi generici sono implementati nel linguaggio Java (Java non è unico in questo), ma di solito è possibile anticipare e lavorare con questi. – scottb

+1

Sono d'accordo ma riesco a capire la confusione dei PO. –