Java泛型食用笔记(二) -- 类型擦除

Java泛型食用笔记(二) -- 类型擦除


在使用别人已经创建的泛型类时,你可能会感觉到泛型给你带来的诸多方便。但当你真正自己需要去实现一个泛型类,也许你会遇到许多令人惊讶的问题。理解 Java 泛型的实现有助于我们认识 Java 泛型的局限,以免浪费时间在无法实现的死胡同里。

1. 类型擦除的魔法

先来看一段代码

public class GenericTest01 {

    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();

        System.out.println(c1 == c2);
    }
}

output:

true

如果你没有了解 Java 泛型的原理,可能对运行结果有些疑惑。在之前使用的过程中,我们发现 ArrayList<String>ArrayList<Integer> 行为是不一样的,比如你往 ArrayList<String> 中放入 Integer 是不合法的,这些表现很容易让人认为二者是不同的类,然而冰冷的结果却证明这两个类是同一个类。

Java 的泛型是通过 擦除 来实现的,事实上,ArrayList<String>ArrayList<Integer> 类都被擦除称为原生类 ArrayList。这也就意味着,在泛型代码内部,无法获取任何有关泛型参数的信息,比如你无法知道你的参数类型有那些成员和构造函数等。

Java 采用擦除的方式来实现泛型是一种折中。在 JDK5 出现前,Java 已经广泛应用了,而 Java 一直强调二进制向后兼容,也就是低版本 JVM 上能正常运行的 Class 文件,在高版本的 JVM 上也能正常运行,擦除使这种兼容性成为可能,采用擦除后编译出的字节码几乎没有变化,这就保证的之前的二进制文件也能在支持泛型的 Java 版本中运行。

你只能在静态类型检查期间感觉到泛型类型的存在,而在运行时,所有的泛型类型都被替换为上界类型。例如 List<T> 擦除为 List,所有 T 都被替换为 Object。

2. 擦除的痕迹

在来看一段代码

public class GenericTest02<T> {               
    public List<T> list;                       
    public Map<String, T> map;                 
      
    public <U> U genericMethod(Map<T, U> m) { 
        return null;  
    }  

    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println(GenericTest02.class.getField("list").toGenericString());
        System.out.println(GenericTest02.class.getField("map").toGenericString());
    }
}

output:

public java.util.List<T> GenericTest02.list
public java.util.Map<java.lang.String, T> GenericTest02.map

在经过之前 Java 的泛型通过擦除实现的洗脑之后,看到这个输出又会再次陷入疑惑。这个时候为什么能拿到类型参数的具体类型呢,不应该所有类型信息都已经擦除了吗。

我们来看下这段代码的字节码,(JDK8 下用 java -p -s -c 生成):

public class GenericTest02<T> {
  public java.util.List<T> list;
    descriptor: Ljava/util/List;

  public java.util.Map<java.lang.String, T> map;
    descriptor: Ljava/util/Map;

  public <U> U genericMethod(java.util.Map<T, U>);
    descriptor: (Ljava/util/Map;)Ljava/lang/Object;
    
    
    ...

}

JDK5 之后,字节码中虽然进行了类型擦除,但还保留了类型参数的信息,只是这里保留的是源码里写的类型参数信息,例如你用的 <T> 保留的就是 T,并不是保留运行时的实际类型。

按照 R大 的描述,Java 的泛型规律是:

  • 位于声明一侧,源码里写了什么运行时就能看到什么
  • 位于使用一侧,源码里写了什么运行时都丢失了

所谓声明一侧包括,泛型类型(泛型类与泛型接口)声明、带有泛型参数的方法和域的声明,这些信息在 class 文件中都有保留。这些信息的保留原因我没有很确定的答案,如果有高手能知道依据麻烦告知一下,我初步猜想是在序列化和反序列化的时候可能可以用上。

但在使用一侧,泛型类型的信息都没有保留,我们看一个例子

public class GenericTest03 { 
    public static <U> void genericMethod(U m) {   
        List<U> list2 = new ArrayList<U>();
        return;  
    }  

    public static void main(String[] args) throws NoSuchFieldException {
        GenericTest03.genericMethod("test");
    }
}

字节码:

public class GenericTest03 {

  public static <U> void genericMethod(U);
    descriptor: (Ljava/lang/Object;)V
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: return

  public static void main(java.lang.String[]) throws java.lang.NoSuchFieldException;
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: ldc           #4                  // String test
       2: invokestatic  #5                  // Method genericMethod:(Ljava/lang/Object;)V
       5: return
       
       ...
       
}

可以看到,方法体内泛型局部变量,泛型方法的调用的泛型信息编译后都完全擦除了。

上述讨论可以看出,声明一侧会保留源码里使用的类型参数,例如声明 List<T>, 只记录了 T,但并没有地方记录 T 本身的实际类型。因此对带有未绑定的泛型变量的泛型类型获取其实际类型是不现实的。同时使用一侧不会保留任何泛型的类型信息,因此即使你使用时绑定了具体类型,任然无法获取泛型类型信息。

3. 案发地点

擦除移除了方法体内的类型信息,因此在边界上,即进入方法体时和离开方法体的地点,编译器进行了类型检查和类型转换操作。

再来一段代码:

public class GenericTest04<T> {
    private T obj;
    public T get() {
        return this.obj;
    }

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

    public static void main(String[] args) {
        GenericTest04<String> holder = new GenericTest04<>();
        holder.set("test");
        String s = holder.get();
    }
}

字节码:

public class GenericTest04<T> {
  private T obj;
    descriptor: Ljava/lang/Object;

    ...

  public T get();
    descriptor: ()Ljava/lang/Object;
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: new           #3                  // class GenericTest04
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String test
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
}

类型检查在字节码里没有表现,因为这是编译器在编译期间的工作;编译器在方法结束后,插入了类型转换的操作。即 main 函数中的第 18 行 checkcast 指令。

小结

本章我们讨论了泛型的实现是一种编译器擦除的魔术。并通过字节码分析了编译器擦除的信息及保留的信息。在掌握这些原理和基础后,就可以理解一些 Java 泛型的局限,比如,“拿不到 T 的实际类型”,“不能对 T 类型使用 instanceof”等问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容