泛型实现参数化类型的概念,使代码可以应用于多种类型,解除类或方法与所使用的类型之间的约束。在 JDK 1.5 开始引入了泛型,但 Java 实现泛型的方式与 C++ 或 C# 差异很大。在平常写代码用到泛型时,仿佛一切都来得如此理所当然。但其实 Java 泛型还是有挺多 tricky 的东西的,编译器在背后为我们做了很多事。下面我们来看看有关 Java 泛型容易忽视的点。
什么是协变?举个例子。
- class Fruit {}
- class Apple extends Fruit {}
- Fruit[] fruit = new Apple[10]; // OK
子类数组可以赋给父类数组的引用。但泛型是不支持这种协变的。
- ArrayList flist = new ArrayList(); // 无法通过编译
但我们可以使用通配符来解决
- ArrayList flist = new ArrayList(); // 使用通配符解决协变问题
- List flist = Arrays.asList(new Apple());
- Apple a = (Apple) flist.get(0); // No warning
- flist.contains(new Apple()); // Argument is 'Object'
- flist.indexOf(new Apple()); // Argument is 'Object'
- //flist.add(new Apple()); 无法编译
表示某种特定类型 (Fruit 或者其子类) 的 List,但是编译器并不关心(不知道)这个实际的具体类型到底是什么。值得注意的是,这并不意味着这个 List 可以持有 Fruit 的任意类型! 由于 List 的具体类型是并不确定的,而且 Java 泛型是不支持协变的,因此带有泛型类型参数的方法都无法正常调用。比如
- List<? extends Fruit>
,即使是传 Object 也无法通过编译。 但对于返回类型是泛型的方法,比如
- add(T item);
,返回值类型与上界类型一样。如上面示例代码调用的
- T get(int index);
返回值就是 Fruit 类型的。
- flist.get(0)
- static voidadd(ListsuperApple> list) {// list.add(new Fruit()); // 无法编译Object object = list.get(0);// pass}
代码中的
表明 list 持有的类型是 Apple 的父类类型,但与上界通配符类似,这并不意味 list 可以持有 Apple 任意的子类类型的对象,编译器并不知道 list 具体的类型是什么。因此,
- List<? super Apple> list
就不能编译了。
- list.add(new Fruit());
表示
- List<?> list
是持有某种特定类型的 List,但是不知道具体是哪种类型。而单独的
- list
,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是
- List list
。
- Object
所谓的擦除,仅仅是对方法的 Code 属性中的字节码(也就是方法内的逻辑代码)进行擦除,实际上元数据(类和接口的声明,类字段的声明)中还是保留了泛型信息。 引用 R 大的话就是: 位于声明一侧的,源码里写了什么到运行时就能看到什么; 位于使用一侧的,源码里写什么到运行时都没了。
- public class GenericClass<T> { // 1
- privateList list; // 2
- privateMap map; // 3
- public U genericMethod(Map m) { // 4 List list = newArrayList<>();// 5
- return null;
- }
- }
上面的代码中,注释 1 到注释 4 的 T 和 U 是保留在 Class 文件当中的,源码是什么,那么通过反射获取得到的就是什么。也就是说,在运行时,是无法获取到具体的 T 和 U 是什么类型的。
但运行时,在方法内部的局部变量的泛型信息是被全部擦除的。如上的注释 5 中的 list 的具体类型是无法在运行时获取到的。
当时今日头条的面试官问过我这个问题,我当时对泛型的认识比较浅薄,以为编译器会将所有的泛型信息擦除,那么运行时也就无能获取到具体的泛型类型了。但其实并不是这样,如上面介绍到,JDK1.5 之后,Class 的格式有变化,编译器会将声明的类,接口,方法的泛型信息保留到字节码当中。那么通过反射,这些信息还是可以获取到的。但要获取到具体的泛型类型,一般也只能获取到继承父类所使用的泛型类型。 比如:
- public class SubClass extends Base<String> {}
那么 Base 所绑定的泛型类型可以被获取到的。对 SubClass.class 调用
可以获取到 T 所绑定的类型。
- getGenericSuperclass
- Type type = SubClass.class.getGenericSuperclass();
- Type targ = ((ParameterizedType) type).getActualTypeArguments()[0];
- System.out.println(type); // SubClass<java.lang.String>
- System.out.println(targ); // class java.lang.String
具体的用法可以参考 Gson 和 Guice 的源码:
为了使 Java 的泛型方法生成的字节码与 1.5 以前的字节码相兼容,由编译期自己生成的方法。顾名思义,桥方法是一座桥,沟通着泛型与多态。
可以通过
方法来判断一个方法是否是桥接方法,在字节码中桥接方法会被标记为
- Method.isBridge()
和
- ACC_BRIDGE
。
- ACC_SYNTHETIC
- public class Fruit<T> {T value;publicTgetValue() {returnvalue;
- }
- }public class Apple extends Fruit<String> {
- @Override
- publicStringgetValue() {return "foo was call";
- }
- }
反编译生成的字节码:
- public class Apple extends Fruit<java.lang.String> {
- publicApple();
- Code:0: aload_01: invokespecial#1 // Method Fruit."<init>":()V
- 4:return
- publicjava.lang.String getValue();
- Code:0: ldc#2 // String calling
- 2: areturnpublicjava.lang.Object getValue();
- Code:0: aload_01: invokevirtual#3 // Method getValue:()Ljava/lang/String;
- 4: areturn
- }
编译器为我们自动生成了有一个桥方法,这个桥方法返回类型为 Object,内部调用了我们自定义的另一个 getValue 方法。
在 Java 代码中,方法的特征签名只包括方法名称,参数顺序和参数类型,而字节码中的特征签名还包括方法返回值和受查异常表。因此,桥方法
与
- public Object getValue()
是可以被 JVM 区分而在同一个 Class 文件中共存的。
- public String getValue()
由于编译期泛型擦除机制,在父类中带泛型参数的方法会被替换成 Object 类型。要让子类重写父类带泛型参数的方法,需要通过桥方法直接复写父类的方法,然后桥方法再调用子类自定义的方法,就以上面作为例子,子类 Apple 中的桥方法
直接 override 父类 Fruit 的
- public Object getValue()
,然后桥方法内部再调用子类 Apple 的
- public Object getValue()
。因此,Java 利用桥方法在保证多态机制不被破坏情况下实现了泛型。
- public String getValue()
来源: http://blog.csdn.net/tellh/article/details/71308245