Java泛型深入解析

泛型(Generic),从其字面意思理解就是:广泛的适用于多种类型。

泛型是Java语言中的一块语法糖,它实现了“参数化类型”的概念,使得类或者方法具备了更加广泛的表达能力。(语法糖:指的是某些语言添加的一些语法,这些语法对语言本身没什么影响,但是可以方便程序员的使用,Java其他常见的语法糖还包括基本类型的自动拆装包,for-each循环等等)

Java泛型告诉编译器,你想使用什么类型,然后编译器帮你处理一切细节。此外,它还将类或者方法和具体的类型进行了解耦。

接下来,本文将从:

  1. 产生背景

  2. 泛型的使用

  3. 泛型擦除的原理

  4. 泛型擦除的影响

  5. 通配符

    五个方面对泛型进行详细的阐述,希望能给读者带来收获。

一:产生的背景

在没有泛型的早期Java版本中,集合类中所有元素的类型都是Object,Java中所有的类又是继承自Object,这就意味着,集合内部可以放任何类型,然后使用元素的时候,在进行向下转型,这种将参数的验证推迟至运行期间,问题无法第一时间被发现并及时处理,实例代码可以参考 代码1。

public class Test {

   public static void main(String args[]) {

       List list = new ArrayList();

       list.add(1);

       list.add(2);

       list.add(3);

       list.add("4");



       int sum = 0;

       for (Object o : list) {

           sum += (int) o;// 运行时异常, cannot cast

       }

   }

}

                                                                       代码1:早期没有泛型的集合类

这个时候,编译期间参数类型检验的重要性就凸显出来了,泛型正是在这一背景下,由Java SE5 引入。

二:泛型的使用

泛型的使用,主要包括泛型类(或接口) 和 泛型方法。

需要注意的是,是否用于泛型方法和这个类是否是泛型类没有关系,普通类也能使用泛型方法,同理,泛型类也能用于普通方法。

泛型方法的返回值前需要通过标记 <T> 告知编译器此方法是泛型方法,符号T随意,示例代码如下。

public class Test {

   static class GenericClass<T> {

       private T value;



       public GenericClass(T value) {

           this.value = value;

       }



       public void print() {

           System.out.println(value);

       }

   }





   static class NormalClass {

       public <E> void genericMethod(E parm) {

           System.out.println(parm);

       }

   }



   public static void main(String[] args) {

       GenericClass<Integer> genericClass = new GenericClass<>(1);

       genericClass.print();



       NormalClass normalClass = new NormalClass();

       normalClass.genericMethod(1);

       normalClass.genericMethod("zhanhtTech");

   }

}

                                                                      代码2:泛型类和泛型方法

三:泛型信息真的被擦除了吗?

Java语言的泛型是”伪泛型”,它只存在于程序源码中,在程序运行时期,泛型信息都被擦除了。在编译后的字节码文件中,就已经替换为原生的数据类型,然后在相应的地方插入强制类型转换代码。

因此对于运行期的Java语言来说,ArrayList<Integer>和 ArrayList<String>其实是一个类型。下面通过几段简单的代码对泛型擦除进行进一步的深入探讨。

首先,咱们通过字节码分析下,编译器到底对泛型做了些什么骚操作,代码见示例3。

public class Test {

   public static void main(String[] args) {

       List<Integer> integerList = new ArrayList<>();

       integerList.add(1);



       List<String> stringList = new ArrayList<>();

       stringList.add("zhanhtTech");



       System.out.println(integerList instanceof ArrayList);

       System.out.println(stringList instanceof ArrayList);
       Integer i = integerList.get(0);

       String s = stringList.get(0);

   }

}

                                                                       代码3:泛型擦除示例

显然,上述代码的运行结果是两个true,下面我们通过 javap -verbose Test命令来看下编译后的字节码,字节码见代码4:

  1. public class com.jd.bundle.buy.common.utils.Test

  2.  minor version: 0

  3.  major version: 52

  4.  flags: ACC_PUBLIC, ACC_SUPER

  5. Constant pool:

  6.   #1 = Methodref          #13.#22        // java/lang/Object."<init>":()V

  7.   #2 = Class              #23            // java/util/ArrayList

  8.   #3 = Methodref          #2.#22         // java/util/ArrayList."<init>":()V

  9.   #4 = Methodref          #10.#24        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

  10.   #5 = InterfaceMethodref #25.#26        // java/util/List.add:(Ljava/lang/Object;)Z

  11.   #6 = String             #27            // zhanhtTech

  12.   #7 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;

  13.   #8 = Methodref          #30.#31        // java/io/PrintStream.println:(Z)V

  14.   #9 = InterfaceMethodref #25.#32        // java/util/List.get:(I)Ljava/lang/Object;

  15.  #10 = Class              #33            // java/lang/Integer

  16.  #11 = Class              #34            // java/lang/String

  17.  #12 = Class              #35            // com/jd/bundle/buy/common/utils/Test

  18.  #13 = Class              #36            // java/lang/Object

  19.  #14 = Utf8               <init>

  20.  #15 = Utf8               ()V

  21.  #16 = Utf8               Code

  22.  #17 = Utf8               LineNumberTable

  23.  #18 = Utf8               main

  24.  #19 = Utf8               ([Ljava/lang/String;)V

  25.  #20 = Utf8               SourceFile

  26.  #21 = Utf8               Test.java

  27.  #22 = NameAndType        #14:#15        // "<init>":()V

  28.  #23 = Utf8               java/util/ArrayList

  29.  #24 = NameAndType        #37:#38        // valueOf:(I)Ljava/lang/Integer;

  30.  #25 = Class              #39            // java/util/List

  31.  #26 = NameAndType        #40:#41        // add:(Ljava/lang/Object;)Z

  32.  #27 = Utf8               zhanhtTech

  33.  #28 = Class              #42            // java/lang/System

  34.  #29 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;

  35.  #30 = Class              #45            // java/io/PrintStream

  36.  #31 = NameAndType        #46:#47        // println:(Z)V

  37.  #32 = NameAndType        #48:#49        // get:(I)Ljava/lang/Object;

  38.  #33 = Utf8               java/lang/Integer

  39.  #34 = Utf8               java/lang/String

  40.  #35 = Utf8               com/jd/bundle/buy/common/utils/Test

  41.  #36 = Utf8               java/lang/Object

  42.  #37 = Utf8               valueOf

  43.  #38 = Utf8               (I)Ljava/lang/Integer;

  44.  #39 = Utf8               java/util/List

  45.  #40 = Utf8               add

  46.  #41 = Utf8               (Ljava/lang/Object;)Z

  47.  #42 = Utf8               java/lang/System

  48.  #43 = Utf8               out

  49.  #44 = Utf8               Ljava/io/PrintStream;

  50.  #45 = Utf8               java/io/PrintStream

  51.  #46 = Utf8               println

  52.  #47 = Utf8               (Z)V

  53.  #48 = Utf8               get

  54.  #49 = Utf8               (I)Ljava/lang/Object;

  55. {

  56.  public com.jd.bundle.buy.common.utils.Test();

  57.    descriptor: ()V

  58.    flags: ACC_PUBLIC

  59.    Code:

  60.      stack=1, locals=1, args_size=1

  61.         0: aload_0

  62.         1: invokespecial #1                  // Method java/lang/Object."<init>":()V

  63.         4: return

  64.      LineNumberTable:

  65.        line 9: 0

  66.  public static void main(java.lang.String[]);

  67.    descriptor: ([Ljava/lang/String;)V

  68.    flags: ACC_PUBLIC, ACC_STATIC

  69.    Code:

  70.      stack=2, locals=5, args_size=1

  71.         0: new           #2                  // class java/util/ArrayList

  72.         3: dup

  73.         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V

  74.         7: astore_1

  75.         8: aload_1

  76.         9: iconst_1

  77.        10: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

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

  79.        18: pop

  80.        19: new           #2                  // class java/util/ArrayList

  81.        22: dup

  82.        23: invokespecial #3                  // Method java/util/ArrayList."<init>":()V

  83.        26: astore_2

  84.        27: aload_2

  85.        28: ldc           #6                  // String zhanhtTech

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

  87.        35: pop

  88.        36: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;

  89.        39: aload_1

  90.        40: instanceof    #2                  // class java/util/ArrayList

  91.        43: invokevirtual #8                  // Method java/io/PrintStream.println:(Z)V

  92.        46: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;

  93.        49: aload_2

  94.        50: instanceof    #2                  // class java/util/ArrayList

  95.        53: invokevirtual #8                  // Method java/io/PrintStream.println:(Z)V

  96.        56: aload_1

  97.        57: iconst_0

  98.        58: invokeinterface #9,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;

  99.        63: checkcast     #10                 // class java/lang/Integer

  100.        66: astore_3

  101.        67: aload_2

  102.        68: iconst_0

  103.        69: invokeinterface #9,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;

  104.        74: checkcast     #11                 // class java/lang/String

  105.        77: astore        4

  106.        79: return

  107.      LineNumberTable:

  108.        line 11: 0

  109.        line 12: 8

  110.        line 14: 19

  111.        line 15: 27

  112.        line 17: 36

  113.        line 18: 46

  114.        line 20: 56

  115.        line 21: 67

  116.        line 22: 79

  117. }

  118. SourceFile: "Test.java"

                                                                          代码4:上述示例的字节码

Java字节码是以字节为基础但为的二进制流,各个数据项目都严格按照规定的格式和顺序排列,具体的信息,感兴趣的话,可以自行查阅相关书籍或者Java虚拟机规范文档。

这里,我主要对涉及泛型的相关部分进行详细介绍。69行—120行记录了main方法的相关信息,这里,为了便于后面的讲解,进行简单定位介绍。前面几个信息记录了方法名,方法的入参类型和返回值类型 以及方法的访问限制信息。方法内部具体的代码是保存在方法的Code属性中。

Code属性首先记录的信息是栈,局部变量表,方法入参的大小;然后,是具体的字节码指令,部分指令是带参数的,后面的#数字,指的是常量池的位置,常量池位于文件的最开始;最后,LineNumberTable记录的是指令和java代码源文件的行对应关系。

下面,对74-109行的字节码进行具体分析,接下来直接使用指令前面的行号。

首先,0-7指令表示的是实例化了一个ArrayList并存储到局部变量表,注意,这个List没有任何Integer的信息。然后,第9行,常量1入栈,接着调用Integer类的valueOf 方法将其转为Integer实例并通过13行的指令,add进list,注意这条指令的注释部分,InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z ,括号里面显示的是add方法对的入参,是Object类型,出参标识是Z,标识的是boolean基本类型。

接下来,一样的方式创建了另一个ArrayList实例。接下来,咱们注意58,63两行指令,先get一个Object类型的数据,然后通过checkCast指令将其转为Integer类型。

所以,通过上面的分析,咱们发现:Java的泛型 = 替换为原生类型 + 强制类型转换。

那么,我的问题来了,是不是由于泛型擦除的存在,泛型的信息在运行时将荡然无存呢?答案是NO!!!

泛型的擦除其实是对方法的Code属性中的字节码进行了擦除,但是方法的元数据还是记录了泛型的信息。Code属性只是方法表中众多属性的一种而已,记录泛型的就是Signature属性,JDk1.5 新增。所以,后期的java版本,能通过反射机制获取方法的泛型信息,示例代码见 代码5。

public class Test {

   public static void main(String args[]) throws Exception {

       List<String> stringList = new ArrayList<>();

       method(stringList);

   }



   public static void method(List<String> list) throws Exception {

       Method m = Test.class.getMethod("method", List.class);

       Type[] t1 = m.getParameterTypes();// 其返回是参数的类型

       Type[] t2 = m.getGenericParameterTypes();//其返回的是参数的参数化的类型,里面的带有实际的参数类型

       Method m2 = Test.class.getMethod("main", String[].class);

       System.out.println(t1[0]);//interface java.util.List

       System.out.println(t2[0]);//java.util.List<java.util.Date>

   }

}

                                                                         代码5:反射获取泛型信息

四:擦除的影响

因为泛型擦除的原因,任何在运行时需要知道确切类型信息的操作都将无法工作,你无法 new 一个 T的实例,无法创建泛型数组。

对于泛型数组,一般可以通过以下两种方式来进行弥补:

  1. 使用ArrayList替代

  2. 通过创建Object的数组,然后强制类型转换。

五:通配符

大家都知道数组是协变的,这就意味着如果一个方法的入参是Object[] 类型,我传入 Integer[] 类型的变量也是合法的,但是泛型就不一样了,它是非协变的,例如 代码6所示。

public class Test {

   public static void main(String args[]) throws Exception {

       Integer[] array = new Integer[10];

       List<Integer> list = new ArrayList<>();



       f1(array);

       f2(list); // compile error

   }



   public static void f1(Object[] array) {

       System.out.println();

   }



   public static void f2(List<Object> list) {

       System.out.println();

   }

}

                                                                                 代码6:数组协变

解决泛型这一问题的方式就是:通配符。它包括三种:无界通配符(?),上界通配符(? extends T) 和 下界通配符(? super T)。它们遵循PECS原则(producer extends )。

以“?” 和 “? extends T”声明的集合,因为无法知悉它需要的具体的子类型,所以不能往此集合中添加元素,但是可以获取,所以它只能作为生产者。

同理,“? super T”声明的集合,传入的只要是T 及其子类,就能添加,但是读取只能用Object类型接收。示例代码如下。

public class Test {



   static class Human {

   }



   static class Man extends Human {

   }



   public static void main(String args[]) throws Exception {

       Object object = new Object();

       Human human = new Human();

       Man man = new Man();



       List<?> list1 = new ArrayList<>();

       list1.add(object); // compile error

       list1.add(human); // compile error

       list1.add(man); // compile error

       Object object1 = list1.get(0); // complie success

       Human object11 = list1.get(0); // complie error



       List<? extends Human> list2 = new ArrayList<>();

       list2.add(object); // compile error

       list2.add(human); // compile error

       list2.add(man); // compile error

       Object object2 = list2.get(0); // complie success

       Human object22 = list2.get(0); // complie success



       List<? super Human> list3 = new ArrayList<>();

       list3.add(object); // compile error

       list3.add(human); // compile success

       list3.add(man); // compile success

       Object object3 = list3.get(0); // complie success

       Human object33 = list3.get(0); // complie error

   }

}

                                                                        代码7:泛型通配符

所以,通配符总结如下: (1) 对于频繁读取,不写入的,用extends

                                         (2) 对于频繁写入,用super

猜你喜欢

转载自blog.csdn.net/zhanht/article/details/81590660