你不知道的Java泛型

Java泛型是JDK1.5引入的新特性.如果用一句话总结泛型的作用,就是类型参数化.

为什么要引入泛型

在JDK1.5之前,如果你使用集合类,代码大致是这样的

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("bob");
        list.add("jack");
        list.add(123);
        for (int i = 0; i < list.size(); i++) {
            System.out.println((String)list.get(i));
        }
    }

这段代码编译没有任何错误.但是一执行就会抛ClassCastException.

我们总结一下这段代码存在的问题:

  1. List中存放的数据无法规范
  2. 编译期无法检查出此类问题.而到运行期发现再去找bug成本很高

为了解决这个问题.JDK1.5中引入了泛型的概念.

泛型的引入

到了JDK1.5之后,代码就成了这个样子.

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("bob");
        list.add("jack");
        list.add(123);
        for (int i = 0; i < list.size(); i++) {
            System.out.println((list.get(i));
        }
    }

这段代码在编译期就已经提示我们不能往list里放入123.
通过引入泛型,JDK为我们解决了之前代码存在的问题.

  1. 我们可以通过泛型规范集合中的元素类型.
  2. 在编译期间就检查出语法错误.

一切看起来很美好.

泛型的擦除

由于泛型是JDK在1.5才提供的功能.JDK作为一个软件,在升级的过程中,要做向下兼容以保证低版本升级到高版本的成本尽可能的小.

这也就导致Java的泛型是在编译器这个层面来实现的.在生成Java字节码层面是不存在泛型的类型的.

这也就是说.不存在List<String>.class和List<Integer>.class.而是只有List.class.如何证明这个事情呢.我们做几个实验.

类型比较

Code:

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        List<Integer> list2 = new ArrayList<Integer>();
        System.out.println(list.getClass() == list2.getClass());
    }

这段代码的执行结果是true.

看起来似乎可以证明,只有List.class.而没有List<String>.class和List<Integer>.class.

但是还是觉得不够通透.我们继续下一个实验.

反射

如果泛型在运行时并不存在,则List的add方法在运行时的方法签名应该和JDK1.5之前保持一致.

boolean add(Object e);

而如果泛型在运行时存在,则方法签名会类似于:

boolean add(String e);

Java的反射可以在在运行时获取,操作类的方法.所以我们只需要看能否获取到指定签名的Method对象就知道在运行时是否存在该方法.

Code:

    public static void main(String[] args) throws NoSuchMethodException {
        List<String> list = new ArrayList<>();
        Method method = list.getClass().getMethod("add", String.class);
        System.out.println(method);
    }

Console output:

Exception in thread "main" java.lang.NoSuchMethodException:
java.util.ArrayList.add(java.lang.String)
    at java.lang.Class.getMethod(Class.java:1786)

List中没有add(String e)方法.继续测试:

Code:

    public static void main(String[] args) throws NoSuchMethodException {
        List<String> list = new ArrayList<>();
        Method method = list.getClass().getMethod("add", Object.class);
        System.out.println(method);
    }

Console output:

public boolean java.util.ArrayList.add(java.lang.Object)

代码执行正常.在运行时我们找到了add(Object e)方法.

到了这个层面,基本可以确定在运行时,泛型确实被擦除了.但是这个分析过程看起来有点曲线救国的感觉.我们能不能有一个一针见血的方法来证明Java在运行时是没有泛型的呢.

Java指令代码

既然运行时没泛型.那好.我们去看一下编译后的指令代码不就可以了么.

先写一个方法:

import java.util.ArrayList;
import java.util.List;

public class DemoClass {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("aaa");
        System.out.println(list);
    }
}

我们先生成这个类的class文件

javac DemoClass.java

然后通过javap命令生成Java指令代码

javap -verbose DemoClass

然后我们得到了一段代码.为了方便阅读.省略了前面大部分.

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String aaa
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        20: aload_1
        21: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        24: return
      LineNumberTable:
        line 9: 0
        line 10: 8
        line 11: 17
        line 12: 24
}

我们看这个main方法Code部分的11:

11: invokeinterface #5, 2
// InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

看到了吧.调用的方法签名中的参数是Object.不是String.

到这里我们已经完全可以确定Java泛型是编译器层面的解决方法.而不是运行时.

泛型类

泛型除了用在集合中,我们也可以自定义泛型类.

Code:

public class TestClass<T> {
    private T data;
    public TestClass(T data) {
        this.data = data;
    }
    public void setData(T data) {
        this.data = data;
    }
    public T getData() {
        return data;
    }
}

我们再写一段测试代码:
Code:

        TestClass<String> testClass = new TestClass<>("bob");
        String name = testClass.getData();
        System.out.println(name);

Console output:

bob

到这里我们有一个问题.泛型在运行时已经被擦除.

String name = testClass.getData();

在运行时返回的应该是Object类型.但是我们却可以直接赋值给String类型.这是为什么.为了搞清楚这个问题.依旧可以去看一下Java的指令码.

我们依旧只看一小段关键部分:

11: invokevirtual #6                  // Method getData:()Ljava/lang/Object;
14: checkcast     #7                  // class java/lang/String
17: astore_2

当我们在执行getData之后,并没有直接进行astore操作.而是有一个checkcast指令.
关于这个指令的描述是:Check whether object is of given type

从这个字面我们可以看出这其实是一个检查类型的指令.但是这个解释并没有说明它的完整功能.
我们可以通过简单测试发现.这个指令是在强制类型转换的时候出现.如果类型可以转则通过.如果类型转换失败.则会抛出ClassCastException.关于这个指令可以自行测试.

到这里我们就可以知道,之所以Object可以直接赋值给String.是JVM帮我们做了强转.

泛型擦除带来的问题

类型丢失

由于泛型在运行时被擦除.所以也就无法在运行时对泛型的类型进行操作.

  1. 无法对泛型进行类型判断
  1. 无法根据T生成对象


泛型与多态

直接看代码
ParentClass

public class ParentClass<T> {
    public void print(T t) {
        System.out.println("parentClass");
        System.out.println(t);
    }
}

ChildClass

public class ChildClass extends ParentClass<String> {
    @Override
    public void print(String s) {
        System.out.println("childClass");
        System.out.println(s);
    }
}

先看测试代码:

  public static void main(String[] args) {
        ParentClass<String> childClass = new ChildClass();
        childClass.print("aaa");
    }

它的输出是

childClass
aaa

可以看到,符合我们对Java运行时绑定的预期.
但是这里有个问题.由于运行时没有泛型.所以父类的print方法签名应该是

public void print(Object t);

而我们的子类里的print方法签名是

public void print(String s);

根据Java对方法重写的定义,要求的是方法签名完全一致.
而我们的代码里其实并没有跟父类完全一样的方法签名.
所以根据动态绑定的原理,应该是调用父类的print(Object t)方法而不是子类的print(String s)

为了搞清楚这个问题,我们需要去看一下ChildClass的指令码.

我们根据指令码可以看到.ChildClass里有两个print方法.

一个和父类相同,print(Object).而另一个和子类中定义的相同.print(String).
而在print(Object)中调用了print(String).

到这里我们就明白了.实际上在这种涉及泛型的多态中,jvm给我们隐式的生成了一个方法(一般叫做桥方法)来达到动态绑定的目的.

参考资料

Java泛型的学习和使用
Java深度历险(五)——Java泛型
Oracle JVM指令解释

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,295评论 18 399
  • 前面,由于对泛型擦除的思考,引出了对Java-Type体系的学习。本篇,就让我们继续对“泛型”进行研究: JDK1...
    贾博岩阅读 5,094评论 3 28
  • 朦胧初醒,遥望漆黑的夜空,没有星星,没有月亮,只有那凌晨5点的思念, 或许明日会更好,或许平凡的事才...
    澄雨落yan阅读 371评论 0 2
  • 脉歌GT100S 女毒 http://money.163.com/15/0401/12/AM47NHSU00253...
    Daseinyang阅读 224评论 0 0
  • 01 前两天,我写了一篇关于早起的文章《想每天早起,做到这点就够了》,其中有一个读者给我留言: 可是在宿舍怎么办呢...
    遇见唐姑娘阅读 1,063评论 42 25