你不知道的Java泛型

作者: 李龙杰 | 来源:发表于2017-11-12 21:58 被阅读272次

    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泛型

        本文链接:https://www.haomeiwen.com/subject/tivomxtx.html