zoukankan      html  css  js  c++  java
  • 《徐徐道来话Java》(2):泛型和数组,以及Java是如何实现泛型的

     数组和泛型容器有什么区别

      要区分数组和泛型容器的功能,这里先要理解三个概念:协变性(covariance)、逆变性(contravariance)和无关性(invariant)。

      若类A是类B的子类,则记作A ≦ B。设有变换f(),若:

           当A ≦ B时,有f(A)≦ f(B),则称变换f()具有协变性;
    
        当A ≦ B时,有f(B)≦ f(A),则称变换f()具有逆变性;
    
        如果以上两者皆不成立,则称变换f()具有无关性。

      在Java中,数组具有协变性,而泛型具有无关性,示例代码如下:

    Object[] array = new String[10];
    //编译错误 ArrayList
    <Object> list=new ArrayList<String>();

      这两句代码,数组正常编译通过,而泛型抛出了编译期错误,应用之前提出的概念对代码进行分析,可知:

    1、String ≦ Object
    
    2、数组的变换可以表达为f(A)=A[],通过之前的示例,可以得出下推论:
    
      f(String) = String[] 以及 f(Object) = Object[];
    
    4、通过代码验证,String[] ≦ Object[] 是成立的,由此可见,数组具有协变性。

      又可知:

      5、ArrayList泛型的变换可以表达为 f(A)= ArrayList<A>,得出推论:
    
        f(String) = ArrayList<String> 以及 f(Object) = ArrayList<Object>;
    
      6、通过代码验证,ArrayList<String> ≦ ArrayList<Object>不成立,由此可见,泛型具备无关性

      最终得出结论,数组具备协变性,而泛型具备无关性

      所以,为了让泛型具备协变性和逆变性,Java引入了有界泛型(参见3.1.2小节内容)概念。

      除了协变性的不同,数组还是具象化的,而泛型不是

      什么是具象化(reified,也可以称之为具体化,物化)?

      在Java语言规范》里,明确的规定了具象化类型的定义:

    完全在运行时可用的类型被称为具象化类型(refiable type),会做这种区分是因为有些类型会在编译过程中被擦除,并不是所有的类型都在运行时可用。

    它包括:

    1、非泛型类声明,接口类型声明;

    2、所有泛型参数类型为无界通配符(仅用‘?’修饰)的泛型参数类;

    3、原始类型;

    4、基本数据类型;

    5、其元素类型为具象化类型的数组;

    6、嵌套类(内部类,匿名内部类等,比如java.util.HashMap.Entry),并且嵌套过程中的每一个类都是具象化的。

      不论是在编译时还是运行时,数组都能确切的知道自己的所属的类型。但是泛型在编译时会丢失部分类型信息,在运行时,它又会被当作Object处理。

      这里要涉及到类型擦除的相关知识,会在后面详细解释。在当前,只需要知道,Java的泛型最后都被当作上界(此概念会在后面说明)处理了。

      引申:数组具备协变性,是Java的一个缺陷,因为极少有地方需要用到数组的协变性,甚至,使用数组的协变会引起不易检查的运行时异常,参见下面代码:

    Object[] array = new String[10];
    
    array[0] = 1;
    

      很明显,这会在运行期抛出异常:java.lang.ArrayStoreException。

      鉴于有如此多的不同,在Java里,数组和泛型是不能混合使用的。参见下面代码:

    List<String>[] genericListArray = new ArrayList<String>[10];
    
    T[] genericArray = new T[];
    

      它们都会在编译期抛出Cannot create a generic array错误。这是因为,数组要求类型是具象化(refied)的,而泛型恰好不是。

      换言之,数组必须清楚的知道自己内部元素的类型,并且会一直保存这个类型信息,在添加的时候元素的时候,该信息会用于做类型检查,而泛型的类型不确定。所以,在编译器层面就杜绝了这个问题。这在《Java语言规范》里有明确的说明:

    If the element type of an array were not reifiable,the virtual machine could not perform the store check described in the preceding paragraph. This is why creation of arrays of non-reifiable types is forbidden. One may declare variables of array types whose element type is not reifiable, but any attempt to assign them a value will give rise to an unchecked warning .

    如果数组的元素类型不是具象化的,虚拟机将无法应用在前面章节里描述过的存储检查。这就是为什么禁止创建(实例化)非具象化的数组。你可以定义(声明)一个元素类型是非具象化的数组类型,但任何师徒给它分配一个值的操作,都会产生一个unchecked warning。

    存储检查:这里涉及到Array的基本原理,可以自行参阅《Java语言规范》或者参考5.1.1ArrayList相关章节

     

      这不得不说,又是Java在泛型设计上的一点缺陷,为什么Java的泛型设计会有这么多缺陷呢?难道真的是Java语言不够好吗?这些内容将在3.3节泛型历史中解答。

    泛型使用建议

      泛型在Java开发和设计中占据了重要的地位,如果正确高效的使用泛型尤为重要。下面通过介绍两条使用泛型时的建议,来加深对泛型的理解:

      1、泛型类型只能是类类型,不能是基本数据类型,如果要使用基本数据类型作为泛型,应当使用其对应的包装类。比如,如果期望在List中存放整形变量,因为int是基本类型,所以不能使用List<int>,应该使用int的包装类Integer,所以正确的使用方法为List<Integer>。

      当然,泛型不支持基本数据类型,试图使用基本数据类型作为泛型的时候必须转化为包装类这点,是Java泛型设计之初的缺陷。

      2、使用到集合的时候,尽量的使用泛型集合来替代非泛型集合。一般来说,软件的开发期和维护期时间占比,也是符合二八定律的,维护期的时长能超出开发期数倍。使用了泛型的集合至少,在IDE工具上,是类型确定的,可以提高代码的可读性,并在编译期就避免一些严重的BUG。

      3、不要使用常见类名(尤其是String这种属于java.lang的)作为泛型名,会造成编译器无法区分开类和泛型,并且不会抛出异常。

    泛型擦除

     

      在学习泛型擦除之前,明确一个概念:Java的泛型不存在于运行时。这也是为什么有人说Java没有真正的泛型。

      泛型擦除(类型擦除),它是指在编译器处理带泛型定义的类接口方法时,会在字节码指令集里抹去全部泛型类型信息,被擦除后泛型,在字节码里只保留泛型的原始类型(raw type)。

      原始类型,是指抹去泛型信息后的类型,在Java中,它必须是一个引用类型(非基本数据类型),一般而言,它对应的是泛型的定义上界。

      举例:<T>中的T对应的原始泛型是Object,<T extends String>对应的原始类型就是String。

     泛型信息会在编译时擦除

     

      如何证明泛型会被擦除呢?这里提供了一段测试代码:

    class TypeErasureSample<T> {
    
    public T v1;
    
    public T v2;
    
    public String v3;
    
    }
    
     
    
    /**
    
     * 泛型擦除示例
    
     */
    
    public class Generic3_2 {
    
    public static void main(String[] args) throws Exception {
    
    TypeErasureSample<String> type = new TypeErasureSample<String>();
    
    type.v1 = "String value";
    
     
    
    // 反射设置v2的值为整型数
    
    Field v2 = TypeErasureSample.class.getDeclaredField("v2");
    
    v2.set(type, 1);
    
     
    
    for (Field f : TypeErasureSample.class.getDeclaredFields()) {
    
    System.out.println(f.getName() + ":" + f.getType());
    
    }
    
     
    
    /*
    
     * 此处会抛出java.lang.ClassCastException: java.lang.Integer cannot be cast
    
     * to java.lang.String
    
     */
    
    System.out.println(type.v2);
    
    }
    
    }

     

      程序运行结果为:

    v1:class java.lang.Object

    v2:class java.lang.Object

    v3:class java.lang.String

    Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

    at capter3.generic.Generic3_2.main(Generic3_2.java:29)

      v1和v2的类型被指定为泛型T,但是通过反射发现,它们实质上还是Object,而v3原本定义的就是String,和前两项一比对,证明反射本身并无错误。

      代码在输出type.v2的过程中抛出了类型转换异常,这说明了两件事:

      1、为v2设置整型数已经成功(可以自行写一段反射来验证);

      2、编译器在构建字节码的时候,一定做了类似于(String)type.v2的强行转换,关于这一点,可以通过反编译验证(反编译工具为jd-gui),结果如下所示:

     

    public class Generic3_2
    
    {
    
      public static void main(String[] args) throws Exception
    
      {
    
        TypeErasureSample type = new TypeErasureSample();
    
        type.v1 = "String value";
    
     
    
        Field v2 = TypeErasureSample.class.getDeclaredField("v2");
    
        v2.set(type, Integer.valueOf(1));
    
     
    
        for (Field f : TypeErasureSample.class.getDeclaredFields()) {
    
          System.out.println(f.getName() + ":" + f.getType());
    
        }
    
     
    
        System.out.println((String)type.v2);
    
      }
    
    }

     

      可以看到,如果编译器认为type.v2有被申明为String的必要的时候,都会加上(String)强行转换。可以进行测试:

      Object o = type.v2;

      String s = type .v2;

      后者会抛出类型转换异常,而前者是正常执行的。由此,可以得出结论,编译器会在构建字节码的时候,抹去一些泛型信息。

     

    编译器保留的泛型信息有哪些?

     

      上一节中介绍了编译器会擦除全部泛型信息,那么是不是所有的泛型信息都会在编译的过程中消失呢,答案是否定的字节码里指令集之外的地方,会保留部分泛型信息。下面的泛型在编译阶段是会被保留的:

      1、泛型接口、类、方法定义上的所有泛型;

      2、成员变量声明处的泛型。

      参考下面的代码:

    /**
    
     * 定义了泛型参数的接口
    
     */
    
    interface GI<T> {
    
    }
    
     
    
    /**
    
     * 定义了泛型参数并实现了泛型接口的类
    
     */
    
    class GC<T> implements GI<T> {
    
    // 两种使用了泛型的成员变量
    
    T m1;
    
    ArrayList<T> m2 = new ArrayList<T>();
    
     
    
    /**
    
     * 定义了泛型参数的方法,并在返回值、参数和异常抛出位置使用了该泛型
    
     */
    
    <K extends Exception> ArrayList<K> method(K p) throws K {
    
    // 在方法体中使用了泛型
    
    K k = p;
    
    ArrayList<K> list = new ArrayList<K>();
    
    list.add(k);
    
    return list;
    
    }
    
    }

     

      代码涵盖了泛型的各种声明和使用情况。接下来使用反编译工具看看结果,可以注意到,接口、类、方法定义的位置,大部分泛型信息依然存在,字段中使用到泛型作为声明的位置,泛型同样存在,而在所有在局部代码快对泛型做引用的位置,泛型内容消失了:

     

     

    abstract interface GI<T>{

    }

     

    class GC<T>  implements GI<T>{

      T m1;

      ArrayList<T> m2 = new ArrayList();

     

      <K extends Exception> ArrayList<K> method(K p) throws Exception{

        Exception k = p;

        ArrayList list = new ArrayList();

        list.add(k);

        return list;

      }

    }

     

      可以注意到,在之前没有提及的位置,比如GC.m2成员变量的实例化位置,method方法体里的泛型信息全部被擦除。

      为什么Java会这么设计?这也很好理解:

        1、如果不保留泛型定义,那么除非拥有源码,不然无法使用泛型。

        2、即使保留了泛型定义,定义位置的泛型信息并未初始化,也就是说,泛型参数没有绑定为特定的某个类,对使用者不具备意义。而且,泛型信息在运行时也会被处理为上界,对使用并不会有影响。

      相信注意细节的读者已经发现了,之前提及的“会被保留泛型信息的位置”里,“异常抛出位置”的K被替换为了Exception,这不正说明它被擦除了?

      事实上,如果通过反射来获取泛型信息的时候(方法将在下一小节详细讲解),会发现,依然可以得到异常的泛型信息。得出结论,作为抛出异常的泛型参数,没有消失

      这是为什么呢?

      既然反编译工具没有记录下泛型信息,只能说明某些反编译工具没有解析二进制文件里的某些信息。这些信息是什么呢?这里要引入的一个概念,方法签名(Method Signatrue)。

      下面列出的是上一个例子的部分字节码内容(也就是class文件反编译的原始内容):

      // Method descriptor #31 (Ljava/lang/Exception;)Ljava/util/ArrayList;

      // Signature: <K:Ljava/lang/Exception;>(TK;)Ljava/util/ArrayList<TK;>;^TK;

      // Stack: 1, Locals: 2

      java.util.ArrayList method(java.lang.Exception p) throws java.lang.Exception;

        0  aconst_null

        1  areturn

          Line numbers:

            [pc: 0, line: 40]

          Local variable table:

            [pc: 0, pc: 2] local: this index: 0 type: capter3.generic.GC

            [pc: 0, pc: 2] local: p index: 1 type: java.lang.Exception

          Local variable type table:

            [pc: 0, pc: 2] local: this index: 0 type: capter3.generic.GC<T>

            [pc: 0, pc: 2] local: p index: 1 type: K

     

      这段内容不长,也无需细看,如果稍微观察下,可以注意到第四行开始就是方法的定义部分,包括返回值ArrayList,参数Exception,抛出的异常Exception,注意到没有?它们,统统不带泛型信息,而在更早之前的位置(1-3行)可以看到三段注释,这就是之前所说的方法签名了

      方法签名是方法定义的一部分,它规定了方法的参数列表和返回值等信息。下面来详细解释下各个部分的概念。

    第一行:

      // Method descriptor #31 (Ljava/lang/Exception;)Ljava/util/ArrayList;

      Method descriptor是标志方法签名的开始。

      #ID是该方法的id号,在同一个方法体内不会重复。

      (参数列表)表示方法有一个Exception类型的形参,类名前的L是引用类型的标记;基础数据类型的标记是对应类型的首字母大写,比如int对应I。数组的标记是在原始标记前加上符号[,比如double[]对应[D,String[]对应[Ljava/lang/String。

      最后的位置是返回值,比如Ljava/util/ArrayList;表示方法的返回值是ArrayList。

     

    第二行:

      // Signature: <K:Ljava/lang/Exception;>(TK;)Ljava/util/ArrayList<TK;>;^TK;

      Signature是签名的意思,标识开始的关键字,这一行对应的就是泛型了。

      <泛型参数名:上界>这部分对应的是方法的泛型描述。

      (参数列表)和第一行的大体意思一致,但是多了泛型的定义,在字节码中,泛型会用其上界来替代(擦除),如果没有定义上界,则默认为Object,真正的泛型的定义就出现在本行的这个位置。用T前缀来表示泛型,比如泛型K就对应TK;。

      紧跟着参数列表的是返回值。该返回值描述和第一行的返回值描述一致,不过,同样多了泛型的描述,也是用T前缀来表达,比如返回值是java.util.ArrayList,这里就变为Ljava/util/ArrayList<TK;>;。

      ^泛型异常,用于描述用泛型表达的异常,如果异常不是泛型,则该部分描述不会生成。比如throws K就会被描述为^TK;。

     

    第三行:

       // Stack: 1, Locals: 2

      Stack,表达的是调用栈(call stack),用于描述在调用栈上最多有多少个对象。为什么会有个这个栈呢?是因为“局部变量”这个概念对于虚拟机来说,是不存在的,所以在某个方法被调用前,需要把该方法要用到的变量都加载到一个全局调用栈内。方法被虚拟机唤起的时候,只需要按顺序传入变量类型,然后自动从调用栈里按需取得变量。

      每次操作执行完成后,栈被清空,所以,栈深等同为变量最多的操作的变量数。

      Locals,用于描述使用到的本地变量,读者可能会疑惑,该方法里明明只用到了一个形参K,为什么会有两个变量呢?这是因为java默认给方法注册了一个this,作为本地变量。

      懂得了字节码的真相,也就懂得了Java泛型的实现原理。

      Java的方法泛型没有记录在方法体内部,而是在方法签名内做了实现。同样,可以在字节码里找到类接口签名,类字段(成员变量)签名等等。

      换言之,Java的泛型是由编译时擦除和签名来实现的。

      Java这样的设计,是为了兼容性的考虑,低版本的字节码和高版本基本上只有签名上的不一样,不影响功能本体,所以,可以不做任何改动就在高版本的虚拟机里运行。

     反射获取泛型信息

     

      上一节中提到了如下的一些泛型信息不会被擦除:

      1、泛型接口、类、方法定义上处的所有泛型

      2、成员变量声明处的泛型

      可以得出推论,这些泛型信息应当能够被反射获取。

      对这些能被反射获取的内容,按照泛型的分类来进行讨论:

      1、泛型接口和泛型类。它们对应的反射对象都是java.reflect.Class,该类提供了三个方法:

     

    public Type getGenericSuperclass(){...}
    
    public Type[] getGenericInterfaces() {...}
    
    public TypeVariable<Class<T>>[] getTypeParameters() {...}
    

     

      分别对应:获取超类的完整类型,获取接口的完整类型,以及获取自身的类型变量。

      java.lang.reflect.Type是一个空接口,在使用标准JDK的情况下,一般来说,泛型的实现类是:sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl。

      它提供了获取原始类型和泛型类型的方法。

      java.lang.reflect.TypeVariable是Type的子接口,它提供的方法就比Type要详细一些,这些多出来的方法包括:

     

     Type[] getBound(),获取上界;
    
        D getGenericDeclaration(),获取泛型定义;
    
        String getName(),获取泛型参数名,也就是<T>中的T。
    

      2、声明为泛型的字段。它对应的反射对象是java.reflect.Field,提供了一个方法:

    public Type getGenericType() {...}
    

      该方法的使用方式和上文一致。

      3、泛型方法。对应的反射对象是java.reflect.Method,提供了三个方法:

    public Type getGenericReturnType() {...}
    
    public Type getGenericParameterTypes() {...}
    
    public Type getGenericExceptionTypes() {...}
    

      分别对应返回值泛型,参数泛型和异常泛型。

      注意!虽然这里可以获取到泛型的定义,但不论是哪一种方式,其获取到的泛型,都不会是具体的某一个类。给定一个泛型的定义<T>,能获取到的只有T这个关键字。

      这是因为,Java目前的泛型实现已经在原理上(泛型擦除)堵死了“反射获取泛型的确定类型”的可能性。

      泛型的原理和基本概念到这里已经讲解得差不多了,后面会介绍一下Java泛型的历史,以说明为什么Java的泛型为什么有这么多的“缺陷”。

     

  • 相关阅读:
    【BZOJ 1455】罗马游戏
    【UR #2】树上GCD
    1067: [SCOI2007]降雨量
    1068: [SCOI2007]压缩
    1066: [SCOI2007]蜥蜴
    1065: [NOI2008]奥运物流
    1064: [Noi2008]假面舞会
    1063: [Noi2008]道路设计
    2329: [HNOI2011]括号修复
    2734: [HNOI2012]集合选数
  • 原文地址:https://www.cnblogs.com/anrainie/p/5852020.html
Copyright © 2011-2022 走看看