Je suis un peu confus sur la façon dont les génériques Java gèrent l'héritage / polymorphisme.
Supposons la hiérarchie suivante -
Animal (parent)
Chien - Chat (Enfants)
Supposons donc que j'ai une méthode doSomething(List<Animal> animals)
. D'après toutes les règles d'héritage et de polymorphisme, je suppose que a List<Dog>
est a List<Animal>
et a List<Cat>
est a List<Animal>
- et que l'une ou l'autre pourrait être transmise à cette méthode. Mais non. Si je veux obtenir ce comportement, je dois explicitement dire à la méthode d'accepter une liste de n'importe quelle sous-classe d'Animal en disant doSomething(List<? extends Animal> animals)
.
Je comprends que c'est le comportement de Java. Ma question est pourquoi ? Pourquoi le polymorphisme est-il généralement implicite, mais lorsqu'il s'agit de génériques, il doit être spécifié?
la source
Réponses:
Non, un
List<Dog>
n'est pas unList<Animal>
. Considérez ce que vous pouvez faire avec unList<Animal>
- vous pouvez y ajouter n'importe quel animal ... y compris un chat. Maintenant, pouvez-vous logiquement ajouter un chat à une portée de chiots? Absolument pas.Soudain, vous avez un chat très confus.
Maintenant, vous ne pouvez pas ajouter un
Cat
à unList<? extends Animal>
parce que vous ne savez pas que c'est unList<Cat>
. Vous pouvez récupérer une valeur et savoir que ce sera unAnimal
, mais vous ne pouvez pas ajouter d'animaux arbitraires. L'inverse est vrai pourList<? super Animal>
- dans ce cas, vous pouvez y ajouter unAnimal
en toute sécurité, mais vous ne savez rien de ce qui pourrait être récupéré, car il pourrait s'agir d'unList<Object>
.la source
Ce que vous recherchez s'appelle des paramètres de type covariant . Cela signifie que si un type d'objet peut être remplacé par un autre dans une méthode (par exemple, il
Animal
peut être remplacé parDog
), il en va de même pour les expressions utilisant ces objets (ilList<Animal>
pourrait donc être remplacé parList<Dog>
). Le problème est que la covariance n'est pas sûre pour les listes mutables en général. Supposons que vous en ayez unList<Dog>
et qu'il soit utilisé comme unList<Animal>
. Que se passe-t-il lorsque vous essayez d'ajouter un chat à ceList<Animal>
qui est vraiment unList<Dog>
? Autoriser automatiquement la covariance des paramètres de type rompt le système de type.Il serait utile d'ajouter une syntaxe pour permettre aux paramètres de type d'être spécifiés comme covariants, ce qui évite les
? extends Foo
déclarations de méthode in, mais cela ajoute une complexité supplémentaire.la source
La raison pour laquelle un
List<Dog>
n'est pas unList<Animal>
est que, par exemple, vous pouvez insérer unCat
dans unList<Animal>
, mais pas dans unList<Dog>
... vous pouvez utiliser des caractères génériques pour rendre les génériques plus extensibles lorsque cela est possible; par exemple, la lecture d'un aList<Dog>
est similaire à la lecture d'unList<Animal>
- mais pas l'écriture.Les génériques dans le langage Java et la section sur les génériques des didacticiels Java ont une très bonne explication approfondie pour expliquer pourquoi certaines choses sont ou ne sont pas polymorphes ou autorisées avec les génériques.
la source
Je pense qu'il convient d'ajouter à ce que mentionnent les autres réponses,
il est également vrai que
La façon dont fonctionne l'intuition du PO - qui est tout à fait valable bien sûr - est la dernière phrase. Cependant, si nous appliquons cette intuition, nous obtenons un langage qui n'est pas Java-esque dans son système de types: supposons que notre langage permette d'ajouter un chat à notre liste de chiens. Qu'est-ce que cela signifierait? Cela signifierait que la liste cesse d'être une liste de chiens et reste simplement une liste d'animaux. Et une liste de mammifères, et une liste de quadrupèdes.
En d'autres termes: A
List<Dog>
en Java ne signifie pas "une liste de chiens" en anglais, cela signifie "une liste qui peut avoir des chiens, et rien d'autre".Plus généralement, l'intuition d'OP se prête à un langage dans lequel les opérations sur les objets peuvent changer de type , ou plutôt, le (s) type (s) d'un objet est une fonction (dynamique) de sa valeur.
la source
Je dirais que l'intérêt de Generics est qu'il ne permet pas cela. Considérez la situation avec les tableaux, qui permettent ce type de covariance:
Ce code se compile bien, mais génère une erreur d'exécution (
java.lang.ArrayStoreException: java.lang.Boolean
sur la deuxième ligne). Ce n'est pas sûr. Le but de Generics est d'ajouter la sécurité du type de temps de compilation, sinon vous pourriez simplement vous en tenir à une classe ordinaire sans génériques.Il y a maintenant des moments où vous devez être plus flexible et c'est à cela que servent les
? super Class
et? extends Class
. Le premier est lorsque vous devez insérer dans un typeCollection
(par exemple), et le second est lorsque vous devez lire à partir de celui-ci, d'une manière sûre. Mais la seule façon de faire les deux en même temps est d'avoir un type spécifique.la source
Pour comprendre le problème, il est utile de faire une comparaison avec les tableaux.
List<Dog>
n'est pas une sous-classe deList<Animal>
.Mais
Dog[]
est une sous - classe deAnimal[]
.Les tableaux sont réifiables et covariants .
Reifiable signifie que leurs informations de type sont entièrement disponibles au moment de l'exécution.
Par conséquent, les tableaux offrent une sécurité de type à l'exécution, mais pas une sécurité de type à la compilation.
C'est l'inverse pour les génériques: les
génériques sont effacés et invariants .
Par conséquent, les génériques ne peuvent pas assurer la sécurité de type à l'exécution, mais ils offrent une sécurité de type à la compilation.
Dans le code ci-dessous, si les génériques étaient covariants, il sera possible de faire de la pollution en tas à la ligne 3.
la source
Les réponses données ici ne m'ont pas complètement convaincu. Au lieu de cela, je fais un autre exemple.
sonne bien, non? Mais vous ne pouvez passer que
Consumer
s etSupplier
s pourAnimal
s. Si vous avez unMammal
consommateur, mais unDuck
fournisseur, ils ne devraient pas convenir bien que les deux soient des animaux. Afin de refuser cela, des restrictions supplémentaires ont été ajoutées.Au lieu de ce qui précède, nous devons définir des relations entre les types que nous utilisons.
Par exemple.,
s'assure que nous ne pouvons utiliser qu'un fournisseur qui nous fournit le bon type d'objet pour le consommateur.
OTOH, on pourrait aussi bien faire
où nous allons dans l'autre sens: nous définissons le type du
Supplier
et limitons qu'il peut être mis dans leConsumer
.On peut même faire
où, ayant les relations intuitives
Life
->Animal
->Mammal
->Dog
,Cat
etc., nous pourrions même mettre unMammal
dans unLife
consommateur, mais pas unString
dans unLife
consommateur.la source
(Consumer<Runnable>, Supplier<Dog>)
whileDog
est un sous-type deAnimal & Runnable
La logique de base d'un tel comportement est de
Generics
suivre un mécanisme d'effacement de type. Donc, au moment de l'exécution, vous n'avez aucun moyen d'identifier le type decollection
contrairement àarrays
où il n'y a pas un tel processus d'effacement. Revenons donc à votre question ...Supposons donc qu'il existe une méthode donnée ci-dessous:
Maintenant, si java permet à l'appelant d'ajouter la liste de type Animal à cette méthode, vous pouvez ajouter une erreur dans la collection et au moment de l'exécution, elle s'exécutera également en raison de l'effacement du type. Alors qu'en cas de tableaux, vous obtiendrez une exception d'exécution pour de tels scénarios ...
Ainsi, en substance, ce comportement est implémenté de sorte que l'on ne puisse pas ajouter de mauvaises choses à la collection. Maintenant, je crois que l'effacement de type existe pour donner une compatibilité avec java hérité sans génériques ....
la source
Le sous-typage est invariant pour les types paramétrés. Même difficile, la classe
Dog
est un sous-type deAnimal
, le type paramétréList<Dog>
n'est pas un sous-type deList<Animal>
. En revanche, le sous-typage covariant est utilisé par les tableaux, donc le type de tableauDog[]
est un sous-type deAnimal[]
.Le sous-typage invariant garantit que les contraintes de type imposées par Java ne sont pas violées. Considérez le code suivant donné par @Jon Skeet:
Comme indiqué par @ Jon Skeet, ce code est illégal, car sinon il violerait les contraintes de type en renvoyant un chat quand un chien s'y attendait.
Il est instructif de comparer ce qui précède au code analogue pour les tableaux.
Le code est légal. Cependant, lève une exception de magasin de tableaux . Un tableau transporte son type au moment de l'exécution de cette manière, la JVM peut appliquer la sécurité de type du sous-typage covariant.
Pour mieux comprendre cela, regardons le bytecode généré par
javap
la classe ci-dessous:En utilisant la commande
javap -c Demonstration
, cela montre le bytecode Java suivant:Observez que le code traduit des corps de méthode est identique. Le compilateur a remplacé chaque type paramétré par son effacement . Cette propriété est cruciale, ce qui signifie qu'elle n'a pas rompu la compatibilité descendante.
En conclusion, la sécurité d'exécution n'est pas possible pour les types paramétrés, car le compilateur remplace chaque type paramétré par son effacement. Cela fait que les types paramétrés ne sont rien d'autre que du sucre syntaxique.
la source
En fait, vous pouvez utiliser une interface pour réaliser ce que vous voulez.
}
vous pouvez ensuite utiliser les collections en utilisant
la source
Si vous êtes sûr que les éléments de la liste sont des sous-classes de ce super type donné, vous pouvez convertir la liste en utilisant cette approche:
C'est utile lorsque vous voulez passer la liste dans un constructeur ou itérer dessus
la source
La réponse ainsi que d'autres réponses sont correctes. Je vais ajouter à ces réponses une solution qui, je pense, sera utile. Je pense que cela revient souvent dans la programmation. Une chose à noter est que pour les collections (listes, ensembles, etc.), le problème principal est l'ajout à la collection. C'est là que les choses s'effondrent. Même la suppression est OK.
Dans la plupart des cas, nous pouvons utiliser
Collection<? extends T>
plutôt alorsCollection<T>
et cela devrait être le premier choix. Cependant, je trouve des cas où ce n'est pas facile à faire. Il appartient au débat de savoir si c'est toujours la meilleure chose à faire. Je présente ici une classe DownCastCollection qui peut prendre convertir unCollection<? extends T>
en unCollection<T>
(nous pouvons définir des classes similaires pour List, Set, NavigableSet, ..) à utiliser lorsque l'utilisation de l'approche standard est très gênante. Ci-dessous est un exemple de la façon de l'utiliser (nous pourrions également utiliserCollection<? extends Object>
dans ce cas, mais je reste simple pour illustrer l'utilisation de DownCastCollection.Maintenant la classe:
}
la source
Collections.unmodifiableCollection
Collection<? extends E>
gère déjà ce comportement correctement, à moins que vous ne l'utilisiez d'une manière qui ne soit pas sûre pour le type (par exemple en le convertissant en autre chose). Le seul avantage que je vois est que, lorsque vous appelez l'add
opération, elle lève une exception même si vous l'avez lancée.Prenons l'exemple du tutoriel JavaSE
Alors pourquoi une liste de chiens (cercles) ne doit pas être considérée implicitement comme une liste d'animaux (formes), c'est à cause de cette situation:
Les "architectes" Java disposaient donc de 2 options pour résoudre ce problème:
ne considérez pas qu'un sous-type est implicitement son super-type et donnez une erreur de compilation, comme cela se produit maintenant
considérez le sous-type comme étant son supertype et limitez lors de la compilation la méthode "add" (donc dans la méthode drawAll, si une liste de cercles, sous-type de forme, est transmise, le compilateur devrait le détecter et vous limiter avec une erreur de compilation à faire cette).
Pour des raisons évidentes, cela a choisi la première voie.
la source
Nous devons également prendre en compte la façon dont le compilateur menace les classes génériques: dans "instancie" un type différent chaque fois que nous remplissons les arguments génériques.
Ainsi , nous avons
ListOfAnimal
,ListOfDog
,ListOfCat
, etc., qui sont des classes distinctes qui finissent par être « créé » par le compilateur lorsque nous précisons les arguments génériques. Et c'est une hiérarchie plate (en fait, ceList
n'est pas du tout une hiérarchie).Un autre argument pour lequel la covariance n'a pas de sens dans le cas des classes génériques est le fait qu'à la base toutes les classes sont les mêmes - sont des
List
instances. La spécialisation d'unList
en remplissant l'argument générique ne prolonge pas la classe, elle le fait simplement fonctionner pour cet argument générique particulier.la source
Le problème a été bien identifié. Mais il y a une solution; rendre doSomething générique:
vous pouvez maintenant appeler doSomething avec List <Dog> ou List <Cat> ou List <Animal>.
la source
une autre solution est de construire une nouvelle liste
la source
Suite à la réponse de Jon Skeet, qui utilise cet exemple de code:
Au niveau le plus profond, le problème ici est cela
dogs
etanimals
partage une référence. Cela signifie qu'une façon de faire ce travail serait de copier la liste entière, ce qui romprait l'égalité de référence:Après avoir appelé
List<Animal> animals = new ArrayList<>(dogs);
, vous ne pouvez pas ensuite attribuer directementanimals
à une ou l' autredogs
oucats
:par conséquent, vous ne pouvez pas mettre le mauvais sous-type de
Animal
dans la liste, car il n'y a pas de sous-type incorrect - tout objet de sous-type? extends Animal
peut être ajoutéanimals
.Évidemment, cela change la sémantique, car les listes
animals
etdogs
ne sont plus partagées, donc l'ajout à une liste ne s'ajoute pas à l'autre (ce qui est exactement ce que vous voulez, pour éviter le problème qu'unCat
pourrait être ajouté à une liste qui n'est que censé contenir desDog
objets). De plus, la copie de la liste entière peut être inefficace. Cependant, cela résout le problème d'équivalence de type, en brisant l'égalité de référence.la source