Java泛型浅究(一)

泛型是什么?

引用维基百科上的一段介绍泛型的话

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

泛型是存在于强类型语言中(先编译后运行)的一种机制,它其实提供了一种容器,它本质上是一个“代码模板”,可让开发者定义类型安全数据结构,无需处理实际数据类型,或者也可以说提供了一种抽象的数据结构,提高了编码的容错性,同时,也提高了代码的可读性。很多编程语言提供了泛型支持,比如我们熟知的Java语言,以及微软近几年推出JavaScript的超级TypeScript,以及C#,等等。

泛型的种类

  • 类型擦除式泛型(Type Erasure Generics)
    在运行时编译器会将泛型信息擦除,只保留Raw类型
  • 具现化式泛型(Reified Generics)
    在编译或者运行时,泛型信息始终会被带着
    目前有Java使用的是“类型擦除式泛型”,存在于源代码中,在字节码文件中就会被擦除,被替换为原生的类型,而以C#为代表的使用具现式泛型。

Java的泛型类型擦除

首先,我们来看一个最简单的例子:

 @Test
public void test_generics() {
     Map<Integer, String> hashMap = new HashMap<>(8);
     hashMap.put(1, "hello");
     System.out.println(hashMap.getClass());
 }

这个单测代码中使用了Map,是String类型的有序容器,我们来打断点看下运行时的hashMap 实例类型。
Java泛型浅究(一)_第1张图片从代码的断点中可以看到,hashMap并没有将泛型信息追加在其后。

接着,我们再看一个运行时插入其他类型值的例子

首先初始化一个HashMap,然后根据泛型的要求,放入一个值,接着在其之后打断点,然后使用IDE的变量计算工具,再给hashMap放入一个Key类型为String,Value 类型也为String的值,从断点信息可以看出,程序并没有报错,将这个值成功添加到Map中了,这足以说明,Java在运行期间,泛型已经被擦除了,可将Object值插入其中。

Java泛型浅究(一)_第2张图片

类型擦除式泛型的缺点

因为Java使用的是擦除式泛型,在编译运行之后泛型会被擦除,所以在使用上有一些缺点,举几个例子来看下:

1.简单类型不支持泛型
Java语言中的数据类型包括基本数据类型和引用类型,引用类型都是Object的子类,而基本类型则是Java中特殊的存在,因为不支持Object类型和int,long相互转换,所以,在当时设计泛型的时候索性不支持基本类型有泛型,也正因为如此,Java在基本类型和引用类型的相互转换(拆箱&装箱操作)上消耗了很多性能。

List<int> primitiveType = new ArrayList<>();
List<long> primitiveType = new ArrayList<>();

2.不得不加入类型参数
因为在运行期间获取不到泛型信息,所有必须要额外传入一些能标识泛型的信息,比如以下这个convert方法,需要将List,转为数组,获取不到T的类型,所以需要再传入Class信息。

public static <T> T[] convert(List<T> list, Class<T> clazz) {
      T[] array = (T[]) Array.newInstance(clazz, list.size());
      return array;
}

Java怎么获取泛型中的类型

在JDK1.5 中,伴随泛型特性的到来,还加入了”Type“ 接口,Type接口的定义是所有超类的的类型的集合,包括原生类型,parameterized(泛型类型),数组类型,和基本类型。
java.lang.reflect.Type
Java泛型浅究(一)_第3张图片
再看Class类:
我们要想从运行时得出泛型的参数,只能找找看Class文件中是否有相关API,查看Class.java 的确有相关名称的API。其中,getGenericInfo(),getGenericSignature0(),是私有的native方法。
Java泛型浅究(一)_第4张图片

getGenericInterfaces

getGenericSuperclass

Java泛型浅究(一)_第5张图片
查看getGenericSuperclass源码的介绍,我们可以了解到,可以使用此方法获取到父类的泛型类型。
通过例子来看怎么获取到泛型的类型

@Test
    public void test_ref_for_generic() {
        TypeRef<Map<String, Integer>> typeRef = new TypeRef<Map<String, Integer>>() {
        };
        Type type = ((ParameterizedType) typeRef.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        System.out.println(type);
    }

    class TypeRef<T> {
        public Class<T> getTClass() {
            Class<T> tClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            return tClass;
        }
    }

Java泛型浅究(一)_第6张图片
从这个例子我们可以得出,Java并不是将全部的泛型信息擦除,在Class元数据上,还是保留。

为什么可以获取到泛型

在java虚拟机规范中,加入了Singature这个接口,可以获取到泛型的相关信息,比如属性名称,名称长度,以及下标。
Java泛型浅究(一)_第7张图片
我们通过反编译字节码也可看出相关元数据信息:
在这里插入图片描述

总结

Java之所以实现了擦除式的泛型,是应为在当时Java遗留的代码量足够庞大,加之在jdk1.2时候已经对集合类做出来改变,比如Vector和现在的List,HashTable和现在和HashMap,如果再出现新泛型的容器,那集合类库也将混乱不已,当然,虽然有了这些不致命的缺点,还是阻止不了Java语言在现在的地位。

参考:
《深入理解Java虚拟机》
《Java虚拟机规范》
Stack Overflow question

你可能感兴趣的:(javase)