Ce que cet article vous apporte est une analyse détaillée (code) des caractères génériques en Java. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.
La non-pertinence des sous-types des types génériques a été évoquée dans la partie précédente de cet article. Mais parfois, on souhaite pouvoir utiliser des types génériques comme des types ordinaires :
◆ Upcaster une référence à un objet générique
◆ Downcaster une référence à un objet générique
Upcast une référence à un objet générique
Par exemple, supposons que nous ayons de nombreuses boîtes, chacune contenant des fruits différents, et que nous devions trouver un moyen universel pour gérer n'importe quelle boîte de fruits. Plus généralement, A est un sous-type de B, et nous devons trouver un moyen d'attribuer une instance de type C à une déclaration de type C.
Pour ce faire, nous devons utiliser une déclaration d'extension avec des caractères génériques, comme dans l'exemple suivant :
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples;
"? extends" est un sous-type du type générique La corrélation devient réalité : Apple est un sous-type de Fruit et List
Downcast une référence à un objet générique
Maintenant, permettez-moi de vous présenter un autre caractère générique : ? Si le type B est un supertype (type parent) du type A, alors C est un sous-type de C :
List<Fruit> fruits = new ArrayList<Fruit>(); List<? super Apple> = fruits;
Pourquoi les marqueurs génériques fonctionnent ?
Le principe est désormais clair : comment tirer profit de cette nouvelle structure grammaticale ?
? extends
Reprenons un exemple utilisé dans cette deuxième partie, qui parle des dépendances de sous-types des tableaux Java :
Apple[] apples = new Apple[ 1 ]; Fruit[] fruits = apples; fruits[ 0 ] = new Strawberry();
Comme nous Vous pouvez voir que lorsque vous ajoutez un objet Strawberry à un tableau d'objets Apple déclaré comme tableau Fruit, le code peut être compilé, mais une exception est levée au moment de l'exécution.
Nous pouvons désormais utiliser des caractères génériques pour convertir le code pertinent en génériques : comme Apple est une sous-classe de Fruit, nous utilisons le caractère générique ? extends afin que la définition d'un objet List
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples; fruits.add( new Strawberry());
Cette fois, le code ne peut pas être compilé ! Le compilateur Java vous empêchera d'ajouter une fraise à une liste de fruits. Nous pouvons détecter l'erreur au moment de la compilation et il n'est pas nécessaire de vérifier au moment de l'exécution pour garantir que les types incompatibles sont ajoutés à la liste. Même si vous ajoutez un objet Fruit à la liste :
fruits.add( new Fruit());
Vous n'avez aucun moyen de faire cela. En fait, vous ne pouvez écrire aucune valeur dans une structure de données qui utilise extends.
La raison est très simple, vous pouvez y penser de cette façon : ce caractère générique ? extends T indique au compilateur que nous avons affaire à un sous-type de type T, mais nous ne savons pas ce qu'est ce sous-type. Puisqu'il n'y a aucun moyen d'en être sûr, afin de garantir la sécurité du type, nous ne sommes pas autorisés à y ajouter des données de ce type. D'un autre côté, parce que nous savons que quel que soit son type, il s'agit toujours d'un sous-type de type T. Lorsque nous lisons des données, nous pouvons nous assurer que les données que nous obtenons sont une instance de type T :
Fruit get = fruits.get( 0 );
? super
Utilisé ? Quelles sont les situations générales du super joker ? Regardons ceci d'abord :
List<Fruit> fruits = new ArrayList<Fruit>(); List<? super Apple> = fruits;
Nous voyons que les fruits pointent vers une liste qui contient certains des supertypes d'Apple. Encore une fois, nous ne savons pas exactement ce qu'est la superclasse, mais nous savons qu'Apple et toutes les sous-classes d'Apple sont compatibles avec son type. Puisque ce type inconnu est Apple et la superclasse de GreenApple, on peut écrire :
fruits.add( new Apple()); fruits.add( new GreenApple());
Si on veut y ajouter la superclasse d'Apple, le compilateur vous avertira :
fruits.add( new Fruit()); fruits.add( new Object());
Parce que nous ne savons pas de quel type de superclasse il s'agit, toutes ces instances ne sont pas autorisées à être ajoutées.
Et si vous obteniez des données de ce type ? Il s'avère que vous ne pouvez supprimer qu'une instance d'Object : comme nous ne savons pas ce qu'est la superclasse, la seule chose que le compilateur peut garantir est qu'il s'agit d'un Object, car Object est la superclasse de n'importe quel type Java.
Principe d'accès et loi PECS
En résumant les caractéristiques des extensions ? et du super joker ?, nous pouvons tirer les conclusions suivantes :
◆ Si Si vous souhaitez obtenir des données à partir d'un type de données, utilisez le caractère générique ? extends
◆ Si vous souhaitez écrire l'objet dans une structure de données, utilisez le super caractère générique ? pour enregistrer les deux, si vous souhaitez toujours le prendre, n'utilisez pas de caractères génériques.
C'est ce que Maurice Naftalin appelle le principe d'accès dans son livre "Java Generics and Collections", et ce que Joshua Bloch appelle la règle PECS dans son livre "Effective Java".
Bloch a rappelé que PECS signifie « Producer Extends, Consumer Super », ce qui est plus facile à retenir et à utiliser.
Ce qui précède suit du bas :
Le didacticiel Java
Java Generics and Collections, par Maurice Naftalin et Philip Wadler
Version Java chinoise efficace ( 2ème édition), de Joshua Bloch.
尽管有这么多丰富的资料,有时我感觉,有很多的程序员仍然不太明白Java泛型的功用和意义。这就是为什么我想使用一种最简单的形式来总结一下程序员需要知道的关于Java泛型的最基本的知识。
Java泛型由来的动机
理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作:
List<Apple> box = ...; Apple apple = box.get( 0 );
上面的代码自身已表达的很清楚:box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:
List box = ...; Apple apple = (Apple) box.get( 0 );
很明显,泛型的主要好处就是让编译器保留参数的类型信息,执行类型检查,执行类型转换操作:编译器保证了这些类型转换的绝对无误。
相对于依赖程序员来记住对象类型、执行类型转换——这会导致程序运行时的失败,很难调试和解决,而编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误。
泛型的构成
由泛型的构成引出了一个类型变量的概念。根据Java语言规范,类型变量是一种没有限制的标志符,产生于以下几种情况:
◆ 泛型类声明
◆ 泛型接口声明
◆ 泛型方法声明
◆ 泛型构造器(constructor)声明
泛型类和接口
如果一个类或接口上有一个或多个类型变量,那它就是泛型。类型变量由尖括号界定,放在类或接口名的后面:
public interface List<T> extends Collection<T> { ... }
简单的说,类型变量扮演的角色就如同一个参数,它提供给编译器用来类型检查的信息。
Java类库里的很多类,例如整个Collection框架都做了泛型化的修改。例如,我们在上面的第一段代码里用到的List接口就是一个泛型类。在那段代码里,box是一个List对象,它是一个带有一个Apple类型变量的List接口的类实现的实例。编译器使用这个类型变量参数在get方法被调用、返回一个Apple对象时自动对其进行类型转换。
实际上,这新出现的泛型标记,或者说这个List接口里的get方法是这样的:
T get( int index);
get方法实际返回的是一个类型为T的对象,T是在List
泛型方法和构造器(Constructor)
非常的相似,如果方法和构造器上声明了一个或多个类型变量,它们也可以泛型化。
public static <t> T getFirst(List<T> list)
这个方法将会接受一个List
例子
你既可以使用Java类库里提供的泛型类,也可以使用自己的泛型类。
类型安全的写入数据…
下面的这段代码是个例子,我们创建了一个List
List<String> str = new ArrayList<String>(); str.add( "Hello " ); str.add( "World." );
如果我们试图在List
str.add( 1 ); // 不能编译
类型安全的读取数据…
当我们在使用List
String myString = str.get( 0 );
遍历
类库中的很多类,诸如Iterator
for (Iterator<String> iter = str.iterator(); iter.hasNext();) { String s = iter.next(); System.out.print(s); }
使用foreach
“for each”语法同样受益于泛型。前面的代码可以写出这样:
for (String s: str) { System.out.print(s); }
这样既容易阅读也容易维护。
自动封装(Autoboxing)和自动拆封(Autounboxing)
在使用Java泛型时,autoboxing/autounboxing这两个特征会被自动的用到,就像下面的这段代码:
List<Integer> ints = new ArrayList<Integer>(); ints.add( 0 ); ints.add( 1 ); int sum = 0 ; for ( int i : ints) { sum += i; }
然而,你要明白的一点是,封装和解封会带来性能上的损失,所有,通用要谨慎的使用。
子类型
在Java中,跟其它具有面向对象类型的语言一样,类型的层级可以被设计成这样:
在Java中,类型T的子类型既可以是类型T的一个扩展,也可以是类型T的一个直接或非直接实现(如果T是一个接口的话)。因为“成为某类型的子类型”是一个具有传递性质的关系,如果类型A是B的一个子类型,B是C的子类型,那么A也是C的子类型。在上面的图中:
◆ FujiApple(富士苹果)是Apple的子类型
◆ Apple是Fruit(水果)的子类型
◆ FujiApple(富士苹果)是Fruit(水果)的子类型
所有Java类型都是Object类型的子类型。
B类型的任何一个子类型A都可以被赋给一个类型B的声明:
Apple a = ...; Fruit f = a;
泛型类型的子类型
如果一个Apple对象的实例可以被赋给一个Fruit对象的声明,就像上面看到的,那么,List
答案会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。
这意味着下面的这段代码是无效的:
List<Apple> apples = ...; List<Fruit> fruits = apples;
下面的同样也不允许:
List < Apple > apples; List < Fruit > fruits = ...; apples = fruits ;
为什么?一个苹果是一个水果,为什么一箱苹果不能是一箱水果?
在某些事情上,这种说法可以成立,但在类型(类)封装的状态和操作上不成立。如果把一箱苹果当成一箱水果会发生什么情况?
List<Apple> apples = ...; List<Fruit> fruits = apples; fruits.add( new Strawberry());
如果可以这样的话,我们就可以在list里装入各种不同的水果子类型,这是绝对不允许的。
另外一种方式会让你有更直观的理解:一箱水果不是一箱苹果,因为它有可能是一箱另外一种水果,比如草莓(子类型)。
这是一个需要注意的问题吗?
应该不是个大问题。而程序员对此感到意外的最大原因是数组和泛型类型上用法的不一致。对于泛型类型,它们和类型的子类型之间是没什么关系的。而对于数组,它们和子类型是相关的:如果类型A是类型B的子类型,那么A[]是B[]的子类型:
Apple[] apples = ...; Fruit[] fruits = apples;
可是稍等一下!如果我们把前面的那个议论中暴露出的问题放在这里,我们仍然能够在一个apple类型的数组中加入strawberrie(草莓)对象:
Apple[] apples = new Apple[ 1 ]; Fruit[] fruits = apples; fruits[ 0 ] = new Strawberry();
这样写真的可以编译,但是在运行时抛出ArrayStoreException异常。因为数组的这特点,在存储数据的操作上,Java运行时需要检查类型的兼容性。这种检查,很显然,会带来一定的性能问题,你需要明白这一点。
重申一下,泛型使用起来更安全,能“纠正”Java数组中这种类型上的缺陷。
现在估计你会感到很奇怪,为什么在数组上会有这种类型和子类型的关系,我来给你一个《Java Generics and Collections》这本书上给出的答案:如果它们不相关,你就没有办法把一个未知类型的对象数组传入一个方法里(不经过每次都封装成 Object[]),就像下面的:
void sort(Object[] o);
泛型出现后,数组的这个个性已经不再有使用上的必要了(下面一部分我们会谈到这个),实际上是应该避免使用
相关推荐:
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!