Java泛型入门篇: 泛型类、泛型接口以及泛型方法
Java泛型进阶篇: 无界通配符、上界通配符以及下界通配符
Java泛型原理篇: 类型擦除以及桥接方法
文章开始之前首先还是先思考一个经典问题,以下程序的输出是什么
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.class == intList.class);
大多数人可能会想,既然泛型可以“限定”list中元素的类型,证明两个list或多或少是有所区别的,那么两个不同泛型类型list的字节码文件应该也不是一个。但运行程序就会发现,控制台输出为true,这意味两个list的class地址都一样,为同一个字节码文件,这似乎与猜想的不太一样。
这个试验也侧面反映出一个现象:泛型在运行时就不存在了。事实上也确实如此,那么是怎么做到在编译时能“限制”类型,而在运行时又没有了“限制”呢?
之前曾经介绍过,Java中的泛型是在1.5版本之后才有的特性,而为了兼容1.5之前版本即没有泛型的代码(如List),Java采用了类型擦除
机制来解决这一问题。
所谓的类型擦除(type erasure)
,指的是泛型只在编译时起作用,在进入JVM之前,泛型会被擦除掉,根据泛型定义的形式而被替换为相应的类型。这也说明了Java的泛型其实是伪泛型。
当泛型类型被声明为一个具体的泛型标识,或一个无界通配符
时,泛型类型将会被替代为Object
。这也比较容易理解,如List>
,在获取元素的时候因为不能够确定具体的类型,所以只能使用Object
来接收,在擦除的时候也是一样的道理,无法确定具体类型,所以擦除泛型时会将其替换为Object
类型,如:
public class TestGeneric<T> {
private T t;
public TesetGeneric(T t) {
this.t = t;
}
public static <E> E test(E e) {
// Do something...
return e;
}
}
将会被修改为
public class TestGeneric {
private Object t;
public TesetGeneric(Object t) {
this.t = t;
}
public static Object test(Object e) {
// Do something...
return e;
}
}
可以使用反射来验证我们的结论是否正确:
public static void main(String[] args) throws Exception {
TestGeneric<String> test = new TestGeneric("");
Class<? extends TestGeneric> cls = test.getClass();
Field t = cls.getDeclaredField("t");
// 结果为class java.lang.class
System.out.println(t.getType());
// 这里参数只能传Object,传其他类型会报找不到该方法
Method method = cls.getDeclaredMethod("test", Object.class);
// 结果为class java.lang.class
System.out.print(method.getReturnType());
}
当泛型类型被声明为一个上界通配符
时,泛型类型将会被替代为相应上界的类型。如List extends Number>
,程序并不能够确定具体的类型,只知道是Number
或其子类,所以会擦除为Number
类型,如
public class TestGeneric<T extends Number> {
private T t;
public TesetGeneric(T t) {
this.t = t;
}
public static <E> E test(E e) {
// Do something...
return e;
}
}
将会被修改为
public class TestGeneric {
private Number t;
public TesetGeneric(Number t) {
this.t = t;
}
public static Number test(Number e) {
// Do something...
return e;
}
}
如果有多个上界,则会转换为extends
关键字后的第一个类型。
public interface Comedy {
}
public interface Chinese {
}
public class ChineseComedy implements Comedy, Chinese {
}
/**
* @author beemo
*/
public class TestGeneric<T extends Comedy & Chinese> {
private T t;
public TestGeneric(T t) {
this.t = t;
}
public <T extends Comedy & Chinese> T test(T t) {
return t;
}
public static void main(String[] args) throws Exception {
ChineseComedy movie = new ChineseComedy();
TestGeneric<ChineseComedy> test = new TestGeneric(movie);
Class<? extends TestGeneric> cls = test.getClass();
Field t = cls.getDeclaredField("t");
// class xxx.Comedy
System.out.println(t.getType());
// 这里参数只能传Comedy,传其他类型会报找不到该方法
Method method = cls.getDeclaredMethod("test", Comedy.class);
// class xxx.Comedy
System.out.print(method.getReturnType());
}
}
/**
* @author beemo
*/
public class TestGeneric<T extends Chinese & Comedy> {
private T t;
public TestGeneric(T t) {
this.t = t;
}
public <T extends Chinese & Comedy> T test(T t) {
return t;
}
public static void main(String[] args) throws Exception {
ChineseComedy movie = new ChineseComedy();
TestGeneric<ChineseComedy> test = new TestGeneric(movie);
Class<? extends TestGeneric> cls = test.getClass();
Field t = cls.getDeclaredField("t");
// class xxx.Chinese
System.out.println(t.getType());
// 这里参数只能传Chinese,传其他类型会报找不到该方法
Method method = cls.getDeclaredMethod("test", Chinese.class);
// class xxx.Chinese
System.out.print(method.getReturnType());
}
}
下界通配符
的擦除,同无界通配符
,只能确定下界类型,但是上界类型无法确定,所以只能替换为Object
。
public class TestGeneric<T> {
private T t;
public TestGeneric(T t) {
this.t = t;
}
public <T> T test(T t) {
return t;
}
public static void main(String[] args) throws Exception {
Number number = 1.1;
TestGeneric<? super Integer> test = new TestGeneric<>(number);
Class<? extends TestGeneric> cls = test.getClass();
Field t = cls.getDeclaredField("t");
// class java.lang.Object
System.out.println(t.getType());
// 这里参数只能传Object,传其他类型会报找不到该方法
Method method = cls.getDeclaredMethod("test", Object.class);
// class java.lang.Object
System.out.print(method.getReturnType());
}
}
先看以下代码
public interface Generic<T> {
void test(T t);
}
public class SubGeneric implements Generic<String> {
@Override
public void test(String s) {
System.out.println(s);
}
public static void main(String[] args) {
Generic subGeneric = new SubGeneric();
subGeneric.test("1");
}
}
由于类型擦除的存在,所有上例中接口的test
方法到JVM中会被修改为
public void test(Object object);
而实现类SubGeneric
由于定义了的Generic
的泛型类型为String
,所以实现类中的test
方法入参类型为String
,这似乎与父接口中的定义不同,但编译器却没报错。
在查看main方法,首先第一行,使用多态创建了一个子类对象,并使用父接口的引用去指向,然后第二行执行了test方法,按照我们以往的理解,这里入参的类型肯定是Object
,因为类型擦除后,接口最终定义的就是Object
。
由于是多态,最终会运行子类的test
,程序运行后最终也能正确的运行,输出了传入的字符串1。那么如果传入的类型为其他类型,如整型的1,那么会发生什么呢。
“矛盾”点似乎出现了。一是子类与父接口的方法参数类型不一致,二是调用时按理说传入任意类型都可以,但是实际却是传入非子类方法中定义的类型那么会有问题。
使用反射来观察SubGeneric
中的方法信息
Class<?> clz = SubGeneric.class;
Method[] declaredMethods = clz.getDeclaredMethods();
可以看到,多了一个与父接口同样的Object
类型参数的方法,而这个方法不是我们定义的,推断是自动生成的,我们在使用javap -v
指令来查看一下汇编指令。
首先可以看到test(Object)
的flags
比test(String)
的flags
多了两个值,分别为ACC_BRIDGE
、ACC_SYNTHETIC
。官网中描述,ACC_BRIDGE
表示此方法为编译器生成的桥接方法
,ACC_SYNTHETIC
表示此方法由编译器生成,并且在源代码中并不存在。这也就解释了为什么子类与父接口的方法类型不一致,但是程序却可以正常执行,因为编译器自动生成了一个与父类接口参数类型一致的方法。
其次可以看到,生成的桥接方法中代码实现。首先先将参数强制转换为String
类型,然后再调用test(String)
方法,这就是为什么虽然可以传递非String
类型的参数,但是却报转型异常的原因了。
总结:桥接方法
是编译器自动生成的方法,这个方法并不会在源代码中出现。桥接方法
的目的是为了解决由于类型擦除特性导致的方法不一致问题。
需要注意的是,除了此种情况以外,一些其他情况也可能会生成桥接方法
,但这并不属于本篇文章的讨论范围,故不在此展开。
有的时候可能想要在程序中定义泛型的数组,比如
Generic<String>[] generics = new Generic<String>[2];
但是这种方式编译器会报错,有时候我们尝试了很多种方式,就是无法创建一个泛型数组,那么具体怎么创建呢?
首先我们需要知道数组是具有协变性
的,例如
Integer[] intArr = new Integer[2];
Object[] objArr = intArr;
以上的写法是允许的,这就导致了问题的发生
objArr[0] = "123";
以上代码编译可以通过,但是因为向一个Integer
的数组中赋值了字符串类型,所以在运行时会报ArrayStoreException
。
通过泛型的学习,我们知道泛型是不具备协变性的,ArrayList
这种写法无法通过编译。我们从一个泛型容器中获取具体元素的时候,并不需要强制转型,因为必然会获得想要的类型。
结合这两个特点,我们就可以分析出我们想要的答案,那就是Java不支持泛型数组。
让我们来分析一下如果支持泛型数组会发生什么
ArrayList<Integer>[] intListArray = new ArrayList<Integer>[2];
ArrayList<Integer>[] intListArray2 = intListArray;
Object[] objArray = intListArray;
ArrayList<String> strs = new ArrayList<>();
strs.add("123");
objArray[0] = strs;
ArrayList<Integer> intList = intListArray2[0];
Integer a = intList.get(0); // ❶
①处按理说我们可以放心的获取一个Integer的元素,但是实际上获得的却是一个字符串类型,这就会导致类似转型异常的发生,泛型也就失去了意义。所以为了安全考虑,是不支持泛型数组的。
正常情况下,我们是可以放心的使用从泛型容器中获得的元素的,如上文所说,可以确定得到的就是明确的类型。但是有一种情况除外,就是利用“万能”的反射。
如同可以“破坏”单例一样,反射也可以“破坏”泛型。由于反射是运行时生效,而泛型类型的检查是在编译时,如果未定义上界,那么将会擦除为Object,即可以接受任意类型。所以利用反射,我们可以注入其他类型的值。