深入理解泛型
为什么要使用泛型
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。
在JDK1.5之前,ArrayList类只维护一个Object引用的数组:
public class ArrayList {
private Object[] elementDate;
. . .
public Object get(int i) { . . . }
public void add(Object o) { . . . }
. . .
}
不用泛型有什么问题
- 可以向数组中添加任意对象。本来我的ArrayList装载的全部都是String对象,但不小心把Integer对象放进去,编译器不会报错,运行时会出现ClassCastException异常。
- 获取一个对象时必须进行强制类型转换。放任意对象进去都会上转型为Object类型,获取到的也是Object对象,需要强制转换为对应类型。
ArrayList<String> list = new ArrayList<>();
用了泛型有什么好处
- 可读性:当使用了泛型之后,人们一看就知道这个数组列表中包含的是String对象。
- 安全性:编译器知道列表中只能插入String对象,错误类型的对象是无法通过编译的。出现编译错误比在运行时出现ClassCastException异常好得多。
简单泛型类
public class Box<T>{
private T any;
public T getAny(){
return any;
}
public void setAny(T any){
this.any = any;
}
}
Box类引入了一个类型变量 T ,用尖括号( <> ) 括起来,并放到类名后面。泛型类可以有多个类型变量。
public class HashMap<K,V>{}
类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合中的元素类型,K和V分别表示表的关键字与值得类型。T(需要时还可以用字母U和S)表示"任意类型"
泛型方法
如果只需要某一个方法使用泛型,还可以定义一个带有类型参数的简单方法。
public class ArrayAlg {
public static <T> T getMiddle(T ... arr){
return arr[arr.length/2];
}
public <T> int getHashcode(T o){
return o.hashCode();
}
}
这个方法是在普通类中定义的,而不是在泛型类中定义的。注意:类型变量放在返回类型的前面。
调用一个泛型方法时,在方法名前面的尖括号中放入具体的类型:
String s = ArrayAlg.<String>getMiddle("111","222","33333");
int hashcode = new ArrayAlg().<String>getHashcode("aaa");
在大多数情况下,方法调用可以省略<String>
类型参数。
类型变量的限定
需求:获得数组中的最小元素
public <T> T getMin(T ... a){
if(a == null || a.length == 0){
return null;
}
T temp = a[0];
for(int i = 1;i< a.length;i++){
//这里编译无法通过
if (temp.compareTo(a[i]) > 0){
temp = a[i];
}
}
return temp;
}
问题:
变量 temp 为 T ,这意味着它可以是任意类的对象。怎么才能确信 T 所属的类有compareTo 方法呢?
解决:
将 T 限制为实现了Comparable接口(public interface Comparable<T> {public int compareTo(T o);}
)的类。可以通过对类型变量 T 设置限定实现这一点:
public <T extends Comparable> T getMin(T ... a) . . .
小结:
- 无论变量需要限定为继承某个类或者实现某个接口,都是使用extends 关键字进行限定
- T 为 Comparable 对象或其子类对象
- 这样就保证了T类所属的类有 compareTo 方法
一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
理解:类似数学上的求交集,则T 即是A的子类又是B的子类
限定类型(T extends Comparable & Serializable
)用 “&” 分隔,而逗号用来分隔类型变量(HashMap<K,V>
)
注意:在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
类型擦除
编译器会擦除类型变量,并替换为限定类型
public class Interval<T extends Comparable & Serializable> implements Serializable{
private T lower;
private T upper;
. . .
public Interval(T first,T second){
if(first.compareTo(second) <= 0){
lower = first;
upper = second;
}else{
lower = second;
upper = first;
}
}
}
public class Interval implements Serializable{
//擦除了类型变量 “ T ”,替换为限定类型 Comparable
private Comparable lower;
private Comparable upper;
. . .
public Interval(Comparable first,Comparable second){. . .}
}
擦除规则
- 没有限定类型用Object替换
- 有一个限定类型用限定类型替换
- 有多个限定类型用第一个限定类型替换
若切换限定:
<T extends Serializable & Comparable>
会发生什么?如果这样做,原始类型用Serializable替换T,而编译器在必要时向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界的末尾。
问题:
- 为什么不直接使用类型变量来替换,而要使用限制变量?
- 假如程序中有很多类似
List<String>
、List<Integer>
、List<Double>
的代码,如果直接用类型变量来替换,那么会产生很多List(个人猜测形如:List_String、List_Integer、List_Double)的字节码文件,会占用大量内存。
-
如果有多个限定类型那么编译器用第一个限定类型替换后会不会出现属于A确不属于B的情况?
- 不会,因为在编译器已经确定下来了T是A、B的交集,这里类型替换其实用什么都是可以的(甚至可以使用Object),然后编译器会在有需要的时候插入强制类型转换
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
例如:
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0);
当调用String str = list.get(0);
编译器做的事:
- 对原生方法List.get的调用(
Object obj = list.get(0);
) - 将返回的Object类型强制转换为String类型(
String str = (String)obj;
)
翻译泛型方法
类型擦除带来了两个复杂的问题。
/**
获得 List<T> 中最小元素和最大元素
*/
// 原类型
public class Pair<T> {
private T first;
private T second;
public Pair() {
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
class StringInterval extends Pair<String>{
public void setSecond(String str){
System.out.println("hello");
super.setSecond(str);
}
}
class Main{
public static void main(String[] args){
Pair p = new StringInterval();
p.setSecond("hi"); //hello
}
}
这里通过上转型对象调用setSecond(String)方法
细想一下
由于会进行类型擦除,Pair 类(父类)擦除之后setSecond方法就会变成
public void setSecond(Object second) {
this.second = second;
}
StringInterval类(子类)的setSecond方法是
public void setSecond(String second) {
super.setSecond(str);
}
通过 Pair(父类)对象引用调用StringInterval(子类)对象的setSecond方法。
Pair p = new StringInterval();
p.setSecond("hi");
这里就出现问题了:现在父类为setSecond(Object)方法,子类为setSecond(String)方法,然后通过上转型对象只能调用到父类的setSecond(Object),而子类没有setSecond(Object)方法只有setSecond(String)方法
类库的设计这为了解决这个问题引入了桥方法(bridge method)的概念
也就是编译器在StringInterval(子类)中生产一个桥方法
//建立父类与子类直接的桥梁
public void setSecond(Object o){//父类可以通过上转型对象调用到
this.setSecond((String)o);//子类将其转到子类对应的方法中
}
这样即使类型擦除后父类的setSecond(Object)也会被子类重写,进而调用子类的setSecond(String)方法。
桥方法可能会变得很奇怪,假设StringInterval也覆盖了getSecond方法
class StringInterval extends Pair<String>{ public String getSecond(){ System.out.println("hello"); return super.setSecond(str); } . . . }
在StringInterval类中,有两个getSecond方法:
String getSecond() //子类自己的
Object getSecond() //编译器添加的桥方法
不能这样编写Java代码(方法名相同,参数类型相同的两个方法是无法通过编译的),但是,在虚拟机中,用参数类型和返回值确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机可以处理这种情况。
总结:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都用他们的限定类型替换
- 桥方法被合成来保持多态
- 为保持类型安全性,必要时插入强制类型转换
约束与局限性
泛型是一个编译期概念
在虚拟机中的所有对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
例如:
-
a instanceof Pair<String> // Error
-
Pair<String> sPair = ... Pair<Employee> ePair = ... if(sPair.getClass() == ePair.getClass()) //true
不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Pair<String> table = new Pair<String>[10]; //Error
这有什么问题呢?擦除之后,table的类型是Pair[]
。
如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常table[0] = "hello"; // Error
。
不过对于泛型类型,擦除会使这种机制无效。如table[0] = new Pair<Employee>();
能够通过编译检查,但运行时可能会出现ClassCastException
只是不允许创建这些数组,而声明类型为Pair<String>[]
的变量还是合法的。不过不能用new Pair<String>[10]
初始化这个变量。
可以声明通配类型的数组,然后进行类型转换:Pair
table = (Pair [])new Pair<?>[10]; 结果将是不安全的。如果在table[0]中存储一个Pair
,然后对table[0].getFirst()调用一个String方法,会得到一个ClassCastException异常。
如果需要收集参数化类型对象,只有一种安全而有效的方法:ArrayList:ArrayList<Pair
>。
Varargs警告
向参数可变的方法传递一个泛型类型的实例。它的参数个数是可变的:
public static <T> void addAll(Collection<T> coll,T ... ts){
for(t:ts) coll.add(t);
}
实际上参数ts是一个数组,包含提供的所有实参。
现在考虑一下调用:
Collection<Pair<String>> table = . . .;
Pair<String> p1 = ...;
Pair<String> p2 = ...;
addAll(table,p1,p2);
为了调用这个方法,Java虚拟机必须建立一个Pair
可以采用增加注解(@SuppressWarnings("unchecked") 或 @SafeVarargs)的方式来抑制这个警告。
@SafeVarargs static <E> E[] array(E ... array){return array;}
// 现在可以调用:
Pair<String>[] table = array(pair1,pair2);
// 这看起来很方便,不过会出现前面的问题。以下代码
Object[] o = table;
o[0] = new Pair<Employee>();
// 能顺利编译,但运行时可能会出现ClassCastException异常。
不能实例化类型变量
不能使用像 new T(...),new T[...] 或T.class这样的表达式中的类型常量。
public Pair(){
first = new T();second = new T(); //非法
}
类型擦除后将T改成了Object,而且,本意肯定不希望调用new Object()。
解决方案:
方法一:
//Java8之后,最好的解决办法是通过构造器表达式:
Pair<String> p = Pair.makePair(String::new);
//makePair 方法接受一个Supplier<T>,这是一个函数式接口
public static <T> Pair<T> makePair(Supplier<T> c){
return new Pair<>(c.get(),c.get());
}
@FunctionalInterface // 函数式接口
public interface Supplier<T> {
T get(); // 返回一个T类型的实例,每次都不一样
}
方法二:
// 通过反射调用Class.newInstance方法来构造泛型对象
// 不能直接调用
first = T.class.newInstance(); // Error
public static <T> Pair<T> makePair(Class<T> cl){
try{
return new Pair<>(cl.newInstance(),cl.newInstance());
}catch(Exception ex){
return null;
}
}
//这个方法可以按照下列方式调用
Pair<String> p = pair.makePair(String.class);
不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。
// class ArrayAlg
public <T extends Comparable> T[] minmax(T... a){
T[] mm = new T[2];//Error
...
}
类型擦除会让这个方法永远构造 Comparable[2] 数组。
假设 T 为 String String[] ss = ArrayAlg.minmax("a","b","c");
而 Java 中无法把Comparable[] 引用转换为 String[] 引用
解决方案:
方法一:
// 让用户提供一个数组构造器表达式
String[] ss = ArrayAlg.minmax(String[]::new,"a","b","c");
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr,T... a){
T[] mm = constr.apply(2);
}
@FunctionalInterface
public interface IntFunction<R> {// 接受一个指定类型
R apply(int value);// 创建一个指定长度的数组
}
方法二:
// 利用反射,调用Array.newInstance
public static <T extends Comparable> T[] minmax(T... a){
T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(),2);
. . .
}
泛型类的静态上下文类型变量无效
// 非泛型类可以通过编译
public class Singleton {
private static Object singleton;
public static <T> T getSingleton(){
return (T)singleton;
}
}
// 泛型类无法通过编译
public class Singleton<T> {
private static Object singleton;
public static T getSingleton(){//Error
return (T)singleton;
}
}
不能抛出或捕获泛型类的实例
- 既不能抛出也不能捕获泛型类对象
- 泛型类不能扩展Throwable
- 使用类型变量是合法的
- catch 子句中不能使用类型变量
public class Problem<T> Exception{}// Error-- 不能继承Throwable
public static <T extends Throwable> void doWork(Class<T> t){// ok
try{
}catch(T e){// catch中不能使用类型变量
}
}
可以消除对受查异常的检查
可以将一个受查异常包装为一个非受查异常
public abstract class Block {
public abstract void body() throws Exception;
public Thread toThread(){
return new Thread(){
public void run(){
try {
body();
} catch (Exception e) {
// 将异常转为非受检查异常
Block.<RuntimeException>throwAs(e);// 这里有一个非受查异常不用处理
}
}
};
}
public static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T)t;
}
}
public class BlockTest {
public static void main(String[] args) {
new Block(){
public void body() throws Exception {
Scanner in = new Scanner(new File("hsy"));
while (in.hasNext())
System.out.println(in.next());
}
}.toThread().start();
}
}
注意擦除后的冲突
在Pair类中添加equals方法
public class Pair<T>{
public boolean equals(T value){
return first.equals(value) && second.equals(value);
}
}
考虑Pair<String>
。从概念上讲,它有两个 equals 方法:
boolean equals(String) // Pair<String>
boolean equals(Object) // Object
但是,其实他经过擦除后就只有一个即boolean equals(Object)
方法,与Object.equals方法冲突。补救方法是重新命名引发错误的方法。
要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。
例如:
class Employee implements Comparable<Employee>{ . . . }
class Manger extends Employee implements Comparable<Manager>{} // Error
泛型类型的继承规则
无论 S 与 T 有什么联系,通常 Pair<S>
与 Pair<T>
没什么联系。例如:Manager 继承 Employee 但 Pair
Pair<Manager> manager = new Pair<>(ceo,cfo);
Pair<Employee> employee = manger;// 非法,这里假设可以
employee.setFirst(lowly);
现在将CFO和一个普通员工组成一对,这对于Pair
泛型类可以扩展或实现其他泛型类。就这一点和普通类没有什么区别。例如,ArrayList
!
通配符类型
前面说了无法将 Pair<Manage>
对象赋值给 Pair<Employee>
,这是不允许的,为了解决这个问题 Java 的设计者发明了一种巧妙的(依然是安全的)“解决方案”:通配符类型。
通配符类型中,允许类型参数变化。例如,通配符类型
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair
Pair<Manager> manager = new Pair<>(ceo,cfo);
Pair<? extends Employee> wildcard = manager; // ok
wildcard.setFirst(lowly); // 无法通过编译 下面会说
这里前两行代码可以类比 一个Father 类有两个子类,然后用父类的指针指向不同的子类
public class Father { } public class Son1 extends Father { } public class Son2 extends Father { }
Father father = new Son1(); father = new Son2(); // ok
对 setFirst 的调用有一个类型错误。仔细看一看类型 Pair<? extends Employee>
。其方法似乎是这样的:
? extends Employee getFirst()
void setFirst(? extends Employee)
这里不能调用 setFirst 方法。编译器只知道需要某个Employee 的子类型,但不知道具体是什么类型。
Pair<Manager> manager = new Pair<>(ceo,cfo);
Pair<? extends Employee> wildcard = manager; // ok
wildcard.setFirst(lowly);// 这里wildcard为Pair<Manager>的上转型对象,如果能调用setFirst方法的话,有可能会传入非Pair<Manager>对象但是wildcard对象的子类对象,这样会出现安全问题,所以这里直接不能调用。
可以调用 getFirst 方法,编译器可以这返回一个Employee 的引用,这完全是合理的。有需要可以强转为对于的子类。
通配符的超类限定
可以指定一个超类型限定(supertype bound),如下所示:
? super Manager
带有超类型限定的通配符的行为与上面的相反。可以为方法提供参数,但不能使用返回值。例如,Pair<? super Manager>
有方法
void setFirst(? super Manager)
? super Manager getFirst()
public void f(Manager[] a,Pair<? super Manager> result){
result.setFirst(a[0]); // 这里 result 为 a[0] 类(或其父类)的对象,当父类对象确定下来之后,对其引用传入一个子类对象完全是合法的
}
// 假设具体的父类为Employee
public void f(Manager[] a,Pair<Employee> result){
result.setFirst(a[0]); // 这里Employee.set(Manager)完全是合法的
}
// 假设具体的父类为Employee
public void f(Employee[] a,Pair<? super Manager> result){
result.setFirst(a[0]); // Error -- 无法通过编译,在编译器无法确定result中的元素类型,并且编译器无法确定Manager是Employee的唯一子类,假设Admin也是Employee的子类,有可能出现把admin放在Manager中,这是不允许的
}
- 带有超类型限定的通配符可以向对象写入
- 带有子类型限定的通配符可以从对象读取
无限定通配符
还可以使用无限定通配符,例如,Pair<?>
。初看起来,这好像与原始的Pair类型一样。实际上,有很大的不同。类型 Pair<?>
有一下方法:
? getFirst()
void setFirst(?)
- getFirst 的返回值只能赋给一个Object
- setFirst 方法不能被调用,甚至不能被Object调用
Pair<?> 和 Pair 本质的不同在于:可以用任意Object对象调用原始 Pair 类的 setObject 方法
为什么要使用这样脆弱的类型?他对于许多简单的操作非常有用。例如,下面这个方法将用来测试一个pair是否包含一个null引用,他不需要实际的类型。
public static boolean hasNulls(Pair<?> p){
return p.getFirst() == null || p.getSecond() == null;
}
// 通过将hasNulls转换成泛型方法,可以避免使用通配符类型:
public static <T> boolean hasNulls(Pair<T> p)
这里,带有通配符的版本可读性更强。
通配符捕获
现有一个交换成对元素的方法:
public static void swap(Pair<?> p)
通配符不是类型变量,因此,不能在编写代码中使用“?” 作为一种类型。也就是说,下述代码是非法的:
? t = p.getFirst();// Error
交换的时候必须临时保存第一个元素。我们可以写一个辅助方法swapHelper,如下所示:
public static <T> void swapHelper(Pair<T> p){ // 将?(T、E、U、S)捕获为具体的 T(此T非彼T)
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
public static void swap(Pair<?> p){swapHelper(p);} // ? 可以是 T、E、U、S
编译器必须能够确信通配符表达的是单个的,确定的类型。例如,ArrayList<Pair<T>>
中的T永远不能捕获ArrayList<Pair<?>>
中的通配符。数组列表中可能有Pair<E>
、Pair<U>
、Pair<S>
等不同的类型。
强化理解
现有一个动物园,里面有很多动物,有Animal、Dog等等。先需要在动物中选出一位领导者,年龄最大的为领导者。
//版本一:以<T extends Comparable>为例
//测试类
public class TestMain{
public static void main(String[] args){
List<Comparable> animals = new ArrayList<>();
animals.add(new Dog(6));
animals.add(new Animal(4));
animals.add("hello");//可以通过编译,String也实现了Comparable
findLeader(animals);
}
public <T extends Comparable> void findLeader(List<T> arr){// 这里传入的参数为Comparable的子类
if(arr.size() == 0) return;
int index = 0;
for(int i =0;i<arr.size();i++){
if( arr.get(i).compareTo(arr.get(index)) > 0 ){
index = i;
}
}
System.out.println("年龄最大的是 "+ arr.get(index));
}
}
public class Animal implements Comparable{
private int age;
public Animal(){
}
public Animal(int age){
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Object o) {
Animal a = (Animal) o;
return this.getAge() - a.getAge();
}
}
public class Dog extends Animal {
public Dog() {
}
public Dog(int age) {
super(age);
}
}
animals.add("hello")
这里编译没有问题,因为String也实现了Comparable,但是运行会报ClassCastException,这里前三种动物会调用自己的compareTo方法通过age来进行比较,当比较到字符串时,字符串无法转换为任意的动物类,也没有age属性,所以就会抛出异常
//版本二:以<T extends Comparable<T>>为例
//测试类
public class TestMain{
public static void main(String[] args){
List<Animal> animals = new ArrayList<>();// Animal 为 Comparable<Animal>
animals.add(new Dog(6));
animals.add(new Animal(4));
animals.add("hello");// Error String不是Animal也就是Comparable<Animal>的子类
findLeader(animals);
}
public <T extends Comparable<T>> void findLeader(List<T> arr){// T 为 Comparable<T>的子类
if(arr.size() == 0) return;
int index = 0;
for(int i =0;i<arr.size();i++){
if( arr.get(i).compareTo(arr.get(index)) > 0 ){
index = i;
}
}
System.out.println("年龄最大的是 "+ arr.get(index));
}
}
public class Animal implements Comparable<Animal>{
private int age;
public Animal(){
}
public Animal(int age){
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Animal o) {//这里不用像上面那样强制类型转换
return this.getAge() - o.getAge();
}
}
public class Dog extends Animal {
public Dog() {
}
public Dog(int age) {
super(age);
}
}
animals.add("hello")
这里编译无法通过,这里为了避免版本一的问题,进一步将类型变量限制为Comparable<Animal>
的子类,提高了安全性。
如果传入一个List<Dog>
会怎样?
public static void main(String[] args){
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog(2));
dogs.add(new Dog(3));
dogs.add(new Dog(5));
findLeader(dogs);// Error
}
findLeader(dogs)
无法通过编译,<T extends Comparable<T>>
表示T为Comparable<T>
的子类。
Animal 是 Comparable<Animal>
的子类,Dog也是Comparable<Animal>
的子类,但Dog不是Comparable<Dog>
的子类,所以无法通过编译
// 版本三:以 <T extends Comparable<? super T>>为例
//测试类
public class TestMain{
public static void main(String[] args){
List<Animal> animals = new ArrayList<>();
animals.add(new Dog(6));
animals.add(new Animal(4));
findLeader(animals);// ok
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog(2));
dogs.add(new Dog(3));
dogs.add(new Dog(5));
findLeader(dogs);// ok
}
public <T extends Comparable<? super T>> void findLeader(List<T> arr){// T 为 Comparable<? super T>的子类
if(arr.size() == 0) return;
int index = 0;
for(int i =0;i<arr.size();i++){
if( arr.get(i).compareTo(arr.get(index)) > 0 ){
index = i;
}
}
System.out.println("年龄最大的是 "+ arr.get(index));
}
}
//其他的类一样
通过<T extends Comparable<? super T>>
Animal 是 Comparable<? super Animal>
的子类,Dag 是 Comparable<? super Dag>
的子类,所以无论是Dag类还是Animal类都可以通过编译
参考:Java核心技术卷Ⅰ