Java 泛型解决了JDK设计上的缺陷
泛型
泛型是在JDK 5.0时,JAVA世界引入的在类、接口或者方法里面可以使用的一种标记类型。
为什么要有泛型?
其实可以类比非关系数据库和关系数据库,
- 非关系数据库实现简单,存取高效,但是当用于复杂的逻辑时,非关系数据库有多难受,那用过的人自然就深有体会。
- 关系数据库的schema设计就让数据变得仅仅有条。
- C语言和jdk的原生数组也是有具体类型的
- Java说一切都是对象,对象都是某个类的实例,但是在设计HashMap和ArrayList这样的容器类型的时候,却没有想到容器的具体类型这一层,导致它们的管理很不好。
所以说jdk 5.0之前的设计有点问题,可能之前认为数组就够用了,但是又设计了HashMap和ArrayList等各种各样的容器类。
在这样的情况下,不清楚容器里面的具体类型,比如在做类型转换和计算的时候总容易出错,导致程序容易奔溃。
上帝在创造世界的时候,一开始并不是很完善的,类和对象的世界当时也存在这样一个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
泛型接口
接口的使用和类相似
//定义一个泛型接口
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>
泛型方法
方法也可以支持泛型。
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以及后续版本的编译器时怎么支持泛型的呢?
编译器做了如下图的事情。
- 首先,编译器会在编译过程中检查代码的泛型声明和使用是否一致
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:
而现在如果不擦除类型,则是这样的容器对象obj2:
于是,现在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容器类的缺陷,引入泛型也是完美的解决了类型混乱的问题的。
然后锦上添花,又使得所有的类更加的通用了,可以更像容器一点。
转载自:https://juejin.cn/post/7035272895668420644