Java 泛型解决了JDK设计上的缺陷

2,046 阅读4分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。

泛型

泛型是在JDK 5.0时,JAVA世界引入的在类、接口或者方法里面可以使用的一种标记类型。

为什么要有泛型?

其实可以类比非关系数据库和关系数据库,

generic2.png

  • 非关系数据库实现简单,存取高效,但是当用于复杂的逻辑时,非关系数据库有多难受,那用过的人自然就深有体会。
  • 关系数据库的schema设计就让数据变得仅仅有条。
  • C语言和jdk的原生数组也是有具体类型的
  • Java说一切都是对象,对象都是某个类的实例,但是在设计HashMap和ArrayList这样的容器类型的时候,却没有想到容器的具体类型这一层,导致它们的管理很不好。

所以说jdk 5.0之前的设计有点问题,可能之前认为数组就够用了,但是又设计了HashMap和ArrayList等各种各样的容器类。

generic9.png

在这样的情况下,不清楚容器里面的具体类型,比如在做类型转换和计算的时候总容易出错,导致程序容易奔溃

上帝在创造世界的时候,一开始并不是很完善的,类和对象的世界当时也存在这样一个bug,导致代码总是容易出错。

所以在JDK 5.0时引入了泛型的设计。

什么是泛型?

在JDK 5.0之后,JAVA世界引入的在类、接口或者方法里面可以使用的一种标记元素类型的符号。 这里的元素类型包括成员的类型函数返回值的类型方法参数的类型

那怎么使用他们呢?

泛型类

class MyList<T>{
    private T[] elements;
    private int size;
    public void add(T item) {
        elements[size++]=item;
    }
    public Object get(int index) {
        return elements[index];
    }
}

可以看出:

  • MyList<T> :T表示类的成员的类型
  • T这个符号被用于成员elements

generic10.png

泛型接口

接口的使用和类相似

//定义一个泛型接口
public interface Gen<T> {
    public T func();
}

//实现的也还是泛型类
class GenImpl1<T> implements Gen<T> {
    @Override
    public T func() {
        return null;
    }
}

//实现的类不再是泛型类了,那么要将T的类型写入了。。
class GenImpl2 implements Gen<String> {
    @Override
    public String func() {
        return null;
    }
}

上面的演示代码:

  • 定义了一个泛型接口interface Gen
  • T是func函数的返回类型 我们知道,接口的作用是被类实现,那么其他类是怎么实现带有泛型的接口呢?

有两种方式:

  • 实现的类自身也还是可以是泛型类,就像这里的class GenImpl1<T> implements Gen<T>
  • 实现的类不再是泛型类了,那么声明的时候要将接口标记符号T具体的类型来替换,就像这里的class GenImpl2 implements Gen<String>

generic5.png

泛型方法

方法也可以支持泛型。

class GenericFunc {
    /**
     *
     * @param e
     * @param <E>
     */
    public <E> void test(E e) {
        System.out.println(e);
    }

}
  • 调用new GenericFunc().test("hello");返回hello
  • 泛型类是将泛型标记符号放在类名的后面
  • 而方法是放在方法返回值的前面,比如 <E> void test(E e)
  • 这是定义了方法参数的类型是泛型E,那么随意输入什么类型的参数,编译都没有问题

支持泛型的JDK

jdk 5.0之前是不支持泛型的,在虚拟机的世界里,只有类和对象。

System.out.println("class: "+ HashMap.class);
System.out.println("class: "+ HashMap<String,String>.class);

HashMap.class存在,但是HashMap<String,String>.class这样的写法编译器会报错。

其实5.0之后这么写也都会报错,在虚拟机运行的世界里,还是只有类和对象。 也就是说:

  • jdk的运行时环境其实还是不变的,没有对泛型的支持。
  • jdk 5.0以及后续版本的编译器支持了泛型

编译器支持泛型

jdk 5.0以及后续版本的编译器时怎么支持泛型的呢? 编译器做了如下图的事情。 generic6.png

  • 首先,编译器会在编译过程中检查代码的泛型声明和使用是否一致
class THolder<T> {
    T item;
    public void setData(T t) {
        this.item = t;
    }

    public T getData() {
        return this.item;
    }

    public static void main(String[] args) {
        THolder<Integer> holder = new THolder<>();
        holder.setData("abc");
        holder.getData();
    }
}

这里setData的是"abc",类型不是Integer,则没法编译通过。

  • 然后,编译器会在编译过程中移除参数标记符号信息,并且将T用一个具体的类型替换,一般是Object
    • 这个过程(不用具体声明的类替换,而用Object类替换的过程)称为擦除 执行下面的操作:
 # javac THolder.java
 # javap -verbose THolder.class

得到反编译的字节码:

{  T item;
    descriptor: Ljava/lang/Object;
    ...
  public void setData(T);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field item:Ljava/lang/Object;
         5: return
...
  public T getData();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field item:Ljava/lang/Object;
         4: areturn
  ...
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #3                  // class THolder
         ...
        20: checkcast     #8                  // class java/lang/Integer
        23: astore_2
        24: return
}

可以看出

  • setData函数里putfield指令的数据类型是Object
  • getData函数里gettfield指令的数据类型是Object
  • 最后, 会做类型强制转换
    • 调用点main函数里getData处的调用指令checkcast将Object类型强制转换为java/lang/Integer

另外泛型类的写法也可以是这样的

  • 上界<T extends Parent>:通过它能给与参数类型添加一个边界
class THolder<T extends Parent> {
    T item;
    public void setData(T t) {
        this.item = t;
    }
    public T getData() {
        return this.item;
    }
    public static void main(String[] args) {
        THolder<Child> holder = new THolder<>();
        holder.setData(new Child());
    }
}
interface Parent{
...
class Child implements Parent{
...

这样的代码按照上面的编译和反编译过程再操作一遍,则是得到这样的字节码:

         2: putfield      #2                  // Field item:LParent;
         1: getfield      #2                  // Field item:LParent;   

可以看到这里用的类型则是Parent了,而不是Object了。

可以不擦除吗?

为什么泛型擦除的时候,不直接使用Integer呢,而是要用Object呢?

其实不擦除也是可以的,而且也就没有后面checkcast这个步骤了。

而JDK采用这种做法的原因是因为历史代码兼容的原因。

怎么说呢? jdk 5.0之前也就是2004年之前编译好的字节码基本都是这样的,类似下图这样的容器对象obj1:

generic7.png

而现在如果不擦除类型,则是这样的容器对象obj2:

generic8.png

于是,现在obj1=obj2这样的赋值操作不可以了。但是如果还是擦除则是可以的,因为擦除后的类型都是Object。 所以现在如果不擦除类型,这就和以前ArrayList的语义不符了。

所以HotSpot的虚拟机采用了擦除的机制来维持这种历史的一致性。

当然别的有些虚拟机可能也没有采用擦除的机制,效率当然更好一些。

Spring支持泛型

spring 4.0里面的bean注入的类型也支持泛型。 因为虽然有泛型擦除,但是还是可以通过某种方式得到具体的类型的。

其他注意的点

  • 因为泛型不支持一些运行时特性(而且最终被擦除),要注意有些写法将编译不过,比如new
  • 通配符,存在三种形式的用通配符的泛型变量表达方式:
    • <?>:C<?> c,表示c中的元素类型不确定
    • <? extends A>: C<? extends A> c,表示c中的元素类型都是A或者A的子类
    • <? super B>:C<? super B> c,表示c中的元素类型是B或者B的父类

将在下一篇文章中详细介绍。

总结

Java 泛型其实是为了解决JDK容器类的缺陷,引入泛型也是完美的解决了类型混乱的问题的。

然后锦上添花,又使得所有的类更加的通用了,可以更像容器一点。