Java泛型小记

  一直以来对Java泛型都处于一知半解的状态,趁着最近细读Java编程思想读到泛型章节,做个笔记备忘。

一、伪泛型
  Java的主要涉及灵感来自于C++,很多地方都有相似之处。但是在泛型(C++里面的模板)的实现方式上却有较大的差异。导致差异的根本原因在于Java5之前Java不支持泛型,而要做到前后兼容必须做出妥协,找出一个折中的方式——type erasure(类型擦除)。类型擦除的意思是参数化类型只存在于编译期,编译通过后,在之后生成的Java字节码中是不包含泛型中的类型信息的。
  比如在代码中定义的ArrayList和ArrayList等类型,在编译后都会变成ArrayList。JVM看到的只是ArrayList,而由泛型具体的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

ArrayList a = new ArrayList<>();
ArrayList b = new ArrayList<>();
System.out.println(a.getClass());
System.out.println(b.getClass());
System.out.println(a.getClass() == b.getClass());
/**
 output:
 class java.util.ArrayList
 class java.util.ArrayList
 true
 */

  在编译时,一旦编译器确认泛型类型是安全使用的,就会将它转换为原始类型。还是以ArrayList为例:

ArrayList<String> list= new ArrayList<String>();
list.add("泛型");
//list.add(1);  编译报错
String str = list.get(0);

可以看到编译器会发现于泛型类型不符的非法操作,在正确性验证通过后,以上代码会被翻译成如下代码:

ArrayList  list= new ArrayList();
list.add("泛型");
String str =String)(list.get(0));

list是ArrayList类的实例,而不是ArrayList的。另外,注意到从list中去除第一个值加上了强制转换,强转的类型也即是我们定义的泛型类型。在引入泛型之前,这本应是我们程序猿做的事,因为在ArrayList里面是一个Object数组来存储我们放进去的对象引用,所以拿出来需要我们强转之后再进行接下来的具体操作。引入泛型之后,只要我们定义了泛型类型,编译器就会为我做这个工作,这样还能规避很多非法转型。
  前面提到,Java之所以采用“伪泛型”很大一部分原因是因为JDK1.5之前压根就没有泛型概念,为了向后兼容代码库而不得不采取的一种折中办法。假如在以前的代码库中有这样一个方法:

public void func(List list){
    //do sonmething
}

JDK1.5之后容器类都用泛型重新编写,你的代码里大多数都是List之类的定义,不然编译器会给你个警告表达他的不满。如果没有泛型擦除,那么像List将是不同于List的全新类型,由于List和List没有直接的继承关系,所以也没法强制转型,所以你想向上面的那个方法传递你的List就不太可能了。在1.5之前已经有太多的代码库,显然全部用泛型重新写一遍是不太现实的事,所以就把所有泛型类都在编译期统一到原始类型,这样就可以愉快地使用1.5之前的代码库了。


二、忙碌的编译器
  上述的擦除、转型之类的工作其实都是编译器一个人在忙碌,但就是因为编译器做的工作有些繁杂,导致刚接触泛型的童鞋迷失在这些工作里面。这里先上Java编程思想里面的一句话,个人觉得精髓至极:

在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。记住,“边界就是发生动作的地方”。

所以,总的来说,编译器对泛型类主要有以下两个工作:

  1. 在泛型类和其方法内部进行擦除,将参数类型擦除到第一个边界处;
  2. 在泛型类的方法调用处进行传递参数的类型检查和返回值的转型;

对于第一条举几个简单的栗子:

package blog.xu;

class A{}
interface B{}
interface C{}

public class GenericType {
    T value;

    public void set(T value){
        this.value = value;
    }

    public T get(){
        return value;
    }
}

借助命令javap -c GenericType.class可以看到反编译后的字节码:

public class blog.xu.GenericType {
  T value;

  public blog.xu.GenericType();
    Code:
       0: aload_0
       1: invokespecial #12     // Method java/lang/Object."":()V
       4: return

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #23     // Field value:Ljava/lang/Object;
       5: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #23     // Field value:Ljava/lang/Object;
       4: areturn
}

可以看到在set和get方法中的value类型都变成了Object,所以,在没有限定类型边界的情况下会统一擦除到Object。而泛型边界则复用了Java关键字extends,现将类的定义改为:

public class GenericType<T extends A&B&C>

可得到如下代码:

public class blog.xu.GenericType<T extends blog.xu.A & blog.xu.B & blog.xu.C> {
  T value;

  public blog.xu.GenericType();
    Code:
       0: aload_0
       1: invokespecial #12    // Method java/lang/Object."":()V
       4: return

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #23    // Field value:Lblog/xu/A;
       5: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #23    // Field value:Lblog/xu/A;
       4: areturn
}

可以看到value的类型变为class A,而这里的A恰好是第一个边界。这里需要注意的地方是如果边界中存在类而不是接口,则必须将类放在第一个,否则会报错。

你可能感兴趣的:(Java)