笔记:泛型
泛型程序设计意味着编写额代码可以被很多不同类型的对象所重用,并提供强类型校验,避免强制类型转换,并使程序具有更好的可读性和安全性。
- 定义简单泛型类
一个泛型类就是具有一个或多个类型变量的类,定义格式如下:
public class Pair<T>
{
……
}
泛型类引入了一个类型变量T,使用尖括号括起来,并放在类名的后面,泛型类可以有多个类型变量,多个类型变量使用","号分割,在类定义中的类型变量可以用于指定方法的方法类型、数据域和局部变量的类型,示例如下:
private T first;
public void setFirst(T first){…}
public T getFirst(){…}
在Java库中,使用类型变量 E 表示集合元素的元素类型;K和V分别表示关键字于值的类型;T、U和S表示任意类型
- 定义泛型方法
定义泛型方法是在普通类中定义的,类型变量放在修饰符的后面,返回类型的前面,定义格式如下:
public class ArrayAlg{
public static <T> T getMiddle(T…a){
return a[a.length/2];
}
}
调用一个泛型方法是,可以在方法名前的尖括号放入具体类型,正常情况下,编译器会推断出具体类型,此时就可以省略,调用格式如下:
String middle = ArrayAlg.<String>getMiddle("123","456","789");
- 类型变量的限定
有时候类或方法需要对类型变量加以约束,我们可以使用关键字 extends ,定义格式如下:
public class Pair<T extends Comparable>{
……
}
对类型变量T增加了约束,要求类型必须实现了 Comparable 接口,如果传递的类型没有实现这个接口,编译器将产生错误,限定类型可以配置多个,使用"&"分割,实例如下:
T extends Comparable & Serializable
- 泛型类型擦除
虚拟机没有泛型类型对象,因此无论何时定义一个泛型类型,都自动提供一个相应的原始类型(raw type),原始类型的名字就是删除类型参数后的泛型类型名,擦除类型变量,并替换为限定类型,如果没有限定类型则使用 Object,例如前面的 Pair<T> 类,原始类型如下:
public class Pair
{
private Object first;
public void setFirst(Object first){…}
public Object getFirst(){…}
}
如果增加了限定名,比如 public class Pair<T extends Comparable> 则其原始类型使用第一个限定类型替换,原始类型如下:
public class Pair
{
private Comparable first;
public void setFirst(Comparable first){…}
public Comparable getFirst(){…}
}
如果有多个限定类型,则编译器会在必要的时候进行强制转换,因此,为了提高效率,应该将没有方法的接口放在后面。
- 返回类型的类型擦除
但程序调用泛型方法时,如果擦除返回类型,编译器会插入强制类型转换,例如如下代码:
Pair<Employee> pairObj = …;
Employee empl = pairObj.getFirst();
编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法 Pair.getFirst 的调用
- 将返回的 Object 类型强制转换为 Employee 类型。
Java泛型转换规则总结如下:
- 虚拟机中没有泛型,只有普通类和方法
- 所有类型参数都用他们的限定类型替换
- 桥方法被合成来保持多态
- 为保持类型安全性,必要时插入强制类型转换。
- 约束与局限性
- 不能用基本类型实例化类型参数
不能用类型参数代替基本类型,因此没有Pair<double> 只有 Pair<Double>,原因是类型擦除,类型擦除后其原始类型为 Pair,类型参数被 Object 替换,而 Object 不能存储 double 值,因此基本类型只能使用独立的类或方法处理。
- 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类,因此所有类型查询只产生原始类型,原因是类型擦除。
- 不能创建参数化类型的数组
不能实例化参数化类型的数组,原因是类型擦除,数组会记住他的元素类型,如果试图存储其他类型的元素,就会抛出 ArrayStoreException 的异常,因此,可以声明类型为 Pair<String>[] 的变量,但不能用 new Pair<String>[10] 初始化这个变量,如果需要使用泛型类型数组,可以使用 ArrayList<Pair<String>> 。
- 不能实例化类型变量
不能使用像 new T(…),new T[…] 或 T.class 这样的表达式中的类型变量,但是可以使用反射调用 Class.newInstance 方法来构造泛型对象,示例代码如下:
public static <T, U> Pair<T, U> makePair(Class<T> cl, Class<U> cu) {
Pair<T, U> pair = new Pair<T, U>();
try {
pair.setFirst(cl.newInstance());
pair.setSecond(cu.newInstance());
return pair;
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
- 泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量,因为类型擦除后,是剩下原始类型,因此禁止使用带有类型变量的静态域和方法。
- 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象,泛型类扩展 Throwable 都是不合法的,无法通过编译。
注意类型擦除后的冲突
当泛型类型被擦除后,会导致方法冲突,例如代码如下:
Public class Pair<T>{
public boolean equals(T value){…}
}
改泛型方法类型擦除后,与 Object.equals(Object)发生冲突,因此解决方法是重新命名引发错误的方法;泛型规范说明还提到另外一个原则,同一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一个额接口的不同参数化,例如代码如下,该代码是错误的:
class Calendar implements Comparable<Calendar>{…}
class GregorianCalendar extends Calendar Implements Comparable<GregorianCalendar>{…}
- 通配符类型
带有超类限定的通配符可以向泛型对象写入,带有子类限定的通配符可以从泛型对象读取,说明如下:
- 通配符的子类限定
固定类型的泛型类型系统,在某些是否使用并不合适,因此Java系统增加了通配符类型,实例如下:
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是 Employee 的子类,如 Pair<Manager>,但不能是 Pair<String>,假设要编写一个打印雇员的方法,代码如下:
public static void printBuddies(Pair<Employee> p){…}
不能将 Pair<Manager>传递给这个方法,要解决这个问题可以使用通配符类型,代码如下:
public static void printBuddies(Pair<? extends Employee> p){…}
使用子类限定通配符将不可能调用 setter 方法,但使用 getter 方法是不存在问题的。
- 通配符的超类(基类)限定
通配符还可以指定超类型限定,如下所示:
Pair<? super Manager>
这个通配符限制为 Manager的所有超类型(基类),带有超类型限定的通配符行为,可以为方法提供参数,但不能使用返回值。