Java泛型小记

来源:互联网 发布:wps如何描述型数据 编辑:程序博客网 时间:2024/06/02 06:59

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

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

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

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

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


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

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

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

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

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

package blog.xu;class A{}interface B{}interface C{}public class GenericType<T> {    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> {  T value;  public blog.xu.GenericType();    Code:       0: aload_0       1: invokespecial #12     // Method java/lang/Object."<init>":()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."<init>":()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恰好是第一个边界。这里需要注意的地方是如果边界中存在类而不是接口,则必须将类放在第一个,否则会报错。

原创粉丝点击