泛型
泛型(Generic type或者 generics)是对Java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
为什么要使用泛型
使用泛型意味着编写的代码可以被很多不同类型的对象所重用。在JAVA 5.0之前,java泛型程序设计是用继承实现的,类可以维护一个Object类型,但是这样会涉及到类型的强制转换,且使用时没有错误检查。但是通过泛型的使用,使得程序具有了更好的可读性和安全性。
泛型类
泛型类就是具有一个或多个类型变量的类。泛型类可以看作是普通类的工厂。可见ArrayList或是Map代码:
public class ArrayList<E> extendsAbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
public interfaceMap<K,V> {}
这里需要注意的是:
1、 类型变量使用大写形式且比较短,在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时可以使用临近的字母U和S)表示任意类型。
2、 ArrayList<Object>不是ArrayList<String>的父类,需要注意。
泛型方法
泛型不光可以应用到类上,还可以定义一个带有类型参数的简单方法。如:
class Test{
public <T> T method(){
return null;
}
public <T> void methodWithParam(T t){
}
}
注意类型变量要放在修饰符的后面,返回类型的前面。
在调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
Testtest = new Test();
test.<Integer>methodWithParam(123);
test.<String>methodWithParam(“abc”);
当然在大多数情况下,可以省略尖括号及其内容。编译器有足够的信息能够判断出所调用的方法,可以判别出T所代表的类型。可以这样用:
test.methodWithParam(123);
test.methodWithParam(“abc”);
泛型重载
见代码:
public class GenericsTest {
public void method(String str){
System.out.println("Stringmethod");
}
public <T> void method(T t){
System.out.println("Tmethod");
}
public <T> void method(T...t){
System.out.println("T...method");
}
/**
* @param args
*/
public static void main(String[] args) {
GenericsTest gt = new GenericsTest();
gt.method("");
gt.method(123);
gt.method(123,454);
gt.method(123,454,3.14);
gt.method(123,454,3.14,"");
}
}
输出结果为:
String method
T method
T...method
T...method
T...method
由上可以总结,
1、 指定类型的方法是要优先于泛型方法的,究其原因在于虚拟机在加载类时进行了类型擦除(详见后续部分)。
2、 另外一个很有意思的地方是,method(T…t),可以放入任何我们想放入的类型,这是怎么回事?这是由于编译器会自动会对传入的基本类型数据打包,并寻找他们共同的超类,可能是Number或是Object。这么做没有错,但是实际应用中不建议这样使用,尤其是Integer,Double,String搅和在一起,在使用会造成极大的困扰,而且最终类型变成了Obejct也不满足我们使用泛型的初衷。
类型变量的限制
有时,类或方法需要对类型变量加以限制。如一个方法内对象调用了compareTo方法,这时就需要确保类型变量都实现了Comparable接口:
public <T extendsComparable<T>> void method(T t){
…
}
限制类型可以是接口也可以是类,但都用extends来修饰。这是因为extends更接近子类的概念,而且java的设计者也不打算在语言中添加一个新的关键字。
限定类型可以是多个,是“&”来分隔。
public <T extendsComparable<T> & Serializable>void method(T t){
…
}
泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
无论何时定义一个泛型类型,都自动提供一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定类型的变量用Object)。对于有多个限定类型的地方,原始类型用第一个限定的类型变量来代替,其余的变量类型会在代码中使用强制转换来处理。所以这里需要注意的是,为了提高效率,应该将标签接口(即没有方法的接口,如:Comparable)放在边界列表的末尾。
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如:
Pair<Employee>buddies = …;
Employeebuddy = buddies.getFirst();
执行时,编译器把这个方法调用翻译为两条虚拟机指令:
1) 对原始方法Pair.getFirst的调用
2) 将返回的Object类型强制转换为Employee类型
同理,泛型域也会做同样的处理。
翻译泛型方法
类型擦除也会出现在泛型方法中。
public <T extendsComparable<T>> void method(T t){
…
}
擦除后变为:
public Comparable void method(Comparablet){
…
}
类型参数T被擦除,只留下了Comparable。方法
public <T extends String> void method(T t){
…
}
和方法
public String void method(String t){
…
}
是不能共存的,因为这已经不是overload了,<T extends String>擦除后只剩下String类型,两个方法就产生了冲突,因为其实两个方法是一样的。但是需要注意的是:
public <T extendsComparable<T> & Serializable>void method(T t){
…
}
public void method(Serializable t){
…
}
并不冲突,原因在于擦除时,需要用限制类型转换时使用的是第一个限制类型。擦除后为:
public void method(Comparablet){
…
}
符合多态的特性。加载时要多注意,需要多多注意。
在泛型重载中,提到了<T>void method(T t)和void method(String str)两个方法的区别,当时看的不是很清楚,但是在编译期对类型参数擦除后,就可以刻清楚地看到,这两个方法是method(Object t)和method(String str)的问题,输出那样的结果就很正常了。
在编写代码时要注意,类型擦除后与多态发生冲突。当然可以庆幸的一点就是IDE可智能的帮你识别这些情况,但是我们要知其然知其所以然。关键一点在于擦除,多考虑一下擦除后的代码就会发现擦除没什么复杂的。
总之要记住Java泛型转换的事实:
1、 虚拟机中没有泛型,只用普通的类和方法
2、 所有的类型参数都用它们的限定类型(最先出现的)替换
3、 桥方法被合成来保持多态
4、 为报出类型安全性,必要时要插入强制类型转换
使用泛型的约束
1、 不能使用基本类型实例化类型参数。因为基本类型不是Object的子类,这样就无法进行擦除操作了,虚拟机会不高兴的,呵呵。好在可以使用基本类型的封装类来进行处理。而且在实际处理中,编译器也会将基本类型自动打包为对应的封装类的。
2、 运行时类型查询只适用于原始类型。虚拟机中的对象总是一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
Pair<String> stringPair = …;
Pair<Double> doublePair = …;
这里,stringPair.getClass() == doublePair.getClass()返回true。
3、 不能抛出也不能捕获泛型类实例。
a) 泛型类是无法扩展Throwable的,这是不合法的
class Problem<T> extends Exception{
…
}
编译时是无法通过的。
b) 不能在catch子句中使用类型变量。
public <T extends Throwable> void doWork(T t){
try{
//do work
}catch(T e){}
}
会返回“Cannot use the type parameter Tin a catch block”的编译错误。
c) 在异常声明中可以使用类型变量。
public <T extends Throwable> void doWork(T t) throws T{
try{
//do work
}catch(Throwable e){}
}
这么做是合法的。
4、 参数化类型的数组不合法。
Pair<String>[] arr = newPair<String>[10];//ERROR
是不合法的。为什么呢?这里又要考虑到擦除这个特性了。擦除后数组类型变为Pair[],可以转换为Object[]:
Object[] objarray = arr;
数组可以记住原始的数据类型,所以存入其他类型是不可以的,如
Objarray[0] = “Hello”会返回一个类型异常。但是如果赋值是:
Objarray[0] = new Pair<Double>();则不会有任何问题,What?看看约束2,所有的类型查询只会对应到原始类型上。
所以我们要禁止参数化类型的数组。在使用时可以考虑使用相应的集合,如ArrayList<String>。
5、 不能实例化类型变量。
不能使用像new T(…),new T[…]或是T.class这样的表达式中的类型变量。因为类型擦除后可能就变为Object,显然初始打算不是要new Object()的。
6、 泛型类的静态上下文中类型变量无效。
不能在静态域或方法中引用类型变量。
public class StaticGenericsTest<T> {
private static T singleIstance;
public static TgetSingleIstance(){
if(singleIstance == null){}
return singleIstance;
}
}
编译器提示的错误是“Cannot make a static reference tothe non-static type T”,但是假设它是可以执行。我们可以传入不同的类型,然后不同的类型都会有其各自的单例,这绝对是让人欣喜的,代码得到了极大的重用,多好。但是实际情况是怎么回事呢?由于擦除,对,又是擦除的存在,类型变量直接被编译器转换为Object,可见我们只可能有一个单例,而不是我们所想的一堆。因此,要禁止使用带有类型变量的静态域和方法。
7、 注意擦除后的冲突。上面说了很多擦除导致的悲剧,多注意了。
泛型类型的继承规则
类型继承时要考虑原始类型。即Pair<List<String>>和Pair<LinkedList<String>>不是父子关系,而且它们的getClass返回的是同样的值。
而且把Pair<LinkedList<String>>实例,赋值给Pair<List<String>>是非法的。这点不像是数组。
泛型类可以扩展或实现其他泛型类。如:List<String>是LinkedList<String>的父类。关注原始类型。
通配符类型
固定的泛型类型系统使用起来不是特别的方便,所以java的设计者发明了一种很巧妙且安全的解决方案——通配符类型。
Pair<?extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类。
public void test(){
ArrayList<ArrayList<String>> al_str = newArrayList<>();
ArrayList<? extends List<String>> al = al_str;
//The methodadd(capture#1-of ? extends List<String>) in the type
//ArrayList<capture#1-of? extends
//List<String>> is notapplicable for the arguments (LinkedList<String>)
al.add(new LinkedList<String>());
}
会有问题,编译错误信息如上。使用通配符没有通过ArrayList<? extends List<String>>引用破坏ArrayList<ArrayList<String>>。
通配符的超类型限定
通配符限定和类型变量限定相似,但是它可以指定超类型限定。
ArrayList<? super LinkedList<String>>
对一个超类型限定的域采的get方法,无法确定具体的类型,只能知道是指定限定类型的超类,一般就只能返回Object类,编译器无法知道超类型限定域的set方法的具体类型。对于带有子类型限定的来说,正好相反。
所以说,待用超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
无限定通配符
对于无限定通配符,如:Pair<?>。它与Pair是不一样的。为什么呢?类型Pair<?>的方法:
?getFirst();
voidsetFirst(?);
其中getFirst的返回值只能赋值给一个Object。setFirst()方法不能被调用,甚至不能被Object调用。而Pair类中的setFirst()方法时可以调用的。
捕获通配符
public void test(ArrayList<?> p){
? t = p.get(0);
}
我们显然不能用“?”来作为一种类型,上面的代码一定是非法的。那么实际中我们应该怎么用呢?怎么处理类型?
我们可以考虑使用一个辅助方法。
public <T> void testHelper(ArrayList<T> p){
T t = p.get(0);
}
public void test(ArrayList<?> p){
testHelper(p);
}
这样通过testHelper的T来捕获通配符。注意testHelper是泛型方法,而test不是泛型方法,因为它具有固定的ArrayList<?>类型参数。注意。
题外话
Class类也是一个泛型类。String.class是一个Class<String>类的对象,也会唯一的对象。