你不知道的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指令解释

推荐阅读更多精彩内容

  • 前面,由于对泛型擦除的思考,引出了对Java-Type体系的学习。本篇,就让我们继续对“泛型”进行研究: JDK1...
    贾博岩阅读 3,839评论 3 27
  • 朦胧初醒,遥望漆黑的夜空,没有星星,没有月亮,只有那凌晨5点的思念, 或许明日会更好,或许平凡的事才...
    澄雨落yan阅读 147评论 0 2
  • 脉歌GT100S 女毒 http://money.163.com/15/0401/12/AM47NHSU00253...
    Daseinyang阅读 153评论 0 0
  • 01 前两天,我写了一篇关于早起的文章《想每天早起,做到这点就够了》,其中有一个读者给我留言: 可是在宿舍怎么办呢...
    遇见唐姑娘阅读 786评论 42 25
  • 我是日记星球271号星宝宝 正在参加21天蜕变之旅 我相信写作的力量 更相信坚持的力量。。。。 选择了快乐 必要承...
    徐梦琳阅读 60评论 0 0