泛型类似于一个模板,实质就是参数化类型,通过一个类型参数T,用来指示元素的类型,可以被用在类、接口和方法中,对应的称为泛型类、泛型接口、泛型方法。
Java还没加入泛型类前,泛型程序设计实际是通过继承实现的。AllayList只维护一个Object引用的数组
public class ArrayList {
private Object [] arr;
public ObjArray(int n) {
this.arr = new Object[n];
}
public void add (int i, Object o) {
this.arr[i] = o;
}
public Object get (int i) {
return this.arr[i];
}
}
这样设计有两个问题:
当获取一个值时,必须进行强制类型转换;
String filename =(String)arr.get(0);
可以向数组列表中加入任何类型的对象,但是编译和运行不报错;
arr.add(new File("..."));
为此引入泛型机制,在调用get时,不需要进行强制类型转化,编译器知道返回值类型,并且可以避免插入错误类型,类型参数T使得程序具有更好的安全性和可读性。
一个泛型类可以有一个或者多个类型变量的类
public class Pair<T>{...}
public class Pair<T,U>{...}
类型变量使用大写,Java中,一般使用变量E代表集合的元素类型,K表示关键字,V表示关键字值的类型,T表示任意类型。
用具体的类型替代类型变量就可以实例化泛型类型:Pair<String>(由于泛型的尖括号内容不被显示,用< 替换<)
类型变量放在修饰符后,返回类型前面;
class ArrayAlg{
public static <T> T getMiddle(T a){
return a[a.length / 2];
}
}
泛型方法可以定义在普通类或者泛型类中;
在实例化一个泛型类对象时,构造函数可以省去泛型类型;
有时,类或方法需要对变量类型进行约束,例如要求它必须是某个超类的子类,或者必须实现了某个接口,那么被定义的泛型类作为接收方,也需要对传入的类型变量T的值做一些限定和约束。
public class Pair<T entends SuperClass>{...}
public static <T entends Comparable> T min(T[] a){...}
T和绑定类型可以是类或接口,关键字extends相比较implemets更接近子类的概念;
限定类型用&分隔,逗号分隔类型变量;
<T extends Comparable & Serializable>
限定中至多一个类,如果用该类作为限定,必须位于限定列表的第一个,可以拥有多个接口超类型;
<T extends SuperClass&Comparable>
先来讲一下什么叫擦除?无论何时定义一个泛型类型,都自动提供一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定的变量用object)
1.不能使用基本类型实例化类型参数
不能使类型参数替代基本类型,应使用基本类型对应的包装器类型,当包装器类型不能接受替换时,可以用独立的类或者方法处理。
Pair<int> node = new Pair<int> (); // 非法
Pair<Integer> node = new Pair<Integer> ();//合法
2.运行时类型查询只适用于原始类型
所有类型查询只产生原始类型,无论何时使用instanceof或者泛型类型的强制类型转换表达式都会报编译器错误。
if(a instanceof Pair<T>) //error
Pair<String> p=(Pair<String>) a;//warning--can only test that a is a Pair
当类型变量不同时,假如使用getClass方法比较,可能会得到true,两次调用getClass都将返回Pair.class
3.不能创建参数化类型的数组
不能实例化参数化类型的数组,如下代码会报错
Pair<String>[] table=new Pair<String>[10];//error
可以把它转换为Object[]类型的数组,数组会记得它的元素类型,Object[] obj=table;
需要注意的是,只是不允许创建这些数组,但声明类型为Pair<String>[]的变量是合法的,不可以用new Pair<String>[10]初始化变量。
另外,如果需要收集参数化类型对象,可以使用AllayList:ArrayList
4.不能实例化类型变量
不能使用new T(…) , new T[…] , T.class这样的表达式中的类型变量,可以通过反射Class.newInstance方法来构造泛型对象,如下代码来支配class对象:
public static <T> Pair<T> makePair(Class<T> cl){
try{
return new Pair<> (cl.newInstance(),cl.newInstance())}
catch(Exception ex){return null;}
}
可以使用如下调用:String.class实际上是Class<String>的唯一一个实例,方法能推断出Pair的类型
Pair<String> p=Pair.makePair(String.class);
5.泛型类的静态上下文中类型变量无效
public class Pair<T> {
private static T t;
public static T get () {
/* 报错
提示: 'Pair.this' can not be referenced from a static context
由于类型擦除后,只有Pair类,包含一个t域,不能在静态域或方法中引用类型变量
*/
return T;
}
}
6.不能抛出或者捕获泛型类的实例
不能抛出或者捕获泛型类对象,包括泛型类拓展Throwable也不合法,如下:
public class Pair {
public static <T extends Throwable> void doWork () {
try {
do work
}catch (T t) {// 报错 提示: Cannot catch type parameters
}
}
}
java异常处理中使用泛型可以消除对已检查异常的检查,如下是合法的:
public class Pair {
public static <T extends Throwable> void doWork (T t) throws T {
try {
do work
}catch (Throwable realCause) {
throw t;
}
}
}
必须注意泛型与Java数组之间的区别,还是用雇员及经理来说,我们可以将一个Manager[]数组赋值给一个类型为Employee[]的变量,但是绝对不可以将Pair<Manager>转换为Pair<Employee>,另外泛型类可以拓展或者实现其他的泛型类,例如ArrayList
固定的泛型类型可能使用中并不是很愉快,假设我们要编写一个打印雇员的方法,但是向之前所说,不能将Pair<Manager> 传递进来,因此Java的设计者提出了“通配符类型”来解决它。
1 .有限定通配符
public static void printBuddis(Pair<? entends Employee> p){
Employee first = p.getFirst();
...
}
有限定通配符存在一个问题,由于编译器只知道某个Employee的子类型,但是并不知道的具体什么类型,因此不能调用setFirst()方法
2.超类型限定通配符
带有超类型限定的通配符可以为方法提供参数,但是不能使用返回值;也就是说带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取;
?super Manager该通配符限制为Manager的所有超类型
3.无限定通配符
Pair>和Pair的本质区别在:可以为任意Object对象调用原始的Pair类的setObject调用,有方法如下:
? getFirst()
void setFirst(?)//可以调用setFirst(null)
可以测试pair是否包括一个null引用
public static boolean hasNull(Pair<?> p){
return p.getFirst()==null|| p.getSecond()==null;
}
4.通配符获取
编写一个交换一个pair元素的方法 :
public static void swap(Pair>)
通配符不是类型变量,因此不能再代码中使用 ?为一种类型。代码 ?t=p.getFrist(); 是非法的。我们可以编写一个辅助方法swapHelper,如下:
public static <T> void swapHelper(Pair<T> p){
T t=p.getFrist();
p.setFirst(p.getSecond());
p.setSecond(t);
}
现在我们可以在swap方法中调用swapHelper了
参考书籍:《Java核心卷Ⅰ》