2016-07-07 1 views
10

j'ai deux interfaces qui ressemblent à ceci:covariance Java de type automatique de retour avec subclassing générique

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

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

Si j'ai un objet Parent brut, appelant foo() par défaut de retourner un Number car il n'y a aucun paramètre de type.

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

Cela a du sens.

Si j'ai un objet Child brut, je m'attendrais à ce que l'appel foo() retournerait un Integer par la même logique. Cependant, le compilateur prétend qu'il renvoie un Number.

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

Je peux passer outre Parent.foo() dans Child pour résoudre ce problème, comme ceci:

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

Pourquoi cela? Y at-il un moyen d'avoir Child.foo() par défaut pour retourner un Integer sans déroger Parent.foo()?

EDIT: Le pré-classement Integer n'est pas définitif. Je viens de choisir Number et Integer comme exemples, mais de toute évidence, ils n'étaient pas le meilleur choix. : S

+3

Je pense que c'est parce que sans le remplacement de l'enfant, l'enfant hérite de la méthode 'foo' du parent, et, selon le parent,' T' est un 'Number'. Pour 'foo' dans le parent de retourner un' Integer', il aurait besoin de connaître les informations sur sa ou ses classe (s) enfant (s), ce qui rompt la hiérarchie ... –

+0

try 'interface Enfant extends Parent < T étend Integer> ' – fukanchik

+5

@fukanchik qui n'est même pas valide Java. –

Répondre

4
  1. Ceci est basé sur les idées de @AdamGent.
  2. Malheureusement, je ne parle pas assez JLS pour prouver le dessous de la spécification.

Imaginer public interface Parent<T extends Number> a été définie dans une unité de compilation différents - dans un fichier séparé Parent.java.

Ensuite, lors de la compilation Child et main, le compilateur verrait la méthode foo comme Number foo().Preuve:

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

impressions:

class java.lang.Number 

Cette sortie est raisonnable que java ne saisissez l'effacement et ne peut retenir T extends dans le résultat .class fichier , plus parce que la méthode foo() est uniquement définie dans Parent . Pour modifier le type de résultat dans le compilateur enfant, vous devez insérer une méthode stubInteger foo() dans le bytecode Child.class. C'est parce qu'il ne reste aucune information sur les types génériques après la compilation.

Maintenant, si vous modifiez votre enfant à être:

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

par exemple ajouter foo() dans le Child le compilateur va créer la copie de la méthode Child dans le fichier .class avec un prototype différent mais encore compatible Integer foo(). Maintenant, la sortie est:

class java.lang.Integer 

Ceci est source de confusion bien sûr, parce que les gens attendent « visibilité lexicale » au lieu de « visibilité bytecode ».

Alternative est quand compilateur compiler cette différemment dans deux cas: l'interface dans le même « champ lexical » où le compilateur peut voir le code source et de l'interface dans une unité de compilation différente lorsque le compilateur ne peut voir bytecode. Je ne pense pas que ce soit une bonne alternative.

+0

J'ai supprimé ma réponse en faveur de la vôtre. Je ne suis pas sûr de savoir pourquoi le mien a été voté (probablement parce que j'étais confus en pensant que le PO voulait savoir pourquoi Java a choisi de mettre en œuvre des génériques comme il le fait). Il aurait été utile si le downvoter venait d'ajouter un commentaire. Mais votre réponse et @Scottb sont meilleurs. –

1

Les T ne sont pas exactement identiques. Imaginez que les interfaces ont été définies comme ceci:

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

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

L'interface Child étend l'interface Parent, afin que nous puissions « remplacer » le paramètre de type formel T1 avec le paramètre de type « réel » que l'on puisse dire est "T2 extends Integer":

interface Parent<<T2 extends Integer> extends Number> 

Ceci n'est autorisé que parce que Integer est un sous-type de Number. Par conséquent, la signature de foo() dans l'interface Parent (après avoir été prolongée dans l'interface Child) est simplifiée:

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

En d'autres termes, la signature ne change pas. La méthode foo() telle que déclarée dans l'interface Parent continue à renvoyer Number comme type brut.

+2

La signature ne change pas mais il y a des langages qui vont faire des extensions génériques (c'est-à-dire copier/créer des méthodes dans les sous-classes). Il doit faire comment Java a implémenté des génériques. –

+1

Il y a certainement des particularités dans la façon dont les types génériques sont implémentés dans le langage Java (Java n'est pas unique dans ce cas), mais il est généralement possible d'anticiper et de travailler avec ceux-ci. – scottb

+1

Je suis d'accord mais je peux comprendre la confusion OP. –