副标题#e#
最近技术交流群里,有朋友问:Object和泛型T有啥区别。回答完问题,不禁在想,面试在即,还有那么多朋友不了泛型?是时候给大家整理一篇泛型相关的文章了,一篇文章全面搞定泛型,让大家再也不愁面试或实践中泛型相关的问题了。
什么是泛型
泛型是在JDK 5时就引入的新特性,也就是“参数化类型”,通俗来讲就是将原来的具体类型通过参数化来定义,使用或调用时再传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新类型的前提下,通过泛型指定的不同类型来控制形参具体的类型)。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
为什么使用泛型
未使用泛型时,可以通过Object来实现参数的“任意化”,但这样做的缺点就是需要显式的强制类型转换,这就需要开发者知道实际的类型。
而强制类型转换是会出现错误的,比如Object将实际类型为String,强转成Integer。编译期是不会提示错误的,而在运行时就会抛出异常,很明显的安全隐患。
Java通过引入泛型机制,将上述的隐患提前到编译期进行检查,开发人员既可明确的知道实际类型,又可以通过编译期的检查提示错误,从而提升代码的安全性和健壮性。
使用泛型前后的对比
拿一个经典的例子来演示一下未使用泛型会出现的问题。
List list = new ArrayList();
list.add(1);
list.add("zhuan2quan");
list.add("程序新视界");
for (int i = 0; i < list.size(); i++) {
String value = (String) list.get(i);
System.out.println("value=" + value);
}
上述代码在编译器并不会报任何错误,但当执行时会抛出如下异常:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
那么,是否可以在编译器就解决这个问题,而不是在运行期抛出异常呢?泛型应运而生。上述代码通过泛型来写之后,变成如下形式:
List<String> list = new ArrayList<>();
list.add(1);
list.add("zhuan2quan");
list.add("程序新视界");
for (String value : list) {
System.out.println("value=" + value);
}
可以看出,代码变得更加清爽简单,而且list.add(1)这行代码在IDE中直接会提示错误信息:
Required type: String
Provided: int
提示错误信息便是泛型对向List中添加的数据产生了约束,只能是String类型。
泛型中通配符
在使用泛型时经常会看到T、E、K、V这些通配符,它们代表着什么含义呢?
本质上它们都是通配符,并没有什么区别,换成A-Z之间的任何字母都可以。不过在开发者之间倒是有些不成文的约定:
T (type) 表示具体的一个java类型;
K V (key value) 分别代表java键值中的Key Value;
E (element) 代表Element;
为什么Java的泛型是假泛型
为了做到向下兼容,Java中的泛型仅仅是一个语法糖,并不是C++那样的真泛型。
还是上面的例子,在直接向泛型为String的List中添加int类型会提示错误:
List<String> list = new ArrayList<>();
list.add(1);
针对上述代码,我们采用反射间接地调用add方法:
@Test
public void test3() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> list = new ArrayList<>();
list.add(1);
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list,"程序新视界");
System.out.println(list);
System.out.println(list.get(1));
}
执行上述代码,我们发现程序并没有抛出异常,正常打印出入:
[1, 程序新视界]
程序新视界
原本只能装入Integer的List,成功装入了一个String类型的值。由此可见,所谓的泛型确实是假泛型。
同时,我们还可以通过字节码来证明。拿上面使用了泛型的实例代码,通过javap -c命令来看看字节码:
Code:
#p#副标题#e#
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
#p#副标题#e##p#分页标题#e#
9: ldc #6 // String zhuan2quan
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #7 // String 程序新视界
20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: invokeinterface #18, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_2
33: aload_2
34: invokeinterface #19, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
39: ifeq 80
从字节码中可以看出,List.add方法本质上就是一个Object。再次证明,Java的泛型仅仅在编译期有效,在运行期则会被擦除,也就是说所有的泛型参数类型在编译后都会被清除掉。这就是我们经常说的类型擦除。
因此,也可以说:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。