1. L'introduction du concept de génériques (pourquoi les génériques sont-ils nécessaires) ? (Recommandé : tutoriel vidéo Java)
Tout d'abord, jetons un coup d'œil au code court suivant :
public class GenericTest { public static void main(String[] args) { List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); for (int i = 0; i < list.size(); i++) { String name = (String) list.get(i); // 1 System.out.println("name:" + name); } } }
définit une collection de type Liste Deux valeurs. de type String y sont ajoutés, suivis d'une valeur de type Integer. Ceci est tout à fait autorisé, car le type de liste par défaut est Objet.
Dans les boucles suivantes, des erreurs similaires à //1 peuvent facilement se produire en raison de l'oubli d'ajouter des valeurs de type Integer à la liste avant ou pour d'autres raisons d'encodage. Parce que la phase de compilation est normale, mais une exception "java.lang.ClassCastException" se produit pendant l'exécution. De telles erreurs sont donc difficiles à détecter lors du codage.
Au cours du processus de codage ci-dessus, nous avons constaté qu'il y avait deux problèmes principaux :
1 Lorsque nous mettons un objet dans une collection, la collection ne se souvient pas du type de l'objet. cet objet est à nouveau retiré de la collection, le type compilé de l'objet passe au type Object, mais son type d'exécution est toujours son propre type.
2. Par conséquent, lors de la suppression des éléments de collection en //1, une conversion de type forcée artificielle vers un type cible spécifique est requise et des exceptions "java.lang.ClassCastException" sont susceptibles de se produire.
Existe-t-il donc un moyen de permettre à une collection de mémoriser les types d'éléments de la collection, de sorte que tant qu'il n'y a pas de problèmes lors de la compilation, les exceptions "java.lang.ClassCastException" ne se produiront pas pendant l'exécution. ? La réponse est d'utiliser des génériques.
2. Que sont les génériques ?
Les génériques, c'est-à-dire les "types paramétrés". En ce qui concerne les paramètres, la chose la plus familière est qu'il existe des paramètres formels lors de la définition d'une méthode, puis les paramètres réels sont transmis lors de l'appel de cette méthode.
Alors, comment comprenez-vous les types paramétrés ? Comme son nom l'indique, le type est paramétré à partir du type spécifique d'origine, similaire aux paramètres variables de la méthode. À ce stade, le type est également défini sous forme de paramètres (qui peuvent être appelés paramètres de type), puis le type. un type spécifique est transmis lors de l'utilisation/de l'appel du type (argument de type).
Cela semble un peu compliqué. Tout d’abord, jetons un coup d’œil à la manière générique d’écrire l’exemple ci-dessus.
public class GenericTest { public static void main(String[] args) { /* List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); */ List<String> list = new ArrayList<String>(); list.add("qqyumidi"); list.add("corn"); //list.add(100); // 1 提示编译错误 for (int i = 0; i < list.size(); i++) { String name = list.get(i); // 2 System.out.println("name:" + name); } } }
Après avoir utilisé l'écriture générique, une erreur de compilation se produira lors de la tentative d'ajout d'un objet de type Integer à //1 via Listf7e83be87db5cd2d9a8a0b8117b38cd4, il est directement limité à la collection de listes qui ne peut contenir que du type String. elements. , il n'est donc pas nécessaire d'effectuer une conversion de type forcée à //2, car à ce stade, la collection peut mémoriser les informations de type de l'élément et le compilateur peut déjà confirmer qu'il s'agit d'un type String.
Combiné avec la définition générique ci-dessus, nous savons que dans Listf7e83be87db5cd2d9a8a0b8117b38cd4, String est un paramètre de type réel, c'est-à-dire que l'interface List correspondante doit contenir des paramètres de type. Et le résultat de retour de la méthode get() est également directement le type de ce paramètre formel (c'est-à-dire le paramètre réel du type entrant correspondant). Jetons un coup d'œil à la définition spécifique de l'interface List :
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); boolean equals(Object o); int hashCode(); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); List<E> subList(int fromIndex, int toIndex); }
Nous pouvons voir qu'après avoir adopté la définition générique dans l'interface List, le E dans 1a4db2c2c2313771e5742b6debf617a1 représente le paramètre de type et peut recevoir un Type spécifique. arguments, et dans cette définition d'interface, partout où E apparaît, il représente le même type d'argument reçu de l'extérieur.
Naturellement, ArrayList est la classe d'implémentation de l'interface List, et sa forme de définition est :
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); } //...省略掉其他具体的定义过程 }
De là, nous comprenons du point de vue du code source pourquoi il y a une erreur de compilation lors de l'ajout d'un Objet de type Integer en //1 , et le type obtenu par get() en //2 est directement le type String.
3. Interfaces génériques personnalisées, classes génériques et méthodes génériques
À partir du contenu ci-dessus, tout le monde a compris le processus de fonctionnement spécifique des génériques. Nous savons également que les interfaces, classes et méthodes peuvent également être définies à l'aide de génériques et utilisées en conséquence. Oui, dans une utilisation spécifique, il peut être divisé en interfaces génériques, classes génériques et méthodes génériques.
Les interfaces génériques personnalisées, les classes génériques et les méthodes génériques sont similaires à List et ArrayList dans le code source Java ci-dessus. Comme suit, nous examinons la définition la plus simple des classes et méthodes génériques :
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); System.out.println("name:" + name.getData()); } } class Box<T> { private T data; public Box() { } public Box(T data) { this.data = data; } public T getData() { return data; } }
Dans le processus de définition des interfaces génériques, des classes génériques et des méthodes génériques, nous voyons couramment les paramètres T, E, K, V des mêmes form sont souvent utilisés pour représenter des paramètres génériques car ils reçoivent des arguments de type transmis par des utilisations externes. Ainsi, pour les différents arguments de type transmis, les types des instances d'objet correspondantes sont-ils générés de la même manière ?
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); System.out.println("name class:" + name.getClass()); // com.qqyumidi.Box System.out.println("age class:" + age.getClass()); // com.qqyumidi.Box System.out.println(name.getClass() == age.getClass()); // true } }
À partir de là, nous avons constaté que lors de l'utilisation de classes génériques, bien que différents arguments génériques soient transmis, différents types ne sont pas réellement générés. Il n'y a qu'une seule classe générique en mémoire, qui est le type original le plus basique (. Box dans cet exemple). Bien sûr, nous pouvons logiquement le comprendre comme plusieurs types génériques différents.
La raison est que le but du concept de génériques en Java est qu'il n'agit que sur l'étape de compilation du code. Pendant le processus de compilation, une fois les résultats génériques correctement vérifiés, les génériques seront les informations pertinentes. effacé, c'est-à-dire que le fichier de classe compilé avec succès ne contient aucune information générique. Les informations génériques n'entrent pas dans la phase d'exécution.
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
四.类型通配符
接着上面的结论,我们知道,Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a是否可以看成具有父子关系的泛型类型呢?
为了弄清这个问题,我们继续看下下面这个例子:
public class GenericTest { public static void main(String[] args) { Box<Number> name = new Box<Number>(99); Box<Integer> age = new Box<Integer>(712); getData(name); //The method getData(Box<Number>) in the type GenericTest is //not applicable for the arguments (Box<Integer>) getData(age); // 1 } public static void getData(Box<Number> data){ System.out.println("data :" + data.getData()); } }
我们发现,在代码//1处出现了错误提示信息:The method getData(Boxc8f01a3f8889dcf657849dd45bc0fc4c) in the t ype GenericTest is not applicable for the arguments (Boxc0f559cc8d56b43654fcbe4aa9df7b4a)。显然,通过提示信息,我们知道Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。那么,原因何在呢?
public class GenericTest { public static void main(String[] args) { Box<Integer> a = new Box<Integer>(712); Box<Number> b = a; // 1 Box<Float> f = new Box<Float>(3.14f); b.setData(f); // 2 } public static void getData(Box<Number> data) { System.out.println("data :" + data.getData()); } } class Box<T> { private T data; public Box() { } public Box(T data) { setData(data); } public T getData() { return data; } public void setData(T data) { this.data = data; } }
这个例子中,显然//1和//2处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。
假设Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上可以视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类,那么//1和//2处将不会有错误提示了,那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且由于在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Boxc8f01a3f8889dcf657849dd45bc0fc4c不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。
好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。
这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Boxc0f559cc8d56b43654fcbe4aa9df7b4a和Boxc8f01a3f8889dcf657849dd45bc0fc4c的父类的一个引用类型,由此,类型通配符应运而生。
类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box6b3d0130bba23ae47fe2b8e8cddf0195在逻辑上是Boxc0f559cc8d56b43654fcbe4aa9df7b4a、Boxc8f01a3f8889dcf657849dd45bc0fc4c...等所有Boxf4669da5dd7ec4cbc793dba728d85fa1的父类。由此,我们依然可以定义泛型方法,来完成此类需求。
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); } }
有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?
在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); //getUpperNumberData(name); // 1 getUpperNumberData(age); // 2 getUpperNumberData(number); // 3 } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); } public static void getUpperNumberData(Box<? extends Number> data){ System.out.println("data :" + data.getData()); } }
此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。
类型通配符上限通过形如Boxa2b037db85f4e1df0e812b9647ac55a8形式定义,相对应的,类型通配符下限为Boxda50108ad159903fabe211f1543600e8形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。
更多java知识请关注java基础教程栏目。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!