Java 泛型本质是参数化类型,可以用在类、接口和方法的创建中。
1 “擦除式”泛型
Java的“擦除式”的泛型实现一直受到开发者的诟病。
“擦除式”的实现几乎只需要在Javac编译器上做出改进即可,不要改动字节码、虚拟机,也保证了以前没有使用泛型的库可以之间运行在java 5.0 之上。但是这个带来了以下弊端:
- 例如对于List<String> 和 List<Integer> ,在运行时由于擦除了,所以这两个都变成了List,因此它们在运行中是同一类型。(而对于C#的泛型来说,无论在源码中、编译后及运行时它们始终是不同的类型)。这导致在运行时,获取不到类型信息。
- “擦除式”的实现,是在元素被赋值时编译器自动插入类型检查指令,访问元素时,自动插入类型强制转换指令。这样频繁的类型检查及转换,导致Java的泛型性能差于C#的泛型。
public class EraseGeneric {
private static class Holder<T> {
T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.setT("hello");
String str = holder.getT();
}
}
图 Holder类被编译后的字节码片段
图 main 方法中,对于Holder类型的赋值及访问操作字节码
1.1 擦除的补偿
如果要在运行时获取类型信息,那么可以通过引入类型标签来对擦除进行补偿。
public class TypeTagGeneric {
private static class User {
public User() {
}
}
private static <T> void fun(Class<T> kind) throws InstantiationException, IllegalAccessException {
T t = kind.newInstance();
System.out.println(t);
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
fun(User.class);
int[] array1 = new int[10];
String[] array2 = new String[10];
}
}
2 协变与逆变
A 类型是B的父类型,对于某个构造器,构造出的复杂类型A`与B`。
协变 | A`仍然是B`的父类型。比如Java中的数组,A[] 仍是B[]的父类型。 |
逆变 | B`是A`的父类型。 |
抗变 | A`与B`没有任何继承关系。例如List<A> 与List<B>没有任何继承关系。 |
表 协变、逆变与抗变
2.1 数组与泛型
T[] t = new T[10]; 这个代码是错误的,Java中规定不能创建泛型数组。
因为Java 在运行时,无法获取泛型的类型信息,因为在创建数组时,也就无法获取到泛型参数所表示的确切类型。
2.1.1 数组的类型
Java中数组的种类有两种:
- 基础类型的数组:[ + 开头大写字母。
int[] : [I
- 引用类型的数组:[ + L + 类型。
String[] array2 : [Ljava/lang/String
public class ArrayGeneric {
private static class Fruit {}
private static class Apple extends Fruit {}
public static void main(String[] args) {
Fruit[] fruits = new Fruit[10];
Apple[] apples = new Apple[10];
System.out.println(fruits instanceof Fruit[]); // true
System.out.println(fruits instanceof Apple[]); // false
System.out.println(apples instanceof Fruit[]); // true
System.out.println(apples instanceof Apple[]); // true
fruits = apples;
// apples = fruits; // 编译错误
System.out.println(fruits.getClass().getSuperclass()); // class java.lang.Object
System.out.println(apples.getClass().getSuperclass()); // class java.lang.Object
// getSuperclass() 方法:如果此 Class 表示 Object 类、一个接口、一个基本类型或 void,则返回 null。
// 如果此对象表示一个数组类,则返回表示该 Object 类的 Class 对象。否则返回该类的超类。
}
}
2.2 通配符
泛型中的通配符用于在两个类型之间建立某种类型的向上转型关系。
协变 | ? extends T, 例如List<? extends Fruit> list。确定了元素类型的父类为Fruit,但不能确定其确切类型,因此不能往该容器添加新的元素(只能添加null)。但是可以从容器中提取元素,类型为Fruit。 |
逆变 | ? super T,例如List<? super Apple> list,确定了元素为Apple的父类,因为可以往容器中添加元素,但不能提取元素。 |
表 通配符的协变与逆变
public class CovarianceAndContravariance {
private static class Fruit {}
private static class Apple extends Fruit {}
private static <T extends Apple> void setItem(List<? super Apple> list, T item) {
list.add(item);
}
private static Fruit getItem(List<? extends Fruit> list,int pos) {
return list.get(pos);
}
public static void main(String[] args) {
List<? super Apple> list = new ArrayList<>();
setItem(list,new Apple());
List<? extends Fruit> list2 = Arrays.asList(new Fruit(),new Apple());
Fruit item = getItem(list2, 0);
}
}
2.2.1 无界通配符
无界通配符,例如List<?>, 其相当于List<? extends Object>,但不等价于List(相当于List<Object>)。其有两个作用:
- 告诉编译器,我用了泛型,只是还没确定哪个类型;
- 用于捕获类型。
public class CaptureGeneric {
private static class Holder<T> {}
private static <T> void fun1(Holder<T> holder) {
System.out.println(holder);
}
private static void fun2(Holder<?> holder) {
fun1(holder);
}
public static void main(String[] args) {
Holder holder = new Holder(); // 原生类型
fun1(holder); // 警告,Unchecked assignment:
fun2(holder); // 不会警告,无边界通配符将不会这个原生类型的类型参数(Object)
}
}