Java基础11:Java泛型详解
泛型概述
泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
一个栗子 一个被举了无数次的例子:
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d("泛型测试","item = " + item);
}
毫无疑问,程序的运行结果会以崩溃结束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
List arrayList = new ArrayList(); … //arrayList.add(100); 在编译阶段,编译器就会报错
特性
泛型只在编译阶段有效。看下面的代码:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
泛型有三种使用方式
分别为:泛型类、泛型接口、泛型方法
泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
一个最普通的泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//在类中声明的泛型整个类里面都可以用,除了静态部分,因为泛型是实例化时声明的。
//静态区域的代码在编译时就已经确定,只与类相关
class A <E>{
T t;
}
//类里面的方法或类中再次声明同名泛型是允许的,并且该泛型会覆盖掉父类的同名泛型T
class B <T>{
T t;
}
//静态内部类也可以使用泛型,实例化时赋予泛型实际类型
static class C <T> {
T t;
}
public static void main(String[] args) {
//报错,不能使用T泛型,因为泛型T属于实例不属于类
// T t = null;
}
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is 123456
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is key_vlaue
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
看一个例子:
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());
D/泛型测试: key is 111111
D/泛型测试: key is 4444
D/泛型测试: key is 55.55
D/泛型测试: key is false
注意: 泛型的类型参数只能是类类型,不能是简单类型。 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。 if(ex_num instanceof Generic){ }
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
当实现泛型接口的类,未传入泛型实参时:
-
/**
- 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
- 即:class FruitGenerator<T> implements Generator<T>{
- 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
当实现泛型接口的类,传入泛型实参时:
-
/**
- 传入泛型实参时:
- 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
- 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
- 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
- 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
泛型通配符
我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过Generic与Generic实际上是相同的一种基本类型。那么问题来了,在使用Generic作为形参的方法中,能否使用Generic的实例传入呢?在逻辑上类似于Generic和Generic是否可以看成具有父子关系的泛型类型呢?
为了弄清楚这个问题,我们使用Generic这个泛型类继续看下面的例子:
public void showKeyValue1(Generic<Number> obj){ Log.d("泛型测试","key value is " + obj.getKey()); } Generic<Integer> gInteger = new Generic<Integer>(123); Generic<Number> gNumber = new Generic<Number>(456); showKeyValue(gNumber); // showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> // cannot be applied to Generic<java.lang.Number> // showKeyValue(gInteger);
通过提示信息我们可以看到Generic不能被看作为`Generic的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic类型的类,这显然与java中的多态理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。由此类型通配符应运而生。
我们可以将上面的方法改一下:
public void showKeyValue1(Generic<?> obj){ Log.d("泛型测试","key value is " + obj.getKey());
类型通配符一般是使用?代替具体的类型实参,注意, 此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型
public void showKeyValue(Generic obj){ System.out.println(obj); } Generic<Integer> gInteger = new Generic<Integer>(123); Generic<Number> gNumber = new Generic<Number>(456); public void test () { // showKeyValue(gInteger);该方法会报错 showKeyValue1(gInteger); } public void showKeyValue1(Generic<?> obj) { System.out.println(obj); } // showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> // cannot be applied to Generic<java.lang.Number> // showKeyValue(gInteger); 1
泛型方法
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。
尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。 泛型类,是在实例化类的时候指明泛型的具体类型;
泛型方法,是在调用方法的时候指明泛型的具体类型 。
/** - 泛型方法的基本介绍 - @param tClass 传入的泛型实参 - @return T 返回值为T类型 - 说明: - 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。 - 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 - 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。 - 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 */ public <T> T genericMethod(Class<T> tClass)throws InstantiationException , IllegalAccessException{ T instance = tClass.newInstance(); return instance; } Object obj = genericMethod(Class.forName("com.test.test"));
泛型方法的基本用法
光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把我泛型方法再总结一下。
/** - 这才是一个真正的泛型方法。 - 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T - 这个T可以出现在这个泛型方法的任意位置. - 泛型的数量也可以为任意多个 - 如:public <T,K> K showKeyName(Generic<T> container){ - ... - } */ public class 泛型方法 { @Test public void test() { test1(); test2(new Integer(2)); test3(new int[3],new Object()); //打印结果 // null // 2 // [I@3d8c7aca // java.lang.Object@5ebec15 } //该方法使用泛型T public <T> void test1() { T t = null; System.out.println(t); } //该方法使用泛型T //并且参数和返回值都是T类型 public <T> T test2(T t) { System.out.println(t); return t; } //该方法使用泛型T,E //参数包括T,E public <T, E> void test3(T t, E e) { System.out.println(t); System.out.println(e); }
类中的泛型方法
当然这并不是泛型方法的全部,泛型方法可以出现在任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下
//注意泛型类先写类名再写泛型,泛型方法先写泛型再写方法名 //类中声明的泛型在成员和方法中可用 class A <T, E>{ { T t1 ; } A (T t){ this.t = t; } T t; public void test1() { System.out.println(this.t); } public void test2(T t,E e) { System.out.println(t); System.out.println(e); } } @Test public void run () { A <Integer,String > a = new A<>(1); a.test1(); a.test2(2,"ds"); // 1 // 2 // ds } static class B <T>{ T t; public void go () { System.out.println(t); } }
泛型方法与可变参数 再看一个泛型方法和可变参数的例子:
public class 泛型和可变参数 { @Test public void test () { printMsg("dasd",1,"dasd",2.0,false); print("dasdas","dasdas", "aa"); } //普通可变参数只能适配一种类型 public void print(String ... args) { for(String t : args){ System.out.println(t); } } //泛型的可变参数可以匹配所有类型的参数。。有点无敌 public <T> void printMsg( T... args){ for(T t : args){ System.out.println(t); } } //打印结果: //dasd //1 //dasd //2.0 //false }
静态方法与泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> { .... .... /** * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 * 如:public static void show(T t){..},此时编译器会提示错误信息: "StaticGenerator cannot be refrenced from static context" */ public static <T> void show(T t){ }
泛型方法总结
泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:
无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。
泛型上下边界 在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
为泛型添加上边界,即传入的类型实参必须是指定类型的子类型
public class 泛型通配符与边界 { public void showKeyValue(Generic<Number> obj){ System.out.println("key value is " + obj.getKey()); } @Test public void main() { Generic<Integer> gInteger = new Generic<Integer>(123); Generic<Number> gNumber = new Generic<Number>(456); showKeyValue(gNumber); //泛型中的子类也无法作为父类引用传入 // showKeyValue(gInteger); } //直接使用?通配符可以接受任何类型作为泛型传入 public void showKeyValueYeah(Generic<?> obj) { System.out.println(obj); } //只能传入number的子类或者number public void showKeyValue1(Generic<? extends Number> obj){ System.out.println(obj); } //只能传入Integer的父类或者Integer public void showKeyValue2(Generic<? super Integer> obj){ System.out.println(obj); } @Test public void testup () { //这一行代码编译器会提示错误,因为String类型并不是Number类型的子类 //showKeyValue1(generic1); Generic<String> generic1 = new Generic<String>("11111"); Generic<Integer> generic2 = new Generic<Integer>(2222); Generic<Float> generic3 = new Generic<Float>(2.4f); Generic<Double> generic4 = new Generic<Double>(2.56); showKeyValue1(generic2); showKeyValue1(generic3); showKeyValue1(generic4); } @Test public void testdown () { Generic<String> generic1 = new Generic<String>("11111"); Generic<Integer> generic2 = new Generic<Integer>(2222); Generic<Number> generic3 = new Generic<Number>(2); // showKeyValue2(generic1);本行报错,因为String并不是Integer的父类 showKeyValue2(generic2); showKeyValue2(generic3); } }
== 关于泛型数组要提一下 ==
看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。
也就是说下面的这个例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
这样也是可以的:
List<String>[] ls = new ArrayList[10];
下面使用Sun的一篇文档的一个例子来说明这个问题:
List<String>[] lsa = new List<String>[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。 下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Correct. Integer i = (Integer) lsa[1].get(0); // OK
最后
本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。
Java泛型详解
2017/03/28 | 分类: 基础技术 | 7 条评论 | 标签: 泛型
分享到:
原文出处: ZiWenXie
引言
泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。
泛型基础
泛型类
我们首先定义一个简单的Box类:
`public` `class` `Box {`` ``private` `String object;`` ``public` `void` `set(String object) { ``this``.object = object; }`` ``public` `String get() { ``return` `object; }``}`
这是最常见的做法,这样做的一个坏处是Box里面现在只能装入String类型的元素,今后如果我们需要装入Integer等其他类型的元素,还必须要另外重写一个Box,代码得不到复用,使用泛型可以很好的解决这个问题。
`public` `class` `Box<T> {`` ``// T stands for "Type"`` ``private` `T t;`` ``public` `void` `set(T t) { ``this``.t = t; }`` ``public` `T get() { ``return` `t; }``}`
这样我们的Box
类便可以得到复用,我们可以将T替换成任何我们想要的类型:
`Box<Integer> integerBox = ``new` `Box<Integer>();``Box<Double> doubleBox = ``new` `Box<Double>();``Box<String> stringBox = ``new` `Box<String>();`
泛型方法
看完了泛型类,接下来我们来了解一下泛型方法。声明一个泛型方法很简单,只要在返回类型前面加上一个类似<K, V>
的形式就行了:
`public` `class` `Util {`` ``public` `static` `<K, V> ``boolean` `compare(Pair<K, V> p1, Pair<K, V> p2) {`` ``return` `p1.getKey().equals(p2.getKey()) &&`` ``p1.getValue().equals(p2.getValue());`` ``}``}``public` `class` `Pair<K, V> {`` ``private` `K key;`` ``private` `V value;`` ``public` `Pair(K key, V value) {`` ``this``.key = key;`` ``this``.value = value;`` ``}`` ``public` `void` `setKey(K key) { ``this``.key = key; }`` ``public` `void` `setValue(V value) { ``this``.value = value; }`` ``public` `K getKey() { ``return` `key; }`` ``public` `V getValue() { ``return` `value; }``}`
我们可以像下面这样去调用泛型方法:
`Pair<Integer, String> p1 = ``new` `Pair<>(``1``, ``"apple"``);``Pair<Integer, String> p2 = ``new` `Pair<>(``2``, ``"pear"``);``boolean` `same = Util.<Integer, String>compare(p1, p2);`
或者在Java1.7/1.8利用type inference,让Java自动推导出相应的类型参数:
`Pair<Integer, String> p1 = ``new` `Pair<>(``1``, ``"apple"``);``Pair<Integer, String> p2 = ``new` `Pair<>(``2``, ``"pear"``);``boolean` `same = Util.compare(p1, p2);`
边界符
现在我们要实现这样一个功能,查找一个泛型数组中大于某个特定元素的个数,我们可以这样实现:
`public` `static` `<T> ``int` `countGreaterThan(T[] anArray, T elem) {`` ``int` `count = ``0``;`` ``for` `(T e : anArray)`` ``if` `(e > elem) ``// compiler error`` ``++count;`` ``return` `count;``}`
但是这样很明显是错误的,因为除了short, int, double, long, float, byte, char
等原始类型,其他的类并不一定能使用操作符>
,所以编译器报错,那怎么解决这个问题呢?答案是使用边界符。
`public` `interface` `Comparable<T> {`` ``public` `int` `compareTo(T o);``}`
做一个类似于下面这样的声明,这样就等于告诉编译器类型参数T
代表的都是实现了Comparable
接口的类,这样等于告诉编译器它们都至少实现了compareTo
方法。
`public` `static` `<T ``extends` `Comparable<T>> ``int` `countGreaterThan(T[] anArray, T elem) {`` ``int` `count = ``0``;`` ``for` `(T e : anArray)`` ``if` `(e.compareTo(elem) > ``0``)`` ``++count;`` ``return` `count;``}`
通配符
在了解通配符之前,我们首先必须要澄清一个概念,还是借用我们上面定义的Box类,假设我们添加一个这样的方法:
`public` `void` `boxTest(Box<Number> n) { ``/* ... */` `}`
那么现在Box<Number> n
允许接受什么类型的参数?我们是否能够传入Box<Integer>
或者Box<Double>
呢?答案是否定的,虽然Integer和Double是Number的子类,但是在泛型中Box<Integer>
或者Box<Double>
与Box<Number>
之间并没有任何的关系。这一点非常重要,接下来我们通过一个完整的例子来加深一下理解。
首先我们先定义几个简单的类,下面我们将用到它:
`class` `Fruit {}``class` `Apple ``extends` `Fruit {}``class` `Orange ``extends` `Fruit {}`
下面这个例子中,我们创建了一个泛型类Reader
,然后在f1()
中当我们尝试Fruit f = fruitReader.readExact(apples);
编译器会报错,因为List<Fruit>
与List<Apple>
之间并没有任何的关系。
`public` `class` `GenericReading {`` ``static` `List<Apple> apples = Arrays.asList(``new` `Apple());`` ``static` `List<Fruit> fruit = Arrays.asList(``new` `Fruit());`` ``static` `class` `Reader<T> {`` ``T readExact(List<T> list) {`` ``return` `list.get(``0``);`` ``}`` ``}`` ``static` `void` `f1() {`` ``Reader<Fruit> fruitReader = ``new` `Reader<Fruit>();`` ``// Errors: List<Fruit> cannot be applied to List<Apple>.`` ``// Fruit f = fruitReader.readExact(apples);`` ``}`` ``public` `static` `void` `main(String[] args) {`` ``f1();`` ``}``}`
但是按照我们通常的思维习惯,Apple和Fruit之间肯定是存在联系,然而编译器却无法识别,那怎么在泛型代码中解决这个问题呢?我们可以通过使用通配符来解决这个问题:
`static` `class` `CovariantReader<T> {`` ``T readCovariant(List<? ``extends` `T> list) {`` ``return` `list.get(``0``);`` ``}``}``static` `void` `f2() {`` ``CovariantReader<Fruit> fruitReader = ``new` `CovariantReader<Fruit>();`` ``Fruit f = fruitReader.readCovariant(fruit);`` ``Fruit a = fruitReader.readCovariant(apples);``}``public` `static` `void` `main(String[] args) {`` ``f2();``}`
这样就相当与告诉编译器, fruitReader的readCovariant方法接受的参数只要是满足Fruit的子类就行(包括Fruit自身),这样子类和父类之间的关系也就关联上了。
PECS原则
上面我们看到了类似<? extends T>
的用法,利用它我们可以从list里面get元素,那么我们可不可以往list里面add元素呢?我们来尝试一下:
`public` `class` `GenericsAndCovariance {`` ``public` `static` `void` `main(String[] args) {`` ``// Wildcards allow covariance:`` ``List<? ``extends` `Fruit> flist = ``new` `ArrayList<Apple>();`` ``// Compile Error: can't add any type of object:`` ``// flist.add(new Apple())`` ``// flist.add(new Orange())`` ``// flist.add(new Fruit())`` ``// flist.add(new Object())`` ``flist.add(``null``); ``// Legal but uninteresting`` ``// We Know that it returns at least Fruit:`` ``Fruit f = flist.get(``0``);`` ``}``}`
答案是否定,Java编译器不允许我们这样做,为什么呢?对于这个问题我们不妨从编译器的角度去考虑。因为List<? extends Fruit> flist
它自身可以有多种含义:
`List<? ``extends` `Fruit> flist = ``new` `ArrayList<Fruit>();` `List<? ``extends` `Fruit> flist = ``new` `ArrayList<Apple>();` `List<? ``extends` `Fruit> flist = ``new` `ArrayList<Orange>();`
-
当我们尝试add一个Apple的时候,flist可能指向
new ArrayList<Orange>()
; -
当我们尝试add一个Orange的时候,flist可能指向
new ArrayList<Apple>()
; -
当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能只想某种特定类型的Fruit,编译器无法识别所以会报错。
所以对于实现了<? extends T>
的集合类只能将它视为Producer向外提供(get)元素,而不能作为Consumer来对外获取(add)元素。
如果我们要add元素应该怎么做呢?可以使用<? super T>
:
`public` `class` `GenericWriting {`` ``static` `List<Apple> apples = ``new` `ArrayList<Apple>();`` ``static` `List<Fruit> fruit = ``new` `ArrayList<Fruit>();`` ``static` `<T> ``void` `writeExact(List<T> list, T item) {`` ``list.add(item);`` ``}`` ``static` `void` `f1() {`` ``writeExact(apples, ``new` `Apple());`` ``writeExact(fruit, ``new` `Apple());`` ``}`` ``static` `<T> ``void` `writeWithWildcard(List<? ``super` `T> list, T item) {`` ``list.add(item)`` ``}`` ``static` `void` `f2() {`` ``writeWithWildcard(apples, ``new` `Apple());`` ``writeWithWildcard(fruit, ``new` `Apple());`` ``}`` ``public` `static` `void` `main(String[] args) {`` ``f1(); f2();`` ``}``}`
这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List<? super Apple> list
,它可以有下面几种含义:
`List<? ``super` `Apple> list = ``new` `ArrayList<Apple>();``List<? ``super` `Apple> list = ``new` `ArrayList<Fruit>();``List<? ``super` `Apple> list = ``new` `ArrayList<Object>();`
当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:
-
“Producer Extends” – 如果你需要一个只读List,用它来produce T,那么使用
? extends T
。 -
“Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用
? super T
。 -
如果需要同时读取以及写入,那么我们就不能使用通配符了。
如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
`public` `class` `Collections {`` ``public` `static` `<T> ``void` `copy(List<? ``super` `T> dest, List<? ``extends` `T> src) {`` ``for` `(``int` `i=``0``; i<src.size(); i++)`` ```dest.set(i, src.get(i));`` ``}``}`
类型擦除
Java泛型中最令人苦恼的地方或许就是类型擦除了,特别是对于有C++经验的程序员。类型擦除就是说Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。对于这一点,如果阅读Java集合框架的源码,可以发现有些类其实并不支持泛型。
说了这么多,那么泛型擦除到底是什么意思呢?我们先来看一下下面这个简单的例子:
`public` `class` `Node<T> {`` ``private` `T data;`` ``private` `Node<T> next;`` ``public` `Node(T data, Node<T> next) {`` ``this``.data = data;`` ``this``.next = next;`` ``}`` ``public` `T getData() { ``return` `data; }`` ``// ...``}`
编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:
`public` `class` `Node {`` ``private` `Object data;`` ``private` `Node next;`` ``public` `Node(Object data, Node next) {`` ``this``.data = data;`` ``this``.next = next;`` ``}`` ``public` `Object getData() { ``return` `data; }`` ``// ...``}`
这意味着不管我们声明Node<String>
还是Node<Integer>
,到了运行期间,JVM统统视为Node<Object>。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:
`public` `class` `Node<T ``extends` `Comparable<T>> {`` ``private` `T data;`` ``private` `Node<T> next;`` ``public` `Node(T data, Node<T> next) {`` ``this``.data = data;`` ``this``.next = next;`` ``}`` ``public` `T getData() { ``return` `data; }`` ``// ...``}`
这样编译器就会将T
出现的地方替换成Comparable
而不再是默认的Object
了:
`public` `class` `Node {`` ``private` `Comparable data;`` ``private` `Node next;`` ``public` `Node(Comparable data, Node next) {`` ``this``.data = data;`` ``this``.next = next;`` ``}`` ``public` `Comparable getData() { ``return` `data; }`` ``// ...``}`
上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题,有些问题在C++的泛型中可能不会遇见,但是在Java中却需要格外小心。
问题一
在Java中不允许创建泛型数组,类似下面这样的做法编译器会报错:
`List<Integer>[] arrayOfLists = ``new` `List<Integer>[``2``]; ``// compile-time error`
为什么编译器不支持上面这样的做法呢?继续使用逆向思维,我们站在编译器的角度来考虑这个问题。
我们先来看一下下面这个例子:
`Object[] strings = ``new` `String[``2``];``strings[``0``] = ``"hi"``; ``// OK``strings[``1``] = ``100``; ``// An ArrayStoreException is thrown.`
对于上面这段代码还是很好理解,字符串数组不能存放整型元素,而且这样的错误往往要等到代码运行的时候才能发现,编译器是无法识别的。接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:
`Object[] stringLists = ``new` `List<String>[]; ``// compiler error, but pretend it's allowed``stringLists[``0``] = ``new` `ArrayList<String>(); ``// OK``// An ArrayStoreException should be thrown, but the runtime can't detect it.``stringLists[``1``] = ``new` `ArrayList<Integer>();`
假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM实际上根本就不知道new ArrayList<String>()
和new ArrayList<Integer>()
的区别。类似这样的错误假如出现才实际的应用场景中,将非常难以察觉。
如果你对上面这一点还抱有怀疑的话,可以尝试运行下面这段代码:
`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); ``// true`` ``}``}`
问题二
继续复用我们上面的Node
的类,对于泛型代码,Java编译器实际上还会偷偷帮我们实现一个Bridge method。
`public` `class` `Node<T> {`` ``public` `T data;`` ``public` `Node(T data) { ``this``.data = data; }`` ``public` `void` `setData(T data) {`` ``System.out.println(``"Node.setData"``);`` ``this``.data = data;`` ``}``}``public` `class` `MyNode ``extends` `Node<Integer> {`` ``public` `MyNode(Integer data) { ``super``(data); }`` ``public` `void` `setData(Integer data) {`` ``System.out.println(``"MyNode.setData"``);`` ``super``.setData(data);`` ``}``}`
看完上面的分析之后,你可能会认为在类型擦除后,编译器会将Node和MyNode变成下面这样:
`public` `class` `Node {`` ``public` `Object data;`` ``public` `Node(Object data) { ``this``.data = data; }`` ``public` `void` `setData(Object data) {`` ``System.out.println(``"Node.setData"``);`` ``this``.data = data;`` ``}``}``public` `class` `MyNode ``extends` `Node {`` ``public` `MyNode(Integer data) { ``super``(data); }`` ``public` `void` `setData(Integer data) {`` ``System.out.println(``"MyNode.setData"``);`` ``super``.setData(data);`` ``}``}`
实际上不是这样的,我们先来看一下下面这段代码,这段代码运行的时候会抛出ClassCastException
异常,提示String无法转换成Integer:
`MyNode mn = ``new` `MyNode(``5``);``Node n = mn; ``// A raw type - compiler throws an unchecked warning``n.setData(``"Hello"``); ``// Causes a ClassCastException to be thrown.``// Integer x = mn.data;`
如果按照我们上面生成的代码,运行到第3行的时候不应该报错(注意我注释掉了第4行),因为MyNode中不存在setData(String data)
方法,所以只能调用父类Node的setData(Object data)
方法,既然这样上面的第3行代码不应该报错,因为String当然可以转换成Object了,那ClassCastException
到底是怎么抛出的?
实际上Java编译器对上面代码自动还做了一个处理:
`class` `MyNode ``extends` `Node {`` ``// Bridge method generated by the compiler`` ``public` `void` `setData(Object data) {`` ``setData((Integer) data);`` ``}`` ``public` `void` `setData(Integer data) {`` ``System.out.println(``"MyNode.setData"``);`` ``super``.setData(data);`` ``}`` ``// ...``}`
这也就是为什么上面会报错的原因了,setData((Integer) data);
的时候String无法转换成Integer。所以上面第2行编译器提示unchecked warning
的时候,我们不能选择忽略,不然要等到运行期间才能发现异常。如果我们一开始加上Node<Integer> n = mn
就好了,这样编译器就可以提前帮我们发现错误。
问题三
正如我们上面提到的,Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过:
`public` `static` `<E> ``void` `append(List<E> list) {`` ``E elem = ``new` `E(); ``// compile-time error`` ``list.add(elem);``}`
但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:
`public` `static` `<E> ``void` `append(List<E> list, Class<E> cls) ``throws` `Exception {`` ``E elem = cls.newInstance(); ``// OK`` ``list.add(elem);``}`
我们可以像下面这样调用:
`List<String> ls = ``new` `ArrayList<>();``append(ls, String.``class``);`
实际上对于上面这个问题,还可以采用Factory和Template两种设计模式解决,感兴趣的朋友不妨去看一下Thinking in Java中第15章中关于Creating instance of types(英文版第664页)的讲解,这里我们就不深入了。
问题四
我们无法对泛型代码直接使用instanceof
关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息,正如我们上面验证过的JVM在运行时期无法识别出ArrayList<Integer>
和ArrayList<String>
的之间的区别:
`public` `static` `<E> ``void` `rtti(List<E> list) {`` ``if` `(list ``instanceof` `ArrayList<Integer>) { ``// compile-time error`` ``// ...`` ``}``}``=> { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }`
和上面一样,我们可以使用通配符重新设置bounds来解决这个问题:
`public` `static` `void` `rtti(List<?> list) {`` ``if` `(list ``instanceof` `ArrayList<?>) { ``// OK; instanceof requires a reifiable type`` ``// ...`` ``}``}`
工厂模式
接下来我们利用泛型来简单的实现一下工厂模式,首先我们先声明一个接口Factory
:
`package` `typeinfo.factory;``public` `interface` `Factory<T> {`` ``T create();``}`
接下来我们来创建几个实体类FuelFilter
和AirFilter
以及FanBelt
和GeneratorBelt
。
`class` `Filter ``extends` `Part {}``class` `FuelFilter ``extends` `Filter {`` ``public` `static` `class` `Factory ``implements` `typeinfo.factory.Factory<FuelFilter> {`` ``public` `FuelFilter create() {`` ``return` `new` `FuelFilter();`` ``}`` ``}``}``class` `AirFilter ``extends` `Filter {`` ``public` `static` `class` `Factory ``implements` `typeinfo.factory.Factory<AirFilter> {`` ``public` `AirFilter create() {`` ``return` `new` `AirFilter();`` ``}`` ``}``}`
Part
类的实现如下,注意我们上面的实体类都是Part
类的间接子类。在Part类我们注册
了我们上面的声明的实体类。所以以后我们如果要创建相关的实体类的话,只需要在调用Part类的相关方法了。这么做的一个好处就是如果的业务中出现了CabinAirFilter
或者PowerSteeringBelt
的话,我们不需要修改太多的代码,只需要在Part类中将它们注册即可。
`class` `Part {`` ``static` `List<Factory<? ``extends` `Part>> partFactories =`` ``new` `ArrayList<Factory<? ``extends` `Part>>();`` ``static` `{`` ``partFactories.add(``new` `FuelFilter.Factory());`` ``partFactories.add(``new` `AirFilter.Factory());`` ``partFactories.add(``new` `FanBelt.Factory());`` ``partFactories.add(``new` `PowerSteeringBelt.Factory());`` ``}`` ``private` `static` `Random rand = ``new` `Random(``47``);`` ``public` `static` `Part createRandom() {`` ``int` `n = rand.nextInt(partFactories.size());`` ``return` `partFactories.get(n).create();`` ``}`` ``public` `String toString() {`` ``return` `getClass().getSimpleName();`` ``}``}`
最后我们来测试一下:
`public` `class` `RegisteredFactories {`` ``public` `static` `void` `main(String[] args) {`` ``for` `(``int` `i = ``0``; i < ``10``; i++) {`` ``System.out.println(Part.createRandom());`` ``}`` ``}``}`
References
ORACLE-DOCUMENTATION THINKING IN JAVA EFFECTIVE JAVA