1 定义泛型类和泛型方法
1.1 泛型类
一个泛型类(generic class)即具有一个或多个类型变量的类,在类名后面引入一个<>括起来的类型变量列表后,就可以将类中方法的返回类型、域或局部变量的类型指定为<>中的类型变量。
public class Pair<T,U>
{
private T first;
private U second;
public Pair(T fisrt, U second) {this.first = first; this.second = second;}
...
}
1.2 泛型方法
在普通类中也可以定义带有类型参数的泛型方法,类型变量放在修饰符之后,返回类型之前,用<>括起。
class ArrayAlg
{
public static <T> T getMiddle(T...a) {return a[a.length / 2];}
}
当在调用泛型方法时,在方法名前的<>中放入具体的类型。
String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");
大多数情况下也可以省略<String>类型参数,编译器可以根据实参列表的类型推断出类型参数的具体类型。但当实参列表中存在不同类型的参数时,编译器可能会提示错误,比如getMiddle(145.06,1,2),编译器会给出错误消息(参数自动装箱为一个Double对象和两个Integer对象,其共同超类为Number和Comparable接口也是泛型类型,这样就无法得到泛型方法的类型变量的具体类型),可以实参全部改为double类型。
2 类型变量的限定与类型擦除机制
2.1 类型变量限定
有时需要对类型变量进行约束,比如某泛型方法中涉及到对类型变量类对象进行比较,需要使实参传进来的对象所属类实现了Comparable接口,可以在定义该泛型方法时,将其类型变量限定为“必须实现了Comparable接口”。
class ArrayAlg
{
public static <T extends Comparable> T min(T[] a)
{
...
}
}
这里用extends关键字,其实应该表示了一种“子类型(subtype)”的概念,T的绑定类型可以是类或接口,但是通通使用extends关键字(因为设计者懒得再搞一个新的关键字表示子类型这个概念了)。
2.2 类型擦除
虚拟机中没有泛型类型对象,所有的对象都属于普通类,无论何时定义一个泛型类型,都会自动提供一个相应的原始类型,即擦除了类型变量,将其替换了限定类型(或者Object类)。
比如1.1中的Pair<T,U>在类型擦除后,其类型变量被替换为Object。
public class Pair
{
private Object first;
private Object second;
public Pair(Object fisrt, Object second) {this.first = first; this.second = second;}
...
}
原始类型用第一个限定的类型变量替换,如果没有给定限定,则用Object替换。
2.2.1 类型擦除中编译器进行的强制类型转换
当程序调用了泛型方法,在虚拟机中擦除了返回类型,那么编译器将插入强制类型转换。
Pair<Employee> buddies = ...;
Employee buddy_first = buddies.getFisrt();
getFirst应该返回Employee类型,但是在虚拟机中将getFirst的返回类型擦除为Object类型了,于是编译器在将buddies.getFirst返回的Object对象赋给buddy_first之前,将其强制转换为Employee类。
2.2.2 编译器生成桥方法避免类型擦除与多态发生冲突
类型擦除和多态很有可能会发生冲突,例如:
public class Child extends Parent<String>
{
public void hello_world(String v)
{
System.out.println("We're in Class Child: " + v);
}
}
Child继承自类型变量被指定为String的泛型类Parent,在泛型Parent类中实现了对应的泛型函数:
public class Parent <T>
{
public void hello_world(T v)
{
System.out.println("We're in Class Parent: " + v);
}
}
当企图实现多态时:
public class Main
{
public static void main(String[] args)
{
Child child = new Child();
Parent<String> parent = child;
parent.hello_world("Hello World!");
}
}
这里对于parent.hello_world的调用希望实现多态,调用最合适的方法,而parent引用了Child类的对象,所以应该调用Child.hello_world函数(多态性)。
重点在于:这时在虚拟机中Parent的hello_world(String v)方法经过类型擦除变为了:
public void hello_world(Object v)了,而Child类中并没有重写参数类型为Object的hello_world方法,但是实际上却可以运行成功。
这是因为编译器在Child类中生成了一个桥方法(bridge method):
public void hello_world(Object v)
{
hello_world((String) v); // 强制类型转换,调用Child类中参数类型为String的hello_world方法
}
这个桥方法实际上就相当于Child类对于Parent中hello_world(Object v)的重写。而Child类中的hello_world(String v)本意上是对超类中hello_world的重写,但是由于发生了类型擦除,导致重写失败,即类型擦除与多态性之间发生了冲突,桥方法就是编译器为了解决这种冲突而添加的机制。
3 Java泛型中存在的约束与局限性
3.1 不能使用基本类型实例化类型参数
比如,对于泛型类Pair<T>,不能将其实例化为Pair<int>,应该实例化为Pair<Integer>。这一点限制是类型擦除产生的,发生类型擦除之后,Pair类中使用Object类型的域存储值,而Object无法存储一个基本类型的值。
3.2 运行时类型查询只适用于原始类型
当使用反射机制查询运行时对象的类类型时,无法获得具有特定类型变量的类信息,比如:
public class Main
{
public static void main(String[] args)
{
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass())
// true
{
...
}
}
}
这里stringPair参数类型为String,而employeePair参数类型为Employee,但是对它们getClass都将返回Pair.class。
3.3 不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10]; // error
创建参数化类型的数组的不允许的,这一点限制也是由于类型参擦而导致的。
首先来看一下ArrayStoreException:
Pair[] table = new Pair[10];
table[0] = new Object();
// 编译时出错,不能其他类型元素
Pair[] table = new Pair[10];
Object[] o = table;
o[0] = new Object();
// 编译时不会出错,但是运行时会抛出ArrayStoreException异常
也就是说,数组存储时只能存储和其创建时元素类型相同的元素,而类型擦除机制可能会破坏这一规则。
然后再试图再objarray中存储其他非Pair类型的元素,会抛出ArrayStoreException异常,而擦除会使这种机制失效:
Pair<String>[] table = new Pair<String>[10]; // 假设合法
Object[] objarray = table;
objarray[0] = new Pair<Double>;
由于发生了类型擦除,table中的类型变量被擦除为Object,这样会导致Pair<Double>类型的值存储进应该存储Pair<String>的数组中,这破坏了上面的数组存储规则,所以,不能创建参数化类型的数组。
但是,只是不允许创建这种数组,却可以声明类型为Pair<String>[]的数组,只是不能new这样的数组。另外,创建通配符的泛型数组是允许的:
Pair<?>[] table = new Pair<?>[10];
或者,可以声明参数化类型数组,在创建时new不具有参数化类型的数组:
Pair<String>[] table = new Pair[10];
ArrayList<String> list = new ArrayList();
3.4 Varargs警告
有一种情况可能会违反上面的“不能创建参数化类型数组”的规则,即向参数个数可变的方法传递泛型类型实例:
public static<T> void addAll(Collection<T> coll, T...ts)
{
for (t : ts) coll.add(t);
}
这里实际上参数ts是一个数组,包含了所有提供的实参,当调用该方法,且具有多个T类型实参时,Java虚拟机将必须建立一个参数化类型数组,比如:
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table,pair1,pair2);
这里,Java虚拟机建立了一个Pair<String>数组,这违反了前面的规则,但是对于这种情况,规则有所放松,不会得到错误信息,而是得到警告。
通过通过在语句或者整个函数前加上:
@SuppressWarnings("unchecked")
@SafeVarargs
即可抑制这种警告。
3.5 不能实例化类型变量
不能使用类似于new T(…), new T[…]或T.class这种表达式,比如:
public class Pair<T>
{
private T first;
private T second;
public Pair()
{
// wrong
first = new T();
second = new T();
}
类型擦除导致T变为Object,而new Object就非常不可取。
解决方法是,让调用者提供一个构造器表达式:
public static <T> Pair<T> makePair(Supplier<T> constr) // Supplier<T> 是一个函数式接口,表示一个无参数且返回类型为T的函数
{
return new Pair<>(constr.get(), constr.get());
}
Pair<String> p = Pair.makePair(String::new); // String::new即lambda表达式中方法引用的构造器引用,作为一个函数传递好函数式接口Supplier<T>
或者,在调用MakePair函数时提供类型实例的Class信息:
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);
3.6 不能构造泛型数组
与不能够实例化一个泛型实例一下,也不能实例化数组。虽然数组会填充null值,构造时看上去是安全的,但是数组本身也有类型,这个类型会被擦除,比如:
public static <T extends Comparable> T[] minmax(T[] a)
{
T[] mm = new T[2]; // wrong
}
类型擦除导致这个方法永远在构造Comparable[2]数组。
3.7 泛型类的静态上下文中类型变量无效
不能再静态域或方法中引用类型变量,比如:
public class Singleton<T>
{
private static T singleInstance; // wrong
public static T getSingleInstance() // wrong
{
if(singleInstance == null)
...
return singleInstance;
}
}
3.8 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象,泛型类扩展Throwable也是不合法的。
比如:
public class Problem<T> extends Exception {...} // wrong
在catch子句中也不能使用类型变量,但是在异常规范中使用类型变量是允许的。
3.9 类型擦除后的冲突
要想支持擦除的转换,就需要强行限制一个类或者类型变量不能同时成为两个接口的子类,而这两个接口是同一个接口的不同参数化。如:
class Employee implements Comparable<Employee> {...}
class Manager extends Employee implements Comparable<Manager>
Comparable<Employee>和C