1. 为什么我们需要泛型
现实世界中我们经常遇到这样一种情况,同一个算法/数据结构适用于多种数据类型,我们不想为每一种类型单独写一个实现。举个例子来说,我们有一个Pair类型,存储key、value两个字段,代码如下。如果有一天,我想存储Integer类型的key, Date类型的Value,这个时候,我需要重新定义一个Pair类型。
public class Pair {
private String key;
private String value;
}
public class PairIntDate {
private Integer key;
private Date value;
}
存储的需求是多种多样的,慢慢地我们会有一大堆PairXxxYyy类型,重复定义的Class会大量冗余。于是人们想到了第二个方案,用Object引用存储,使用的时候再做强制类型转换。
public static class Pair {
public Object key;
public Object value;
}
使用的时候,写入可以直接赋值,读取的时候强制类型转换。Java的ArrayList就是这么做的,通过维护一个Object[] elementData实现数据的存储。
Pair p = new Pair();
p.key = "stringKey";
p.key = Integer.valueOf(1);
String key = (String) p.key;
这种方法带来了两个问题:
- 读取时要强制类型转换
- 没有编译检查,能写入任意类型数据,导致读取时失败
Java 5开始引入的泛型就是为了解决这个问题的,使用泛型时,我们可以这样定义Pair并使用,完美的解决了上面两个问题。
public class RawArray {
public static class Pair<K, V> {
private K key;
private V value;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
public static void main(String[] args) {
Pair<String, Integer> pair = new Pair<>();
pair.setKey("pairKey");
pair.setValue(Integer.valueOf(1));
Integer localValue = pair.getValue();
}
}
2. 泛型的实现原理
2.1 类型擦除
JVM内并没有泛型这个概念,所有的类型都是普通的类。以上面的RawArray为例,我们看看Java内部是怎么做的。通过javap命令查看class文件的字节码,命令如下:
javap -verbose RawArray$Pair.class
javap -verbose RawArray.class
Pair类的字节码如图,实际存在的字段key、value都是Object类型,getKey方法实际返回值类型是Object,setKey的入参实际类型也是Object类型,这就是我们经常说的类型擦除,泛型会擦除到定义的上界。
下面我们看看main方法里是如何使用Pair对象的,调用setKey方法时的类型检测是编译器行为,在字节码中并没有体现。getValue方法返回的是Object类型对象,编译器插入了checkcast命令替我们完成类类型转换。
2.2 桥接方法
从上一节学习的Pair字节码我们知道,Pair类的setKey方法的参数、getKey方法的返回值都被擦除为Object类型。如果我们继承Pair, 并重写setKey、getKey方法。
public static class ExtendPair extends Pair<String, Integer> {
public String getKey() {
return super.getKey();
}
public void setKey(String key) {
super.setKey(key);
}
}
回忆一下Java的基础知识, Java的方法签名指的是方法名和参数列表,父类Pair有两个方法:
- Object getKey()
- void setKey(Object)
子类ExtendPair重写/新增了两个方法
- String getKey()
- void setKey(String key)
两个setKey的方法签名不同,实际是两个重载方法,而getKey方法签名相同,返回值却不同,Java语言规范内是不允许的。如果我将ExtendPair向上转型为Pair并调用getKey和getValue方法会怎么样呢?通过阅读ExtendPair的字节码,我们找到了答案。
Java语言规范中不允许同时定义String getKey()、Object getKey()两个签名相同的方法,但是字节码层面是允许的,通过ExtendPair的字节码,生成的Object getKey()会调用String getKey()方法,这个方法被称为桥接方法,向上转型为Pair后,实际调用的是Object getKey()方法,签名和父类完全一致,然后由它转发个String getKey()方法。
setKey的桥接方法实现和getKey如出一辙,这里就不详细讲解了,有兴趣可以看看对应的字节码。值得一提的是,在类继承时提到的协变返回类型也是通过桥接方法实现的。
3. Java泛型的定义
3.1 泛型方法
之前的案例里我们已经看到泛型类( RawArray$Pair)的定义了,泛型类的类型参数不能用到静态方法上。我们来看一下泛型方法的定义,类型参数放到修饰词之后,返回值之前,看示例的of方法。通常调用泛型方法时我们不需要明确地知道类型参数,编译器自己能通过入参/返回值接收对象的引用推断出类型参数。
public static class Pair<K, V> {
private K key;
private V value;
public static <T,S> Pair<T,S> of(T k, S v) {
Pair<T,S> r = new Pair<>();
r.key = k;
r.value = v;
return r;
}
}
通过这么使用我们定义的泛型方法
var aPair = Pair.of("pairKey",1);
System.out.println(aPair.getKey());
3.2 类型限定
讲Java泛型的原理的时候,我们讲过Java会将泛型的类型擦除,最后存储的是Object类型的引用。Java并不支持动态语言里的Duking Type,这个时候如果我们想调用Pair对象里key、value对象上的方法,我们只能调用Object上定义的方法。如果我们想调用指定类型下的方法,就需要用到类型限定。
我们通过一个例子来看,假设我们有一个Range类,接收两个值来表示一段区间,这两个值必须支持比较(Comparable),同时我们希望在较小的实例上执行某些操作(包装在Runnbale中),代码如下
public static class Range<T extends Comparable<T> & Runnable> {
private T min;
private T max;
public static <S extends Comparable<S> & Runnable> Range<S> of(S s1, S s2) {
if (s1.compareTo(s2) > 0) {
S temp = s1;
s1 = s2;
s2 = temp;
}
s1.run();
Range<S> range = new Range<>();
range.min = s1;
range.max = s2;
return range;
}
}
示例中的S extends Comparable<S> & Runnable就是我们说的类型限定,如果有多个限定类型用&号分隔,Java允许类继承一个父类多个接口,指定类型限定的时候,通过extends关键字,后面接一个0或1个父类,0或N个接口,如果有的话,父类限定要放到最前面。
可以看到Comparable.compareTo方法、Runnable.run方法都可以直接在泛型方法内部调用,虽然还是要求用接口做类型限定,比起Duking Type还略有缺憾,但应该说已经不错了。
3.3 通配符类型
假设我们有3个类,Animal表示动物,Dog是Animal的子类,Cage是一个泛型类,可以持有一个对象的引用,代码如下
public static class Animal {
}
public static class Dog extends Animal {
}
public static class Cage<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
各个类直接的关系如下图,Animal和Dog有继承关系,Cage和Cage并没有,最右侧的一列我们稍后再讲。
如果我们一个方法delivery用于投递Cage,如果我们使用Cage调用这个方法,编译器提示Required Cage, Provide Cage
private static void delivery(Cage<Animal> cage)
使用通配符可以解决这个问题,Cage表示持有某个Animal的子类,这个类型可以是Animal,也可以是Dog
private static void delivery(Cage<? extends Animal> cage)
除了子类型限定以外,还有超类型限定,无限定通配符
类型 | 语法 | 简记 | 描述 |
子类限定 | Cage | 读 | 能调Cage.getT方法,用Animal接受返回值;不能调Cage.setT,T是继承Animal的类,不知道确切类型 |
超类限定 | Cage | 写 | 能调Cage.setT方法,用Animal子类做入参;不能调Cage.getT,T是Animal的超类,不知道确切类型 |
无限定通配符 | Cage | 读Object | 能调Cage.getT方法,用Object接收返回值;不能调Cage.setT,不知道T的确切类型 |
4. Java泛型的局限
4.1 类型检查只能用于原始类型
由于类型擦除的原因Pair和Pair都会将类型参数擦除到Object,所以这两个泛型类型的实例变量的Class实际是相同的
Pair<Integer,Integer> pi = new Pair<>();
Pair<Long,Long> pl = new Pair<>();
System.out.println(pi.getClass() == pl.getClass());
调用instanceof时,下面两个判断都会发true,而对pi instanceof Pair检查时,会报编译错误。
if (pi instanceof Pair<Integer, Integer>) {
System.out.println("Pair<Integer,Integer>");
}
if (pi instanceof Pair) {
System.out.println("Pair");
}
4.2 不能参数化类型数组
不能创建泛型类型的数组,数组会存储元素的类型,通过Class.getComponentType()获取数组元素的类型,Class.getComponentType()只能元素的原始类型,运行时无法做到阻止往Pair[]的数组中添加Pair的元素。但是编译器允许定义泛型数组的变量,定义一个Pair的数组后强制类型转换为泛型数组。
Java不允许我们创建泛型对象的数组,可变参数数组的时候这个限制略有放松,允许我们定义泛型的可变参数列表,下面这段代码是允许的
private void varArgs(Pair<String, Integer>... ps) {
// code goes here
}
4.3 无法实例化类型变量
因为类型擦除的作用,我们无法通过T.class引用到Class对象,当然更无法通过T.class创建对象的实例。早期我们通过传入Class clazz来完成实例的创建。Java 8之后可以通过函数式接口来创建。
public static class Holder<T> {
public T makeT() {
return T.class.newInstance(); // 无法正常允许,不能通过T.class因为到Class对象
}
public T makeT(Class<T> clazz) throws Exception {
return clazz.getConstructor().newInstance();
}
public T makeT(Supplier<T> supplier) {
return supplier.get();
}
}
泛型数组的创建也可以采用类似的方法,传入Class clazz或者传入一个函数式接口来创建
public static class Holder<T> {
public T[] makeT(Class<T> clazz) {
return (T[]) Array.newInstance(clazz, 10);
}
public T[] makeT(IntFunction<T[]> make) {
return make.apply(10);
}
}
// 调用方式
Holder<String> ss = new Holder<>();
System.out.println(Arrays.toString(ss.makeT(String.class)));
System.out.println(Arrays.toString(ss.makeT(String[]::new)));
4.4 不能定义/抛出/捕获泛型异常
继承自Throwable的类不能是泛型类型,catch的括号里不能用类型变量。下面的throwsAs是CoreJava里的一个示例,通过欺骗编译器允许方法不声明检查型异常。
public static <T extends Throwable> void throwsAs(Throwable t) throws T {
throw (T) t;
}
public T makeT(Class<T> clazz) {
try {
return clazz.getConstructor().newInstance();
} catch (Throwable t) {
GenInstance.<RuntimeException>throwsAs(t);
return null;
}
}
5. Java泛型的反射
Java反射的信息最终来自.class文件,而在编译的时候我们并不知道类会怎么被使用。假设我们定义了Company>类,使用时Company、Company引用的是同一个Class对象。为了方便讲解,我们定义如下的测试类
public static class KeyNiuTech extends Company<Date> {
}
public static class Company<T extends Comparable<? super T>> {
private T[] staff;
private T aStaff;
public static <O extends Comparable<? super O>> Company<O> of(O o) {
Company<O> c = new Company<>();
c.aStaff = o;
return c;
}
}
通过查看这两个的字节码可以看到,这两个能拿到的信息的上限,Company能拿到类型参数T以及T的上限Comparable,以及Comparable的类型参数。KeyNiuTech里更有意思一点,KeyNiuTech继承自Company,这里的这个类型Date也被保存了,记不记得我们解析JSON时经常需要传递一个TypeReference的匿名子类?
Java提供了5种类型来支持泛型的泛型,类型之间的关系如下图
我们来看一下每种实现类分别代表着哪种类型的数据
类型 | 说明 | 举例 |
Class | 具体类型 | KeyNiuTech.class、Company.class |
TypeVariable | 类型变量 | T extends Comparable |
WildcardType | 通配符 | ? super T |
ParameterizedType | 泛型类 | Company、Comparable |
GenericeArrayType | 泛型数组 | Company.staff通过Field获取后,读getGenericeType()返回的就是这个类型 |
最后一个示例结尾,这段代码用于打印类的定义信息
private static StringBuilder classDetail(Class clazz) {
StringBuilder sb = new StringBuilder();
int modifier = clazz.getModifiers();
sb.append(Modifier.toString(modifier)).append(" ");
sb.append(clazz.getName());
TypeVariable[] tvs = clazz.getTypeParameters();
if (tvs != null && tvs.length > 0) {
sb.append("<");
for (TypeVariable tv : tvs) {
sb.append(tv.getName());
Type[] bounds = tv.getBounds();
if (bounds != null && bounds.length > 0) {
sb.append(" extends ");
for (Type bound : bounds) {
sb.append(bound.getTypeName());
}
}
}
sb.append(">");
}
Type superClass = clazz.getGenericSuperclass();
if (superClass instanceof Class) {
sb.append(" extends ");
sb.append(superClass.getTypeName());
} else if (superClass instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) superClass;
sb.append(" extends ");
sb.append(pt.getRawType().getTypeName());
sb.append("<");
Type[] actualTypes = pt.getActualTypeArguments();
for (Type at : actualTypes) {
sb.append(at.getTypeName());
}
sb.append(">");
}
Type[] superInterfaces = clazz.getGenericInterfaces();
for (Type ifs : superInterfaces) {
}
return sb;
}
输出示例
public static com.company.generic.GenericReflect$KeyNiuTech extends com.company.generic.GenericReflect$Company<java.util.Date>