泛型(Generic),从其字面意思理解就是:广泛的适用于多种类型。
泛型是Java语言中的一块语法糖,它实现了“参数化类型”的概念,使得类或者方法具备了更加广泛的表达能力。(语法糖:指的是某些语言添加的一些语法,这些语法对语言本身没什么影响,但是可以方便程序员的使用,Java其他常见的语法糖还包括基本类型的自动拆装包,for-each循环等等)
Java泛型告诉编译器,你想使用什么类型,然后编译器帮你处理一切细节。此外,它还将类或者方法和具体的类型进行了解耦。
接下来,本文将从:
-
产生背景
-
泛型的使用
-
泛型擦除的原理
-
泛型擦除的影响
-
通配符
五个方面对泛型进行详细的阐述,希望能给读者带来收获。
一:产生的背景
在没有泛型的早期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:
-
public class com.jd.bundle.buy.common.utils.Test
-
minor version: 0
-
major version: 52
-
flags: ACC_PUBLIC, ACC_SUPER
-
Constant pool:
-
#1 = Methodref #13.#22 // java/lang/Object."<init>":()V
-
#2 = Class #23 // java/util/ArrayList
-
#3 = Methodref #2.#22 // java/util/ArrayList."<init>":()V
-
#4 = Methodref #10.#24 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-
#5 = InterfaceMethodref #25.#26 // java/util/List.add:(Ljava/lang/Object;)Z
-
#6 = String #27 // zhanhtTech
-
#7 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
-
#8 = Methodref #30.#31 // java/io/PrintStream.println:(Z)V
-
#9 = InterfaceMethodref #25.#32 // java/util/List.get:(I)Ljava/lang/Object;
-
#10 = Class #33 // java/lang/Integer
-
#11 = Class #34 // java/lang/String
-
#12 = Class #35 // com/jd/bundle/buy/common/utils/Test
-
#13 = Class #36 // java/lang/Object
-
#14 = Utf8 <init>
-
#15 = Utf8 ()V
-
#16 = Utf8 Code
-
#17 = Utf8 LineNumberTable
-
#18 = Utf8 main
-
#19 = Utf8 ([Ljava/lang/String;)V
-
#20 = Utf8 SourceFile
-
#21 = Utf8 Test.java
-
#22 = NameAndType #14:#15 // "<init>":()V
-
#23 = Utf8 java/util/ArrayList
-
#24 = NameAndType #37:#38 // valueOf:(I)Ljava/lang/Integer;
-
#25 = Class #39 // java/util/List
-
#26 = NameAndType #40:#41 // add:(Ljava/lang/Object;)Z
-
#27 = Utf8 zhanhtTech
-
#28 = Class #42 // java/lang/System
-
#29 = NameAndType #43:#44 // out:Ljava/io/PrintStream;
-
#30 = Class #45 // java/io/PrintStream
-
#31 = NameAndType #46:#47 // println:(Z)V
-
#32 = NameAndType #48:#49 // get:(I)Ljava/lang/Object;
-
#33 = Utf8 java/lang/Integer
-
#34 = Utf8 java/lang/String
-
#35 = Utf8 com/jd/bundle/buy/common/utils/Test
-
#36 = Utf8 java/lang/Object
-
#37 = Utf8 valueOf
-
#38 = Utf8 (I)Ljava/lang/Integer;
-
#39 = Utf8 java/util/List
-
#40 = Utf8 add
-
#41 = Utf8 (Ljava/lang/Object;)Z
-
#42 = Utf8 java/lang/System
-
#43 = Utf8 out
-
#44 = Utf8 Ljava/io/PrintStream;
-
#45 = Utf8 java/io/PrintStream
-
#46 = Utf8 println
-
#47 = Utf8 (Z)V
-
#48 = Utf8 get
-
#49 = Utf8 (I)Ljava/lang/Object;
-
{
-
public com.jd.bundle.buy.common.utils.Test();
-
descriptor: ()V
-
flags: ACC_PUBLIC
-
Code:
-
stack=1, locals=1, args_size=1
-
0: aload_0
-
1: invokespecial #1 // Method java/lang/Object."<init>":()V
-
4: return
-
LineNumberTable:
-
line 9: 0
-
public static void main(java.lang.String[]);
-
descriptor: ([Ljava/lang/String;)V
-
flags: ACC_PUBLIC, ACC_STATIC
-
Code:
-
stack=2, locals=5, args_size=1
-
0: new #2 // class java/util/ArrayList
-
3: dup
-
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
-
7: astore_1
-
8: aload_1
-
9: iconst_1
-
10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-
13: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
-
18: pop
-
19: new #2 // class java/util/ArrayList
-
22: dup
-
23: invokespecial #3 // Method java/util/ArrayList."<init>":()V
-
26: astore_2
-
27: aload_2
-
28: ldc #6 // String zhanhtTech
-
30: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
-
35: pop
-
36: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
-
39: aload_1
-
40: instanceof #2 // class java/util/ArrayList
-
43: invokevirtual #8 // Method java/io/PrintStream.println:(Z)V
-
46: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
-
49: aload_2
-
50: instanceof #2 // class java/util/ArrayList
-
53: invokevirtual #8 // Method java/io/PrintStream.println:(Z)V
-
56: aload_1
-
57: iconst_0
-
58: invokeinterface #9, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
-
63: checkcast #10 // class java/lang/Integer
-
66: astore_3
-
67: aload_2
-
68: iconst_0
-
69: invokeinterface #9, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
-
74: checkcast #11 // class java/lang/String
-
77: astore 4
-
79: return
-
LineNumberTable:
-
line 11: 0
-
line 12: 8
-
line 14: 19
-
line 15: 27
-
line 17: 36
-
line 18: 46
-
line 20: 56
-
line 21: 67
-
line 22: 79
-
}
-
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的实例,无法创建泛型数组。
对于泛型数组,一般可以通过以下两种方式来进行弥补:
-
使用ArrayList替代
-
通过创建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