Cet article présente principalement l'utilisation des génériques Java et les problèmes liés à l'effacement de type. A une très bonne valeur de référence. Jetons un coup d'œil avec l'éditeur ci-dessous
Introduction
Java a introduit le mécanisme générique dans la version 1.5. L'essence des génériques est constituée de types paramétrés, c'est-à-dire<.>Le type de variable est un paramètre qui est spécifié comme un type spécifique lorsqu'il est utilisé. Les génériques peuvent être utilisés pour les classes, les interfaces et les méthodes. En utilisant des génériques, le code peut être rendu plus simple et plus sûr. Cependant, les génériques en Java utilisent l'effacement de type, ils ne sont donc que des pseudo-génériques. Cet article résume l'utilisation des génériques et les problèmes existants, se référant principalement aux « Réflexions sur la programmation Java ».
Les deux autres articles de cette série :Utilisation de base
Classes génériques
S'il existe une classe Holder utilisée pour envelopper une variable, le type de cette variable peut être arbitraire. Comment écrire le Holder ? Avant les génériques, vous pouviez faire ceci :public class Holder1 { private Object a; public Holder1(Object a) { this.a = a; } public void set(Object a) { this.a = a; } public Object get(){ return a; } public static void main(String[] args) { Holder1 holder1 = new Holder1("not Generic"); String s = (String) holder1.get(); holder1.set(1); Integer x = (Integer) holder1.get(); } }Dans Holder1, il y a une variable référencée par Object. Étant donné que n'importe quel type peut être transtypé en Object, ce Holder peut accepter n'importe quel type. Lorsqu'il le retire, Holder sait seulement qu'il enregistre un Objet
Objet , il doit donc être forcé au type correspondant. Dans la méthode principale,holder1 enregistre d'abord une string, qui est un objet String, puis enregistre un objet Integer (le paramètre 1 sera automatiquement encadré). Le casting est déjà problématique lors de la suppression de variables du Holder. Vous devez également mémoriser différents types ici. Si vous faites une erreur, une exception d'exécution se produira.
Regardons la version générique de Holder :public class Holder2<T> { private T a; public Holder2(T a) { this.a = a; } public T get() { return a; } public void set(T a) { this.a = a; } public static void main(String[] args) { Holder2<String> holder2 = new Holder2<>("Generic"); String s = holder2.get(); holder2.set("test"); holder2.set(1);//无法编译 参数 1 不是 String 类型 } }Dans Holder2, la variable a est un type paramétré T. T est juste un identifiant, et d'autres lettres peuvent également être utilisé. Lors de la création de l'objet Holder2, le type du paramètre T est passé entre crochets, puis dans cet objet, toutes les occurrences de T équivaut à être remplacées par String. Ce que get récupère maintenant n'est pas un objet, mais un objet String, il n'y a donc pas besoin de À travers l'exemple ci-dessus, nous pouvons voir que pan rend le code plus simple et plus sûr. Après l'introduction des génériques, certaines classes de la bibliothèque Java, telles que les classes conteneurs couramment utilisées, ont également été réécrites pour prendre en charge les génériques. Lorsque nous les utiliserons, nous transmettrons des types de paramètres, tels que : ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a ;>();.
Méthodes génériques
Les génériques peuvent non seulement cibler des classes, mais peuvent également rendre une méthode générique individuellement. Par exemple :public class GenericMethod { public <K,V> void f(K k,V v) { System.out.println(k.getClass().getSimpleName()); System.out.println(v.getClass().getSimpleName()); } public static void main(String[] args) { GenericMethod gm = new GenericMethod(); gm.f(new Integer(0),new String("generic")); } } 代码输出: Integer StringLa GenericMethod. la classe elle-même n'est pas générique. Il n'est pas nécessaire de transmettre des paramètres génériques lors de la création de son objet, mais sa méthode f est une méthode générique. Avant le type de retour se trouve son identifiant de paramètre b77a8d9c3c319e50d4b02a976b347910. Notez qu'il existe ici deux paramètres génériques, il peut donc y avoir plusieurs paramètres génériques. Lors de l'appel d'une méthode générique, vous n'avez pas besoin de transmettre explicitement les paramètres génériques, ce qui n'est pas le cas dans l'appel ci-dessus. En effet, le compilateur utilise l'inférence de type de paramètre pour déduire les types de K et V en fonction des types des arguments transmis (ici, entier et chaîne).
Effacement de type
Qu'est-ce que l'effacement de type
Utilisation des génériques Java Le mécanisme d'effacement de type a a suscité beaucoup de controverses, à tel point que les fonctions génériques de Java ont été limitées et ne peuvent être qualifiées que de "pseudo-génériques". Qu’est-ce que l’effacement de texte ? Pour faire simple, les paramètres de type n'existent qu'au moment de la compilation, la machine virtuelle Java (JVM) ne connaît pas l'existence de génériques. Regardons d'abord un exemple :public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } }Le code ci-dessus a deux ArrayLists différentes : ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a et ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4. À notre avis, leurs types paramétrés sont différents, l’un enregistre les entiers et l’autre les chaînes. Mais en comparant leurs objets Class, le résultat du code ci-dessus est vrai. Cela signifie qu'il s'agit de la même classe du point de vue de la JVM. Dans des langages tels que C et
C# qui prennent en charge les vrais génériques, ce sont des classes différentes.
Le paramètre générique sera effacé jusqu'à sa première limite. Par exemple, dans la classe Holder2 ci-dessus, si le type de paramètre est un seul T, alors il sera effacé en Objet, ce qui équivaut à tous les endroits où T apparaît. Remplacer par Objet. Ainsi, du point de vue de la JVM, la variable enregistrée a est toujours de type Object. La raison pour laquelle il est automatiquement supprimé est le type de paramètre que nous avons transmis. En effet, le compilateur insère le code de conversion de type dans le fichier de bytecode compilé et nous n'avons pas besoin de le convertir manuellement. Si le type de paramètre a des limites, effacez-le jusqu'à sa première limite, qui sera abordée dans la section suivante.擦除带来的问题
擦除会出现一些问题,下面是一个例子:
class HasF { public void f() { System.out.println("HasF.f()"); } } public class Manipulator<T> { private T obj; public Manipulator(T obj) { this.obj = obj; } public void manipulate() { obj.f(); //无法编译 找不到符号 f() } public static void main(String[] args) { HasF hasF = new HasF(); Manipulator<HasF> manipulator = new Manipulator<>(hasF); manipulator.manipulate(); } }
上面的 Manipulator 是一个泛型类,内部用一个泛型化的变量 obj,在 manipulate 方法中,调用了 obj 的方法 f(),但是这行代码无法编译。因为类型擦除,编译器不确定 obj 是否有 f() 方法。解决这个问题的方法是给 T 一个边界:
class Manipulator2<T extends HasF> { private T obj; public Manipulator2(T x) { obj = x; } public void manipulate() { obj.f(); } }
现在 T 的类型是 1f179c3e268e631bc7ed98c5289251b7,这表示 T 必须是 HasF 或者 HasF 的导出类型。这样,调用 f() 方法才安全。HasF 就是 T 的边界,因此通过类型擦除后,所有出现 T 的
地方都用 HasF 替换。这样编译器就知道 obj 是有方法 f() 的。
但是这样就抵消了泛型带来的好处,上面的类完全可以改成这样:
class Manipulator3 { private HasF obj; public Manipulator3(HasF x) { obj = x; } public void manipulate() { obj.f(); } }
所以泛型只有在比较复杂的类中才体现出作用。但是像 1f179c3e268e631bc7ed98c5289251b7 这种形式的东西不是完全没有意义的。如果类中有一个返回 T 类型的方法,泛型就有用了,因为这样会返回准确类型。比如下面的例子:
class ReturnGenericType<T extends HasF> { private T obj; public ReturnGenericType(T x) { obj = x; } public T get() { return obj; } }
这里的 get() 方法返回的是泛型参数的准确类型,而不是 HasF。
类型擦除的补偿
类型擦除导致泛型丧失了一些功能,任何在运行期需要知道确切类型的代码都无法工作。比如下面的例子:
public class Erased<T> { private final int SIZE = 100; public static void f(Object arg) { if(arg instanceof T) {} // Error T var = new T(); // Error T[] array = new T[SIZE]; // Error T[] array = (T)new Object[SIZE]; // Unchecked warning } }
通过 new T() 创建对象是不行的,一是由于类型擦除,二是由于编译器不知道 T 是否有默认的构造器。一种解决的办法是传递一个工厂对象并且通过它创建新的实例。
interface FactoryI<T> { T create(); } class Foo2<T> { private T x; public <F extends FactoryI<T>> Foo2(F factory) { x = factory.create(); } // ... } class IntegerFactory implements FactoryI<Integer> { public Integer create() { return new Integer(0); } } class Widget { public static class Factory implements FactoryI<Widget> { public Widget create() { return new Widget(); } } } public class FactoryConstraint { public static void main(String[] args) { new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.Factory()); } }
另一种解决的方法是利用模板设计模式:
abstract class GenericWithCreate<T> { final T element; GenericWithCreate() { element = create(); } abstract T create(); } class X {} class Creator extends GenericWithCreate<X> { X create() { return new X(); } void f() { System.out.println(element.getClass().getSimpleName()); } } public class CreatorGeneric { public static void main(String[] args) { Creator c = new Creator(); c.f(); } }
具体类型的创建放到了子类继承父类时,在 create 方法中创建实际的类型并返回。
总结
本文介绍了 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!