泛型-泛型简介
程序员文章站
2022-04-10 14:44:15
...
泛型简介
先拿一个例子来说明泛型是什么。
有两个类如下,要构造两个类的对象,并打印出各自的成员x。
public class StringFoo {
private String x;
public String getX() {
return x;
}
public void setX(String x) {
this.x = x;
}
}
public class DoubleFoo {
private Double x;
public Double getX() {
return x;
}
public void setX(Double x) {
this.x = x;
}
}
如果要实现对Integer、Long、Date等类型的操作,还要写相应的类,实在是无聊之极。
因此,对上面的两个类进行重构,写成一个类,考虑如下:
上面的类中,成员和方法的逻辑都一样,就是类型不一样。Object是所有类的父类,因此可以考虑用Object做为成员类型,这样就可以实现通用了。
public class ObjectFoo {
private Object x;
public Object getX() {
return x;
}
public void setX(Object x) {
this.x = x;
}
}
调用的代码如下:
public class ObjectFooDemo {
public static void main(String args[]) {
ObjectFoo strFoo = new ObjectFoo();
strFoo.setX("Hello Generics!");
ObjectFoo douFoo = new ObjectFoo();
douFoo.setX(new Double("33"));
ObjectFoo objFoo = new ObjectFoo();
objFoo.setX(new Object());
String str = (String)strFoo.getX();
Double d = (Double)douFoo.getX();
Object obj = objFoo.getX();
System.out.println("strFoo.getX=" + str);
System.out.println("douFoo.getX=" + d);
System.out.println("strFoo.getX=" + obj);
}
}
以上,是没有泛型的情况下,我们编写的代码,采用最顶层基类Object进行类型声明,然后将值传入,取出时要进行强制类型转换。
JDK 从1.5 开始引入了泛型的概念,来优雅解决此类问题。采用泛型技术,编写的代码如下:
public class GenericsFoo <T> {
private T x;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
}
调用的代码如下:
public class GenericsFooDemo {
public static void main(String args[]){
GenericsFoo<String> strFoo=new GenericsFoo<String>();
strFoo.setX("Hello Generics!");
GenericsFoo<Double> douFoo=new GenericsFoo<Double>();
douFoo.setX(new Double("33");
GenericsFoo<Object> objFoo=new GenericsFoo<Object>();
objFoo.setX(new Object());
String str = strFoo.getX();
Double d = douFoo.getX();
Object obj = objFoo.getX();
System.out.println("strFoo.getX=" + str);
System.out.println("douFoo.getX=" + d);
System.out.println("strFoo.getX=" + obj);
}
}
注意,有几点明显的改变:
1. 对象创建时,明确给出类型,如GenericsFoo<String>。
2. 对象通过getX方法取出时,不需要进行类型转换。
3. 对各个方法的调用,如果参数类型与创建时指定的类型不匹配时,编译器就会报错。
那么我们为什么要泛型呢? 有两个好处:
1. 可以在编译时检查存储的数据是否正确。我们开发有一个趋向就是尽早的发现错误,最好就是在编译阶段, 泛型正好符合这一条件。
2. 减少了强制转换, String str = (String)strList.get(0);这样的操作属于一种向下转型, 是比较危险的操作, 当List内存储的对象不适String时就会抛出异常。
JDK1.5 中,java.util 包中的各种数据类型工具类,都支持泛型,在编程中被广泛使用,需要好好掌握。
泛型最常见的应用是应用在类、接口和方法上,下面分别介绍。
3.4.2 泛型应用在接口上:
public interface ValuePair<A,B> {
public A getA();
public B getB();
public String toString();
}
这里A和B都是代表类型。尖角号<>中,可以使用一个类型,也可以使用多个类型。
3.4.3 泛型应用在类上:
public class ValuePairImpl<A,B> {
public final A first;
public final B second;
public ValuePairImpl(A a, B b) { first = a; second = b; }
public A getA() { return first; }
public B getB() { return second; }
public String toString() {
return "(" + first + ", " + second + ")";
}
}
如果这个类实现泛型接口,则相应的写法为:
public class ValuePairImpl<A,B> implements ValuePair<A, B> {
……
}
3.4.4 泛型应用在方法上:
泛型也可以应用在单独的方法上,示例如下:
public class GenericMethod {
public <T> void printValue(T v) {
String str = v.getClass().getName() + “ = “ + v.toString();
System.out.println(str);
}
}
注意语法:在public修饰符后面是<>, 然后是函数返回值, 接着是函数名,函数参数。当然,返回值也可以是泛型的类型。
3.4.5 限制泛型的可用类型
以上介绍的三种泛型应用,应用在接口、类、方法上,是一种通用的做法,对泛型可以传入的类型没有任何限制。但有些场景下,我们希望对可用的类型进行限制,比如希望传入的类型必须从某个类继承(也就是说,必须是某个类的子类、孙类等),这种情况下就用到了泛型限制的语法。
extends:限制泛型类型必须为某个类的后代,包括本类型。
语法:<T extends parentClass>
这里,T为泛型类型,extends 关键字限制泛型类型必须是parentClass的后代。parentClass 指定父类的类型,也可以是接口。
在Java语言中,对类只能单继承,对接口可以多继承,如果要限制指定类型必须从某个类继承,并且实现了多个接口,则语法为:
<T extends parentClass & parentInterface1 & parentInterface2>
注意,类必须在接口前面。
举例如下:
public class BaseClass {
int value;
public BaseClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class SubClass extends BaseClass{
public SubClass(int value) {
super(value*2);
}
}
public class GenericBound<T extends BaseClass> {
public long sum(List<T> tList) {
long iValue = 0;
for (BaseClass base : tList) {
iValue += base.getValue();
}
return iValue;
}
public static void main(String[] args) {
GenericBound<SubClass> obj = new
GenericBound<SubClass>();
List<SubClass> list = new LinkedList<SubClass>();
list.add(new SubClass(5));
list.add(new SubClass(6));
System.out.println(obj.sum(list));
}
}
运行,输出结果为22.
接着,我们再深入探讨一下。把上面的例子该写如下:
public class GenericBound<T extends BaseClass> {
public long sum(List<T> tList) {
long iValue = 0;
for (BaseClass base : tList) {
iValue += base.getValue();
}
return iValue;
}
public static void main(String[] args) {
// 注意!!!
// 将obj 的类型由GenericBound<SubClass>改为GenericBound<BaseClass>,无法通过编译
GenericBound<BaseClass> obj = new
GenericBound<SubClass>();
List<SubClass> list = new LinkedList<SubClass>();
list.add(new SubClass(5));
list.add(new SubClass(6));
System.out.println(obj.sum(list));
}
}
语句GenericBound<BaseClass> obj = new GenericBound<SubClass>(); 无法通过编译,其根本原因在于,GenericBound类声明处的<T extends BaseClass>,限制了构造此类实例的时候T是确定的一个类型,这个类型是BaseClass的后代。但是BaseClass的后代还又很多,如SubClass3,SubClass4,如果针对每一种都要写出具体的子类类型,那也太麻烦了,干脆还不如用Object通用一下。能不能象普通类那样,用父类的类型引入各种子类的实例,这样不就简单了很多?答案是肯定的,泛型针对这种情况提供了更好的解决方案,那就是“通配符泛型”,下面详细讲解。
3.4.6 通配符泛型
Java 的泛型类型如同 java.lang.String,java.io.File 一样,属于普通的 Java 类型。比方说,下面两个变量的类型就是互不相同的:
Box<Object> boxObj = new Box<Object>();
Box<String> boxStr = new Box<String>();
虽然 String 是 Object 的子类,但是 Box<String> 和 Box<Object> 之间并没有什么关系——Box<String> 不是 Box<Object> 的子类或者子类型,因此,以下赋值语句是非法的:
boxObj = boxStr; // 无法通过编译
因此,我们希望使用泛型时,能象普通类那样,用父类的类型引入各种子类的实例,从而简化程序的开发。Java的泛型中,提供 ? 通配符来满足这个要求。
代码示例如下:
public class WildcardGeneric {
public void print(List<?> lst) {
for (int i = 0; i < lst.size(); i++) {
System.out.println(lst.get(i));
}
}
public static void main(String[] args) {
WildcardGeneric wg = new WildcardGeneric();
ArrayList<String> strList = new ArrayList<String>();
strList.add("One");
strList.add("Two");
wg.print(strList);
LinkedList<Integer> intList = new LinkedList<Integer>();
intList.add(25);
intList.add(30);
wg.print(intList);
}
}
但是这种情况下,WildcardGeneric.print 方法的参数可以接受类型可能对于程序员设计的意图而言太广泛了一点。因为我们可能只是希望 print 可以接受一个List,但这个List中的元素必须是Number的后代。因此,我们要对通配符有所限制,这时可以使用边界通配符(bounded wildcard)形式来满足这个要求。我们将 print 方法再修改一下:
public void print(List<? extends Number> lst) {
for (int i = 0; i < lst.size(); i++) {
System.out.println(lst.get(i));
}
}
这样,List<Integer>、List<Short> 等等类型的变量就可以传给 print 方法,而储存其他类型元素的 List 的泛型类型变量(如List<String>)传给 print 方法将是非法的。
除了 ?extends上边界通配符(upper bounded wildcard)以外,我们还可以使用下边界通配符(lower bounded wildcard),例如 List<? super ViewWindow>。
最后总结一下使用通配符的泛型类型的三种形式:
GenericType<?>
GenericType<? extends upperBoundType>
GenericType<? super lowerBoundType>
3.4.7 泛型深入
我们已经初步掌握了泛型的基本用法,接着再来探讨一下深入的主题。
我们还是先来看一段代码:
public class GenericsFoo <T> {
private T x;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public static void main(String[] args) {
GenericsFoo<String> gf = new GenericsFoo<String>();
gf.setX("Hello");
GenericsFoo<?> gf2 = gf;
gf2.setX("World"); // 报错!!!
String str = gf2.getX(); // 报错!!!
gf2.setX(gf2.getX()); // 报错!!!
}
}
注意,main 方法中的最后三行都是非法的,无法通过编译。本来是一个<String>的泛型,通过<?>来引用后,setX() 传入一个String就报错,getX() 返回值的类型也不是String。更为奇怪的是,语句gf2.setX(gf2.getX()); 就是从里面取出值然后再原封不动设置回去,也不行。这是怎么回事?
为了彻底弄清楚这些问题,我们需要了解JDK对泛型的内部实现原理。先看两个例子:
public class GenericClassTest {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
Class c3 = new ArrayList().getClass();
System.out.println(c1 == c3);
}
}
运行后,输出结果为:
true
true
这个例子说明,泛型ArrayList<String>、ArrayList<Integer>和没有使用泛型的ArrayList 其实是同一个类。就如同没使用泛型一样。
再看第二个例子:
class Element {}
class Box<T> {}
class Pair<KEY, VALUE> {}
public class GenericClassTest2 {
public static void main(String[] args) {
List<Element> list = new ArrayList<Element>();
Map<String,Element> map = new HashMap<String, Element>();
Box<Element> box = new Box<Element>();
Pair<Integer, String> p = new Pair<Integer, String>();
System.out.println(Arrays.toString(
list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
box.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
p.getClass().getTypeParameters()));
}
}
运行后,输出结果为:
[E]
[K, V]
[T]
[KEY, VALUE]
查阅JDK的文档,Class.getTypeParameters() 方法返回一个TypeVariable对象的数组,数组中每个TypeVariable对象描述了泛型中声明的类型。这似乎意味着,我们可以从TypeVariable 对象中找出泛型实例化时真正的类型。但是,从程序运行的输出结果,我们可以看出,Class.getTypeParameters() 返回的一系列TypeVariable 对象,仅仅表征了泛型声明时的参数化类型占位符,真正实例化时的类型都被抛弃了。因此,Java 泛型的真相是:
在泛型代码中,根本就没有参数化类型的信息。
产生这样一个事实的原因在于,JDK 对泛型的内部实现,采用了擦除(erasure)的方式,具体擦除的方式如下:
1) ArrayList<String>、ArrayList<Integer>、ArrayList<?>都被擦除成ArrayList
2) ArrayList<T extends BaseClass>、ArrayList<? extends BaseClass>都被擦除成ArrayList<BaseClass>
3) ArrayList<? super BaseClass>被擦除成ArrayList<BaseClass>
理解了擦除的实现机制后,我们再回过头来,分析一下前面的例子,看看为什么不能通过编译:
public class GenericsFoo <T> {
private T x;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public static void main(String[] args) {
GenericsFoo<String> gf = new GenericsFoo<String>();
gf.setX("Hello");
GenericsFoo<?> gf2 = gf;
gf2.setX("World"); // 报错!!!
String str = gf2.getX(); // 报错!!!
gf2.setX(gf2.getX()); // 报错!!!
}
}
由于擦除机制,GenericsFoo<?>都被擦除成GenericsFoo,类型被丢失后,那么相应的方法声明就变成了:
public Object getX();
public void setX(null x);
因此,泛型类的任何取出值的get方法,返回值都变成了Object,可以被调用,但返回值需要进行类型转换;泛型类的任何设置值的set方法,参数类型都变成了null,任何类型都无法转换成null类型,因此所有的set方法都无法被调用。
这样就形成了一个有趣的现象:对<?>的泛型类型,只能get,不能set。
以上阐述了Java泛型的擦除机制,导致一些有用的类型信息丢失。但我们可以通过一些技巧,让编译器重新构建出类型信息,从而使得set方法可以被正常调用。见如下的代码:
public void setGeneric(GenericsFoo<?> foo) {
setGenericHelper(foo);
}
private<V> void setGenericHelper(GenericsFoo<V> foo) {
foo.setX(foo.getX());
}
setGenericHelper() 是一个泛型方法,泛型方法引入了额外的类型参数(位于返回类型之前的尖括号中),这些参数用于表示参数和/或方法的返回值之间的类型约束。setGenericHelper () 这种声明方式,允许编译器(通过类型接口)对GenericsFoo 泛型的类型参数命名。但一个类型可以有父类、祖父类,还可以实现多个接口,那么编译器会转换成哪个类型呢?
我们用下面这段代码来验证一下:
public class GenericClassTest3 {
public static<T> String getType(T arg) {
return arg.getClass().getName();
}
public static void main(String[] args) {
Integer i = new Integer(5);
System.out.println(GenericClassTest3.getType(i));
}
}
程序运行后,输出结果为:
java.lang.Integer
因此,编译器能够推断 T 是 Integer、Number、 Serializable 或 Object,但它选择 Integer 作为满足约束的最具体类型。
另外,由于泛型的擦除机制,我们也无法直接对泛型类型用new操作符,比如:
public class GenericNew<T> {
public T create() {
T obj = new T(); // 无法通过编译!!!
return obj;
}
}
由于擦除机制,语句T obj = new T(); 擦除后就变成了
obj = new ();
于是就无法通过编译了。
但是,在一些情况下,我们还是需要对泛型类型的动态实例化。对于创建单个对象和创建数组,代码示例如下:
public class GenericNew<T> {
public T create(Class<T> cls) {
try {
Object obj = cls.newInstance();
return (T)obj;
} catch(Exception e) {
return null;
}
}
public T[] createArray(Class<T> cls, int len) {
try {
Object obj = java.lang.reflect.Array.newInstance(cls, len);
return (T[])obj;
} catch (Exception e) {
return null;
}
}
}
以上代码中,create 方法实现了动态创建一个泛型类型的实例,createArray 方法实现了动态创建一个泛型类型的实例数组。
先拿一个例子来说明泛型是什么。
有两个类如下,要构造两个类的对象,并打印出各自的成员x。
public class StringFoo {
private String x;
public String getX() {
return x;
}
public void setX(String x) {
this.x = x;
}
}
public class DoubleFoo {
private Double x;
public Double getX() {
return x;
}
public void setX(Double x) {
this.x = x;
}
}
如果要实现对Integer、Long、Date等类型的操作,还要写相应的类,实在是无聊之极。
因此,对上面的两个类进行重构,写成一个类,考虑如下:
上面的类中,成员和方法的逻辑都一样,就是类型不一样。Object是所有类的父类,因此可以考虑用Object做为成员类型,这样就可以实现通用了。
public class ObjectFoo {
private Object x;
public Object getX() {
return x;
}
public void setX(Object x) {
this.x = x;
}
}
调用的代码如下:
public class ObjectFooDemo {
public static void main(String args[]) {
ObjectFoo strFoo = new ObjectFoo();
strFoo.setX("Hello Generics!");
ObjectFoo douFoo = new ObjectFoo();
douFoo.setX(new Double("33"));
ObjectFoo objFoo = new ObjectFoo();
objFoo.setX(new Object());
String str = (String)strFoo.getX();
Double d = (Double)douFoo.getX();
Object obj = objFoo.getX();
System.out.println("strFoo.getX=" + str);
System.out.println("douFoo.getX=" + d);
System.out.println("strFoo.getX=" + obj);
}
}
以上,是没有泛型的情况下,我们编写的代码,采用最顶层基类Object进行类型声明,然后将值传入,取出时要进行强制类型转换。
JDK 从1.5 开始引入了泛型的概念,来优雅解决此类问题。采用泛型技术,编写的代码如下:
public class GenericsFoo <T> {
private T x;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
}
调用的代码如下:
public class GenericsFooDemo {
public static void main(String args[]){
GenericsFoo<String> strFoo=new GenericsFoo<String>();
strFoo.setX("Hello Generics!");
GenericsFoo<Double> douFoo=new GenericsFoo<Double>();
douFoo.setX(new Double("33");
GenericsFoo<Object> objFoo=new GenericsFoo<Object>();
objFoo.setX(new Object());
String str = strFoo.getX();
Double d = douFoo.getX();
Object obj = objFoo.getX();
System.out.println("strFoo.getX=" + str);
System.out.println("douFoo.getX=" + d);
System.out.println("strFoo.getX=" + obj);
}
}
注意,有几点明显的改变:
1. 对象创建时,明确给出类型,如GenericsFoo<String>。
2. 对象通过getX方法取出时,不需要进行类型转换。
3. 对各个方法的调用,如果参数类型与创建时指定的类型不匹配时,编译器就会报错。
那么我们为什么要泛型呢? 有两个好处:
1. 可以在编译时检查存储的数据是否正确。我们开发有一个趋向就是尽早的发现错误,最好就是在编译阶段, 泛型正好符合这一条件。
2. 减少了强制转换, String str = (String)strList.get(0);这样的操作属于一种向下转型, 是比较危险的操作, 当List内存储的对象不适String时就会抛出异常。
JDK1.5 中,java.util 包中的各种数据类型工具类,都支持泛型,在编程中被广泛使用,需要好好掌握。
泛型最常见的应用是应用在类、接口和方法上,下面分别介绍。
3.4.2 泛型应用在接口上:
public interface ValuePair<A,B> {
public A getA();
public B getB();
public String toString();
}
这里A和B都是代表类型。尖角号<>中,可以使用一个类型,也可以使用多个类型。
3.4.3 泛型应用在类上:
public class ValuePairImpl<A,B> {
public final A first;
public final B second;
public ValuePairImpl(A a, B b) { first = a; second = b; }
public A getA() { return first; }
public B getB() { return second; }
public String toString() {
return "(" + first + ", " + second + ")";
}
}
如果这个类实现泛型接口,则相应的写法为:
public class ValuePairImpl<A,B> implements ValuePair<A, B> {
……
}
3.4.4 泛型应用在方法上:
泛型也可以应用在单独的方法上,示例如下:
public class GenericMethod {
public <T> void printValue(T v) {
String str = v.getClass().getName() + “ = “ + v.toString();
System.out.println(str);
}
}
注意语法:在public修饰符后面是<>, 然后是函数返回值, 接着是函数名,函数参数。当然,返回值也可以是泛型的类型。
3.4.5 限制泛型的可用类型
以上介绍的三种泛型应用,应用在接口、类、方法上,是一种通用的做法,对泛型可以传入的类型没有任何限制。但有些场景下,我们希望对可用的类型进行限制,比如希望传入的类型必须从某个类继承(也就是说,必须是某个类的子类、孙类等),这种情况下就用到了泛型限制的语法。
extends:限制泛型类型必须为某个类的后代,包括本类型。
语法:<T extends parentClass>
这里,T为泛型类型,extends 关键字限制泛型类型必须是parentClass的后代。parentClass 指定父类的类型,也可以是接口。
在Java语言中,对类只能单继承,对接口可以多继承,如果要限制指定类型必须从某个类继承,并且实现了多个接口,则语法为:
<T extends parentClass & parentInterface1 & parentInterface2>
注意,类必须在接口前面。
举例如下:
public class BaseClass {
int value;
public BaseClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class SubClass extends BaseClass{
public SubClass(int value) {
super(value*2);
}
}
public class GenericBound<T extends BaseClass> {
public long sum(List<T> tList) {
long iValue = 0;
for (BaseClass base : tList) {
iValue += base.getValue();
}
return iValue;
}
public static void main(String[] args) {
GenericBound<SubClass> obj = new
GenericBound<SubClass>();
List<SubClass> list = new LinkedList<SubClass>();
list.add(new SubClass(5));
list.add(new SubClass(6));
System.out.println(obj.sum(list));
}
}
运行,输出结果为22.
接着,我们再深入探讨一下。把上面的例子该写如下:
public class GenericBound<T extends BaseClass> {
public long sum(List<T> tList) {
long iValue = 0;
for (BaseClass base : tList) {
iValue += base.getValue();
}
return iValue;
}
public static void main(String[] args) {
// 注意!!!
// 将obj 的类型由GenericBound<SubClass>改为GenericBound<BaseClass>,无法通过编译
GenericBound<BaseClass> obj = new
GenericBound<SubClass>();
List<SubClass> list = new LinkedList<SubClass>();
list.add(new SubClass(5));
list.add(new SubClass(6));
System.out.println(obj.sum(list));
}
}
语句GenericBound<BaseClass> obj = new GenericBound<SubClass>(); 无法通过编译,其根本原因在于,GenericBound类声明处的<T extends BaseClass>,限制了构造此类实例的时候T是确定的一个类型,这个类型是BaseClass的后代。但是BaseClass的后代还又很多,如SubClass3,SubClass4,如果针对每一种都要写出具体的子类类型,那也太麻烦了,干脆还不如用Object通用一下。能不能象普通类那样,用父类的类型引入各种子类的实例,这样不就简单了很多?答案是肯定的,泛型针对这种情况提供了更好的解决方案,那就是“通配符泛型”,下面详细讲解。
3.4.6 通配符泛型
Java 的泛型类型如同 java.lang.String,java.io.File 一样,属于普通的 Java 类型。比方说,下面两个变量的类型就是互不相同的:
Box<Object> boxObj = new Box<Object>();
Box<String> boxStr = new Box<String>();
虽然 String 是 Object 的子类,但是 Box<String> 和 Box<Object> 之间并没有什么关系——Box<String> 不是 Box<Object> 的子类或者子类型,因此,以下赋值语句是非法的:
boxObj = boxStr; // 无法通过编译
因此,我们希望使用泛型时,能象普通类那样,用父类的类型引入各种子类的实例,从而简化程序的开发。Java的泛型中,提供 ? 通配符来满足这个要求。
代码示例如下:
public class WildcardGeneric {
public void print(List<?> lst) {
for (int i = 0; i < lst.size(); i++) {
System.out.println(lst.get(i));
}
}
public static void main(String[] args) {
WildcardGeneric wg = new WildcardGeneric();
ArrayList<String> strList = new ArrayList<String>();
strList.add("One");
strList.add("Two");
wg.print(strList);
LinkedList<Integer> intList = new LinkedList<Integer>();
intList.add(25);
intList.add(30);
wg.print(intList);
}
}
但是这种情况下,WildcardGeneric.print 方法的参数可以接受类型可能对于程序员设计的意图而言太广泛了一点。因为我们可能只是希望 print 可以接受一个List,但这个List中的元素必须是Number的后代。因此,我们要对通配符有所限制,这时可以使用边界通配符(bounded wildcard)形式来满足这个要求。我们将 print 方法再修改一下:
public void print(List<? extends Number> lst) {
for (int i = 0; i < lst.size(); i++) {
System.out.println(lst.get(i));
}
}
这样,List<Integer>、List<Short> 等等类型的变量就可以传给 print 方法,而储存其他类型元素的 List 的泛型类型变量(如List<String>)传给 print 方法将是非法的。
除了 ?extends上边界通配符(upper bounded wildcard)以外,我们还可以使用下边界通配符(lower bounded wildcard),例如 List<? super ViewWindow>。
最后总结一下使用通配符的泛型类型的三种形式:
GenericType<?>
GenericType<? extends upperBoundType>
GenericType<? super lowerBoundType>
3.4.7 泛型深入
我们已经初步掌握了泛型的基本用法,接着再来探讨一下深入的主题。
我们还是先来看一段代码:
public class GenericsFoo <T> {
private T x;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public static void main(String[] args) {
GenericsFoo<String> gf = new GenericsFoo<String>();
gf.setX("Hello");
GenericsFoo<?> gf2 = gf;
gf2.setX("World"); // 报错!!!
String str = gf2.getX(); // 报错!!!
gf2.setX(gf2.getX()); // 报错!!!
}
}
注意,main 方法中的最后三行都是非法的,无法通过编译。本来是一个<String>的泛型,通过<?>来引用后,setX() 传入一个String就报错,getX() 返回值的类型也不是String。更为奇怪的是,语句gf2.setX(gf2.getX()); 就是从里面取出值然后再原封不动设置回去,也不行。这是怎么回事?
为了彻底弄清楚这些问题,我们需要了解JDK对泛型的内部实现原理。先看两个例子:
public class GenericClassTest {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
Class c3 = new ArrayList().getClass();
System.out.println(c1 == c3);
}
}
运行后,输出结果为:
true
true
这个例子说明,泛型ArrayList<String>、ArrayList<Integer>和没有使用泛型的ArrayList 其实是同一个类。就如同没使用泛型一样。
再看第二个例子:
class Element {}
class Box<T> {}
class Pair<KEY, VALUE> {}
public class GenericClassTest2 {
public static void main(String[] args) {
List<Element> list = new ArrayList<Element>();
Map<String,Element> map = new HashMap<String, Element>();
Box<Element> box = new Box<Element>();
Pair<Integer, String> p = new Pair<Integer, String>();
System.out.println(Arrays.toString(
list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
box.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
p.getClass().getTypeParameters()));
}
}
运行后,输出结果为:
[E]
[K, V]
[T]
[KEY, VALUE]
查阅JDK的文档,Class.getTypeParameters() 方法返回一个TypeVariable对象的数组,数组中每个TypeVariable对象描述了泛型中声明的类型。这似乎意味着,我们可以从TypeVariable 对象中找出泛型实例化时真正的类型。但是,从程序运行的输出结果,我们可以看出,Class.getTypeParameters() 返回的一系列TypeVariable 对象,仅仅表征了泛型声明时的参数化类型占位符,真正实例化时的类型都被抛弃了。因此,Java 泛型的真相是:
在泛型代码中,根本就没有参数化类型的信息。
产生这样一个事实的原因在于,JDK 对泛型的内部实现,采用了擦除(erasure)的方式,具体擦除的方式如下:
1) ArrayList<String>、ArrayList<Integer>、ArrayList<?>都被擦除成ArrayList
2) ArrayList<T extends BaseClass>、ArrayList<? extends BaseClass>都被擦除成ArrayList<BaseClass>
3) ArrayList<? super BaseClass>被擦除成ArrayList<BaseClass>
理解了擦除的实现机制后,我们再回过头来,分析一下前面的例子,看看为什么不能通过编译:
public class GenericsFoo <T> {
private T x;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public static void main(String[] args) {
GenericsFoo<String> gf = new GenericsFoo<String>();
gf.setX("Hello");
GenericsFoo<?> gf2 = gf;
gf2.setX("World"); // 报错!!!
String str = gf2.getX(); // 报错!!!
gf2.setX(gf2.getX()); // 报错!!!
}
}
由于擦除机制,GenericsFoo<?>都被擦除成GenericsFoo,类型被丢失后,那么相应的方法声明就变成了:
public Object getX();
public void setX(null x);
因此,泛型类的任何取出值的get方法,返回值都变成了Object,可以被调用,但返回值需要进行类型转换;泛型类的任何设置值的set方法,参数类型都变成了null,任何类型都无法转换成null类型,因此所有的set方法都无法被调用。
这样就形成了一个有趣的现象:对<?>的泛型类型,只能get,不能set。
以上阐述了Java泛型的擦除机制,导致一些有用的类型信息丢失。但我们可以通过一些技巧,让编译器重新构建出类型信息,从而使得set方法可以被正常调用。见如下的代码:
public void setGeneric(GenericsFoo<?> foo) {
setGenericHelper(foo);
}
private<V> void setGenericHelper(GenericsFoo<V> foo) {
foo.setX(foo.getX());
}
setGenericHelper() 是一个泛型方法,泛型方法引入了额外的类型参数(位于返回类型之前的尖括号中),这些参数用于表示参数和/或方法的返回值之间的类型约束。setGenericHelper () 这种声明方式,允许编译器(通过类型接口)对GenericsFoo 泛型的类型参数命名。但一个类型可以有父类、祖父类,还可以实现多个接口,那么编译器会转换成哪个类型呢?
我们用下面这段代码来验证一下:
public class GenericClassTest3 {
public static<T> String getType(T arg) {
return arg.getClass().getName();
}
public static void main(String[] args) {
Integer i = new Integer(5);
System.out.println(GenericClassTest3.getType(i));
}
}
程序运行后,输出结果为:
java.lang.Integer
因此,编译器能够推断 T 是 Integer、Number、 Serializable 或 Object,但它选择 Integer 作为满足约束的最具体类型。
另外,由于泛型的擦除机制,我们也无法直接对泛型类型用new操作符,比如:
public class GenericNew<T> {
public T create() {
T obj = new T(); // 无法通过编译!!!
return obj;
}
}
由于擦除机制,语句T obj = new T(); 擦除后就变成了
obj = new ();
于是就无法通过编译了。
但是,在一些情况下,我们还是需要对泛型类型的动态实例化。对于创建单个对象和创建数组,代码示例如下:
public class GenericNew<T> {
public T create(Class<T> cls) {
try {
Object obj = cls.newInstance();
return (T)obj;
} catch(Exception e) {
return null;
}
}
public T[] createArray(Class<T> cls, int len) {
try {
Object obj = java.lang.reflect.Array.newInstance(cls, len);
return (T[])obj;
} catch (Exception e) {
return null;
}
}
}
以上代码中,create 方法实现了动态创建一个泛型类型的实例,createArray 方法实现了动态创建一个泛型类型的实例数组。
更多泛型-泛型简介相关文章请关注PHP中文网!
上一篇: 浅谈关于预检请求
下一篇: CSS+JS打造带渐变的进度条