Le but de cet article est d'introduire des génériques Java, afin que tout le monde puisse avoir une compréhension finale, claire et précise de tous les aspects des génériques Java, et également jeter les bases du prochain article "Revenderstanding Java Reflection".
Introduction
Les génériques sont un point de connaissance très important à Java. Les génériques sont largement utilisés dans les cadres de collection Java. Dans cet article, nous examinerons la conception de Java Generics à partir de zéro, qui impliquera la transformation des jacques et l'effacement de type pénible.
Bases génériques
Classes génériques
Définissons d'abord une classe de boîte simple:
Box de classe publique {objet de chaîne privée; public void set (string object) {this.object = objet; } public String get () {return objet; }}C'est la pratique la plus courante. L'un des inconvénients de cela est que seuls les éléments de type chaîne peuvent être chargés dans la boîte. À l'avenir, si nous devons charger d'autres types d'éléments tels que Integer, nous devons également réécrire une autre boîte. Le code ne peut pas être réutilisé et l'utilisation des génériques peut bien résoudre ce problème.
Classe publique Box <T> {// t signifie "type" privé t t; public void set (t t) {this.t = t; } public t get () {return t; }}De cette façon, notre classe de boîte peut être réutilisée, et nous pouvons remplacer T par tout type que nous voulons:
Box <Integer> IntegerBox = new Box <Integer> (); Box <Deuble> doublebox = new Box <DWWED> (); Box <string> StringBox = new Box <string> ();
Méthodes génériques
Après avoir lu la classe générique, découvrons les méthodes génériques. Déclarer une méthode générique est simple, ajoutez simplement un formulaire similaire à <k, v> au type de retour:
classe publique util {public static <k, v> booléen compare (paire <k, v> p1, paire <k, v> p2) {return p1.getKey (). equals (p2.getKey ()) && p1.getValue (). equals (p2.GetValue ()); }} Paire de classe publique <k, v> {clé privée k; valeur en V privée; paire publique (key k, Vale V) {this.key = key; this.value = valeur; } public void setKey (k key) {this.key = key; } public void setValue (V valeur) {this.value = valeur; } public k getKey () {return key; } public v getValue () {return Value; }}Nous pouvons appeler des méthodes génériques comme celle-ci:
Paire <Integer, string> p1 = new paire <> (1, "Apple"); paire <nteger, string> p2 = new pair <> (2, "Pear"); boolean Samed = util. <Integer, string> compare (p1, p2);
Ou utilisez l'inférence de type dans Java 1.7 / 1.8 pour permettre à Java de déduire automatiquement les paramètres de type correspondant:
Paire <Integer, string> p1 = new pair <> (1, "Apple"); paire <nteger, string> p2 = new pair <> (2, "pear"); boolean Samen = util.compare (p1, p2);
Symbole limite
Maintenant, nous voulons implémenter une telle fonction pour trouver le nombre d'éléments dans un tableau générique qui sont plus grands qu'un élément spécifique. Nous pouvons l'implémenter comme ceci:
public static <t> int countGreaterthan (t [] anArray, t elem) {int county = 0; pour (t e: anArray) if (e> elem) // error compiler ++ count; Return Count;}Mais cela est évidemment faux, car à l'exception des types primitifs tels que court, int, double, long, flottant, octet, char, etc., d'autres classes peuvent ne pas nécessairement utiliser les opérateurs>, donc le compilateur rapporte une erreur. Comment résoudre ce problème? La réponse est d'utiliser le symbole limite.
interface publique comparable <T> {public int compareto (t o);}Faites une déclaration similaire à ce qui suit, ce qui équivaut à dire au compilateur que le paramètre Type T représente les classes qui implémentent l'interface comparable, ce qui équivaut à dire au compilateur qu'il implémente tous au moins la méthode compareto.
Le public statique <t étend comparable <T>> int countGreaterthan (t [] anArray, t elem) {int count = 0; pour (t e: anArray) if (e ..compareto (elem)> 0) ++ count; Return Count;}Magazine
Avant de comprendre les jilèges, nous devons d'abord clarifier un concept ou emprunter la classe de boîte que nous avons définie ci-dessus, supposons que nous ajoutions une méthode comme celle-ci:
public void boxtest (box <nombre> n) {/ * ... * /}Alors, quel type de paramètres Box <bumber> n permet-il d'accepter? Pouvons-nous passer dans Box <nouger> ou Box <Double>? La réponse est non. Bien que Integer et Double soient des sous-classes de nombre, il n'y a pas de relation entre Box <nEger> ou Box <Deuble> et Box <nom> dans les génériques. Ceci est très important, et nous utiliserons un exemple complet pour approfondir notre compréhension.
Tout d'abord, nous définissons quelques classes simples et nous les utiliserons ci-dessous:
Classe Fruit {} classe Apple étend les fruits {} Orange étend les fruits {}Dans l'exemple suivant, nous créons un lecteur de classe générique, puis dans F1 (), lorsque nous essayons Fruit F = fruitReader.readExact (pommes); Le compilateur rapportera une erreur car il n'y a pas de relation entre la liste <fruit> et la liste <mple>.
classe publique GenericReading {static List <Plate> pommes = arrays.aslist (new Apple ()); Liste statique <frure> fruit = arrays.aslist (new fruit ()); Lecteur de classe statique <T> {t readExact (list <T> list) {return list.get (0); }} statique void f1 () {lecteur <fruit> fruitReader = nouveau lecteur <fruit> (); // erreurs: la liste <frure> ne peut pas être appliquée à la liste <mechn>. // fruit f = fruitReader.readExact (pommes); } public static void main (String [] args) {f1 (); }}Mais selon nos habitudes de pensée habituelles, il doit y avoir un lien entre la pomme et les fruits, mais le compilateur ne peut pas le reconnaître. Alors, comment puis-je résoudre ce problème dans le code générique? Nous pouvons résoudre ce problème en utilisant des caractères génériques:
classe statique covariantreader <T> {t Readcovariant (list <? étend t> list) {return list.get (0); }} static void f2 () {covariantreader <fruit> fruitReader = new Covariantreader <fruit> (); Fruit f = fruitReader.readcovariant (fruit); Fruit a = fruitReader.readcovariant (pommes);} public static void main (String [] args) {f2 ();}Ceci est assez similaire à dire au compilateur que les paramètres acceptés par la méthode de la lecture duader FruitReder sont aussi longs que la sous-classe qui satisfait les fruits (y compris les fruits lui-même), de sorte que la relation entre la sous-classe et la classe parent est également associée.
Principes PECS
Nous avons vu une utilisation similaire à <? étend t> ci-dessus. En utilisant, nous pouvons obtenir des éléments de la liste, alors pouvons-nous ajouter des éléments dans la liste? Essayons-le:
classe publique générique et covariance {public static void main (String [] args) {// wildcards permettent la covariance: list <? étend les fruits> Flist = new ArrayList <Ple> (); // Erreur de compilation: ne peut ajouter aucun type d'objet: // flist.add (new Apple ()) // flist.add (new Orange ()) // flist.add (new fruit ()) // flist.add (new object ()) flist.add (null); // légal mais sans intérêt // Nous savons qu'il renvoie au moins des fruits: fruit f = flist.get (0); }}La réponse est non, le compilateur Java ne nous permet pas de le faire, pourquoi? Nous pourrions aussi bien considérer ce problème du point de vue du compilateur. Parce que la liste <? étend les fruits> Flist peut avoir de nombreuses significations:
Liste <? étend les fruits> flist = new ArrayList <fruit> (); list <? étend les fruits> flist = new ArrayList <Ple> (); list <? étend les fruits> flist = new ArrayList <Orange> ();
Par conséquent, pour les classes de collecte qui implémentent <? Étend T>, ils ne peuvent être considérés que comme un élément producteur (Get) à l'extérieur, et ne peuvent pas être utilisés comme consommateur pour obtenir (ajouter) des éléments à l'extérieur.
Que devons-nous faire si nous voulons ajouter l'élément? Vous pouvez utiliser <? Super T>:
classe publique GenericWriting {static List <Plate> Apples = new ArrayList <Plate> (); Liste statique <frure> fruit = new ArrayList <fruit> (); statique <T> void writeExact (list <T> list, t item) {list.add (item); } static void f1 () {writeExact (pommes, new Apple ()); writeExact (fruit, nouveau pomme ()); } statique <T> void writewithwildcard (list <? super t> list, t item) {list.add (item)} static void f2 () {writewithwildcard (pommes, new Apple ()); WriteWithWildCard (fruit, new Apple ()); } public static void main (String [] args) {f1 (); f2 (); }}De cette façon, nous pouvons ajouter des éléments au conteneur, mais l'inconvénient de l'utilisation de Super est que nous ne pouvons pas obtenir d'éléments dans le conteneur à l'avenir. La raison est très simple. Nous continuons à considérer cette question du point de vue du compilateur. Pour la liste <? Super Apple> Liste, il peut avoir les significations suivantes:
Liste <? Super Apple> list = new ArrayList <Ple> (); list <? Super Apple> list = new ArrayList <fruit> (); list <? super Apple> list = new ArrayList <Bject> ();
Lorsque nous essayons d'obtenir une liste de pomme, nous pouvons obtenir un fruit, qui peut être d'autres types de fruits tels que l'orange.
Sur la base de l'exemple ci-dessus, nous pouvons résumer une règle, "producteur s'étend, consommateur super":
Après avoir lu le code source des collections Java, nous pouvons constater que nous utilisons généralement les deux ensemble, comme les suivants:
Collections de classe publiques {public static <t> void copy (list <? Super t> dest, list <? étend t> src) {for (int i = 0; i <src.size (); i ++) dest.set (i, src.get (i)); }}Type effacer
La chose la plus pénible de Java Generics est peut-être l'effacement du type, en particulier pour les programmeurs ayant une expérience C ++. L'effacement de type signifie que les génériques Java ne peuvent être utilisés que pour la vérification de type statique pendant la compilation, puis le code généré par le compilateur effacera les informations de type correspondantes. De cette façon, pendant la course, le JVM connaît en fait le type spécifique représenté par le générique. Le but de cela est dû au fait que les génériques Java ont été introduits après 1,5. Afin de maintenir la compatibilité descendante, vous ne pouvez faire que l'effacement de type pour être compatible avec le code non générique précédent. Pour ce point, si vous lisez le code source du framework de la collection Java, vous pouvez constater que certaines classes ne prennent pas en charge les génériques.
Cela dit, que signifie l'effacement générique? Regardons d'abord l'exemple simple suivant:
Node de classe publique <T> {Données P privés; Node privé <T> Suivant; Node public (T data, nœud <t> Suivant)} this.data = data; this.next = suivant; } public t getData () {return data; } // ...}Une fois le compilateur terminé la vérification de type correspondant, le code ci-dessus sera en fait converti en:
Node de classe publique {données d'objets privés; Node privé Suivant; Node public (données d'objet, nœud suivant) {this.data = data; this.next = suivant; } Objet public getData () {return data; } // ...}Cela signifie que peu importe que nous déclarons le nœud <string> ou le nœud <nteger>, le JVM est tout considéré comme le nœud <objet> pendant l'exécution. Existe-t-il un moyen de résoudre ce problème? Cela nous oblige à réinitialiser les limites nous-mêmes et à modifier le code ci-dessus à ce qui suit:
Le nœud de classe publique <T étend comparable <T>> {données privées; Node privé <T> Suivant; Node public (T data, nœud <t> suivant) {this.data = data; this.next = suivant; } public t getData () {return data; } // ...}De cette façon, le compilateur remplacera l'endroit où T apparaît par comparable au lieu de l'objet par défaut:
Node de classe publique {données comparables privées; Node privé Suivant; Node public (données comparables, nœud suivant) {this.data = data; this.next = suivant; } public comparable getData () {return data; } // ...}Le concept ci-dessus peut être plus facile à comprendre, mais en fait, l'effacement générique apporte beaucoup plus de problèmes. Ensuite, jetons un aperçu systématique de certains des problèmes abordés par l'effacement de type. Certains problèmes peuvent ne pas être rencontrés dans les génériques C ++, mais vous devez être très prudent en Java.
Question 1
Les tableaux génériques ne sont pas autorisés en Java. Si le compilateur fait quelque chose comme les suivants, il rapportera une erreur:
List <Integer> [] arrayoflists = new List <Integer> [2]; // Erreur de compilation
Pourquoi le compilateur ne soutient-il pas la pratique ci-dessus? Continuez à utiliser la pensée inverse, nous considérons ce problème du point de vue du compilateur.
Examinons d'abord l'exemple suivant:
Objet [] Strings = new String [2]; Strings [0] = "Hi"; // okstrings [1] = 100; // Une arraystoreException est lancée.
Le code ci-dessus est facile à comprendre. Les tableaux de chaînes ne peuvent pas stocker des éléments entiers, et ces erreurs doivent souvent être découvertes tant que le code n'est pas exécuté, et le compilateur ne peut pas les reconnaître. Ensuite, jetons un coup d'œil à ce qui se passera si Java soutient la création de tableaux génériques:
Objet [] stringlists = new list <string> []; // Erreur du compilateur, mais prétendez qu'il est autorisé à stringlists [0] = new ArrayList <string> (); // ok // Une arraystoreException doit être lancée, mais le runtime ne peut pas le détecter.Stringlists [1] = new ArrayList <Integer> ();
Supposons que nous soutenons la création de tableaux génériques. Étant donné que les informations de type pendant l'exécution ont été effacées, le JVM ne connaît en fait pas la différence entre New ArrayList <string> () et New ArrayList <Integer> (). Si de telles erreurs se produisent dans les scénarios d'application pratiques, ils seront très difficiles à détecter.
Si vous en êtes toujours sceptique, vous pouvez essayer d'exécuter le code suivant:
classe publique ElasteTypeeQuIvalence {public static void main (String [] args) {class c1 = new ArrayList <string> (). getClass (); Classe C2 = new ArrayList <Integer> (). GetClass (); System.out.println (C1 == C2); // vrai }}Question 2
Continuez à réutiliser notre classe de nœud ci-dessus. Pour le code générique, le compilateur Java nous aidera secrètement à implémenter une méthode de pont.
Node de classe publique <T> {Données publiques T; Node public (T data) {this.data = data; } public void setData (t data) {System.out.println ("node.setData"); this.data = data; }} classe publique MyNode étend Node <Integer> {public mynode (integer data) {super (data); } public void setData (données entières) {System.out.println ("mynode.setData"); super.setData (données); }}Après avoir lu l'analyse ci-dessus, vous pouvez penser qu'après l'effacement de type, le compilateur transformera le nœud et le mynode en ce qui suit:
Node de classe publique {données d'objets publics; Node public (données d'objet) {this.data = data; } public void setData (données d'objet) {System.out.println ("node.setData"); this.data = data; }} public class MyNode étend le nœud {public mynode (integer data) {super (data); } public void setData (données entières) {System.out.println ("mynode.setData"); super.setData (données); }}En fait, ce n'est pas le cas. Regardons d'abord le code suivant. Lorsque ce code est exécuté, une classCastException sera lancée, ce qui invite à cette chaîne ne peut pas être convertie en entier:
Mynode mn = new mynode (5); nœud n = mn; // un type brut - le compilateur lance un avertissement non coché. // provoque un lancement d'une classe classcastException.// entier x = mn.data;
Si nous suivons le code que nous avons généré ci-dessus, nous ne devons pas signaler une erreur lors de l'exécution de la ligne 3 (notez que j'ai commenté la ligne 4), car la méthode SetData (données de chaîne) n'existe pas dans MyNode, afin que nous ne puissions qu'appeler la méthode SetData (données d'objet) du nœud de classe parent. Étant donné que de cette façon, le code de ligne 3 ci-dessus ne doit pas signaler une erreur, car bien sûr, la chaîne peut être convertie en objet, alors comment classCastException est-elle lancée?
En fait, le compilateur Java gère automatiquement le code ci-dessus:
class MyNode étend le nœud {// la méthode de pont générée par le compilateur public void setData (données d'objet) {setData ((Integer) data); } public void setData (données entières) {System.out.println ("mynode.setData"); super.setData (données); } // ...}C'est pourquoi l'erreur ci-dessus est signalée. Lorsque setData ((entier) données); La chaîne ne peut pas être convertie en entier. Par conséquent, lorsque le compilateur invite un avertissement non contrôlé dans la ligne 2 ci-dessus, nous ne pouvons pas choisir de l'ignorer, sinon nous devrons attendre l'heure d'exécution pour trouver l'exception. Ce serait formidable si nous ajoutions Node <Integer> n = mn au début, afin que le compilateur puisse nous aider à trouver des erreurs à l'avance.
Question 3
Comme nous l'avons mentionné ci-dessus, les génériques Java ne peuvent fournir que la vérification de type statique dans une large mesure, puis les informations de type seront effacées, de sorte que le compilateur ne passera pas la méthode suivante d'utilisation des paramètres de type pour créer des instances:
public static <e> void append (list <e> list) {e elem = new e (); // COMPILE-TIME ERROR LIST.ADD (ELEM);}Mais que devons-nous faire si nous voulons créer des instances en utilisant des paramètres de type dans certains scénarios? La réflexion peut être utilisée pour résoudre ce problème:
public static <e> void append (list <e> list, class <e> cls) lève une exception {e elem = cls.newinstance (); // ok list.add (elem);}Nous pouvons l'appeler comme ceci:
List <string> ls = new ArrayList <> (); append (ls, string.class);
En fait, pour le problème ci-dessus, vous pouvez également utiliser des modèles de conception d'usine et de modèle pour le résoudre. Les amis intéressés peuvent souhaiter jeter un œil à l'explication de la création d'instance de types dans le chapitre 15 en pensant en Java. Nous n'y allons pas ici.
Question 4
Nous ne pouvons pas utiliser directement le mot clé instanceof pour le code générique, car le compilateur Java effacera toutes les informations de type générique pertinentes lors de la génération du code, tout comme le JVM que nous avons vérifié ci-dessus ne peut pas reconnaître la différence entre ArrayList <Integer> et ArrayList <string> pendant l'exécution:
public static <e> void rtti (list <e> list) {if (list instanceof ArrayList <Integer>) {// error compile-time // ...}} => {ArrayList <Integer>, arrayList <string>, linkedList <actor>, ...}Comme ci-dessus, nous pouvons utiliser les caractères génériques pour réinitialiser les limites pour résoudre ce problème:
public static void rtti (list <?> list) {if (list instanceof arrayList <?>) {// ok; instanceof nécessite un type réifiable // ...}}Résumer
Ce qui précède consiste à recouvrir les génériques Java dans cet article, j'espère que cela sera utile à tout le monde. Les amis intéressés peuvent continuer à se référer à ce site:
Explication détaillée des bases du tableau Java
Les bases de la programmation Java: imitation du partage de code de connexion utilisateur
Les bases de la programmation Java Network: communication à sens unique
S'il y a des lacunes, veuillez laisser un message pour le signaler. Merci vos amis pour votre soutien pour ce site!