介绍
引言
Java语言的泛型采用的是 类型擦除法实现的 伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。但是不可能完全擦除,因为运行时还是要知道泛型的真实类型的,那这个真是类型是如何存储的,又如何能拿到?顺着这个问题可以把Java内存划分、类文件结构和对象内存布局串联起来。
泛型的基本概念
Java 泛型(generics)是 JDK 1.5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序在编译期检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
参数化类型的意义
参数化类型的意义是将原来具体的类型参数化,类似方法中的变量参数,此时类型也可以定义成参数形式。
比如JDK集合包的List接口,这里的T可以称为类型形参,在使用/调用时可以传入具体的类型(类型实参)。
泛型的引入实现了在不创建新的类型的情况下,通过泛型指定不同类型来控制形参具体限制的类型。也就是说在泛型的使用过程中,操作的数据类型被指定为了一个类型参数。
泛型可以在哪里使用(语法总结)
泛型可以使用在类、接口、方法和构造器中。
泛型的声明 都是在Class类型后面紧跟一个<>符号,在<>内可以定义1至多个类型参数;泛型可以使用统配符 **?**表示无边界类型,另外可以使用 extends 、super 操作符 分别定义泛型的上界或者下界。
那泛型有什么用(意义)
可能就有小伙伴疑惑了,上面的泛型好像并没有什么用,我直接写下面这种手动擦除的代码不行吗。
1
2
3
4
5
|
public class Manipulator3 {
private HasF obj;
public Manipulator3(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
}
|
这提出了很重要的一点:只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型)更加泛化化时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。
泛型参数和他们在有用的泛型代码中的应用,通常比简单的替换来的更复杂。例如,如果某个类有一个返回T的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型。
例如对于下面两种写法,泛型的写法可以不用强制转换类型。编译器会 在编译期执行类型检查并插入转型代码。理解编译器对泛型的处理非常重要。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// SimpleHolder.java
public class SimpleHolder {
private Object obj;
public void set(Object obj) { this.obj = obj; }
public Object get() { return obj; }
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
Holder.set("Item");
String string = (String)Holder.get();
}
}
// GenericHolder.java
public class GenericHolder<t> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<string> holder = new GenericHolder<>();
holder.set("Item");
String s = holder.get();
}
}
|
其实它们生成的字节码是完全相同的。对进入set()的类型检查是不需要的,因为这是由编译器执行的,而对从get()返回的值进行转型仍旧是需要的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public GenericHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public T get();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class GenericHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method get:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
|
可以看出,使用泛型机制编写的代码要比哪些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型是我们需要的程序设计手段。
泛型的使用和类型擦除
泛型本质上是参数化类型(paramentersized type)的应用,可以声明在类、接口或者方法上。没有泛型时可以用Object
结合强制类型转化实现相似需求,但是运行时风险就明显了,泛型就是用于解决这个问题的。
Java泛型并没有真的新增类型,List<String>和List<Int>
在编译后都是List
,依靠的是类型擦除和强制转换来实现的。
在字节码文件中的形式
这里只简单介绍下相关基础,Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中。
结构中只有无符号数和表两种结构,无符号数描述数字、索引引用、数量值等,表由无符号数和其它表组合。
文件中依次为Class版本、常量池、访问标志、父类信息、字段表、方法表等。类、字段表、方法表等都有属性表,在属性表中有个Signature属性就是用于记录泛型签名信息的。
属性表中有个Code属性,即是方法编译成后的字节码指令序列,泛型擦除仅仅是擦除Code表中的泛型信息。
在内存中的形式
直接指针模型,对象在堆内存中有对象头和对象实例两部分,对象头包括mark word和类型指针两部分。
Java内存模型
泛型信息以参数化类型信息(paramentersized typ)的方式存储在对象类型实例(即方法区中的Class)中,各种泛型实例的类型指针都指向方法区中同一class类型实例。
分类
泛型类
不用泛型的容器类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Container {
private String key;
private String value;
public Container(String k, String v) {
key = k;
value = v;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
|
Container类保存了一对key-value键值对,但是类型是定死的,也就说如果我想要创建一个键值对是String-Integer类型的,当前这个Container是做不到的,必须再自定义。那么这明显重用性就非常低。
当然,我可以用Object来代替String,并且在Java SE5之前,我们也只能这么做,由于Object是所有类型的基类,所以可以直接转型。但是这样灵活性还是不够,因为还是指定类型了,只不过这次指定的类型层级更高而已,有没有可能不指定类型?有没有可能在运行时才知道具体的类型是什么?
这,就是泛型出现的意义!
示例代码:
使用泛型的容器类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Container<K, V> {
private K key;
private V value;
public Container(K k, V v) {
key = k;
value = v;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
|
在编译期,是无法知道K和V具体是什么类型,只有在运行时才会真正根据类型来构造和分配内存。可以看一下现在Container类对于不同类型的支持情况:
1
2
3
4
5
6
7
8
9
10
11
|
public class Test {
public static void main(String[] args) {
Container<String, String> c1 = new Container<String, String>("name", "findingsea");
Container<String, Integer> c2 = new Container<String, Integer>("age", 24);
Container<Double, Double> c3 = new Container<Double, Double>(1.1, 2.2);
System.out.println(c1.getKey() + " : " + c1.getValue());
System.out.println(c2.getKey() + " : " + c2.getValue());
System.out.println(c3.getKey() + " : " + c3.getValue());
}
}
|
输出:
name : findingsea
age : 24
1.1 : 2.2
我们可以在Java类上定义泛型,最常见的比如JDK中的集合类 List
、Map<K,V>
等。
1
2
3
|
public interface List<E> {
boolean add(E e);
}
|
1
2
3
4
5
6
|
public class AbstractList<E> extends List<E>{
public boolean add(E e) {
add(size(), e);
return true;
}
}
|
泛型接口
示例代码:
在泛型接口中,生成器是一个很好的理解,看如下的生成器接口定义:
1
2
3
|
public interface Generator<T> {
public T next();
}
|
然后定义一个生成器类来实现这个接口:
1
2
3
4
5
6
7
8
9
10
|
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
|
调用:
1
2
3
4
5
6
7
8
9
10
|
public class Test {
public static void main(String[] args) {
FruitGenerator generator = new FruitGenerator();
System.out.println(generator.next());
System.out.println(generator.next());
System.out.println(generator.next());
System.out.println(generator.next());
}
}
|
输出:
1
2
3
4
|
Banana
Banana
Pear
Banana
|
泛型方法
一个基本的编程原则是:
无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。
示例代码:
下面来看一个简单的泛型方法的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Test {
public static <T> void out(T t) {
System.out.println(t);
}
public static void main(String[] args) {
out("findingsea");
out(123);
out(11.11);
out(true);
}
}
|
可以看到方法的参数彻底泛化了,这个过程涉及到编译器的类型推导和自动打包,也就说原来需要我们自己对类型进行的判断和处理,现在编译器帮我们做了。这样在定义方法的时候不必考虑以后到底需要处理哪些类型的参数,大大增加了编程的灵活性。
再看一个泛型方法和可变参数的例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class Test {
public static <T> void out(T... args) {
for (T t : args) {
System.out.println(t);
}
}
public static void main(String[] args) {
out("findingsea", 123, 11.11, true);
}
}
|
我们可以在方法上定义泛型,此时该泛型的作用域只在该方法中。
1
2
3
4
5
|
public class Collections {
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
}
|
比如JDK中Collections的sort函数在<>内声明了一个泛型T,在函数的第二个参数的类型声明为Comparator<? super T>,使用了 super 操作符 对Comparator接口的泛形参数做了下界限制,这样任何能够处理T类及其父类的Comparator都可以作为一个合法的参数,拓宽了这个函数的使用场景。
泛型通配符及上下边界
引言
思考这种情形
我们先定义一个水果类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class Fruit {
private String name;
public Fruit(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
|
然后再定义一个苹果类:
1
2
3
4
5
|
public class Apple extends Fruit{
public Apple(String name) {
super(name);
}
}
|
接下来定义一个泛型容器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class GenericHolder<T> {
private T obj;
public GenericHolder(){}
public GenericHolder(T obj){
this.obj = obj;
}
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
|
接下来开始我们的测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public class Test {
/**
* 吃水果
* @param fruitHolder
*/
public static void eatFruit(GenericHolder<Fruit> fruitHolder){
System.out.println("我正在吃 " + fruitHolder.getObj().getName());
}
public static void main(String args[]){
//这是一个贴了水果标签的袋子
GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
//这是一个贴了苹果标签的袋子
GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
//这是一个水果
Fruit fruit = new Fruit("水果");
//这是一个苹果
Apple apple = new Apple("苹果");
//现在我们把水果放进去
fruitHolder.setObj(fruit);
//调用一下吃水果的方法
eatFruit(fruitHolder);
//贴了水果标签的袋子放水果当然没有问题
//现在我们把水果的子类——苹果放到这个袋子里看看
fruitHolder.setObj(apple);
//同样是可以的,其实这时候会发生自动向上转型,apple向上转型为Fruit类型后再传入fruitHolder中
//但不能再将取出来的对象赋值给redApple了
//因为袋子的标签是水果,所以取出来的对象只能赋值给水果类的变量
//无法通过编译检测 redApple = fruitHolder.getObj();
eatFruit(fruitHolder);
//放苹果的标签,自然只能放苹果
appHolder.setObj(apple);
// 这时候无法把appHolder 传入eatFruit
// 因为GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型
// eatFruit(appHolder);
}
}
|
运行结果:
原理
在这里,我们往eatFruit方法里传入fuitHolder的时候,是可以正常编译的,但是如果将appHolder传入,就无法通过编译了,因为作为参数时,GenericHolder<Fruit>
和 GenericHolder<Apple>
是两种不同的类型,所以无法通过编译,那么问题来了,如果我想让eatFruit方法能同时处理GenericHolder<Fruit>
和 GenericHolder<Apple>
两种类型怎么办?而且这也是很合理的需求,毕竟Apple是Fruit的子类,能吃水果,为啥不能吃苹果???如果要把这个方法重载一次,未免也有些小题大做了(而且事实上也无法通过编译,具体原因之后会有说明)。
在代码的逻辑里:
- 苹果 IS-A 水果
- 装苹果的盘子 NOT-IS-A 装水果的盘子
这个时候,泛型的边界符就有它的用武之地了。我们先来看效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class Test {
/**
* 吃水果
* @param fruitHolder
*/
public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){
System.out.println("我正在吃 " + fruitHolder.getObj().getName());
}
public static void main(String args[]){
//这是一个贴了水果标签的袋子
GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
//这是一个贴了苹果标签的袋子
GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
//这是一个水果
Fruit fruit = new Fruit("水果");
//这是一个苹果
Apple apple = new Apple("苹果");
//现在我们把水果放进去
fruitHolder.setObj(fruit);
//调用一下吃水果的方法
eatFruit(fruitHolder);
//放苹果的标签,自然只能放苹果
appHolder.setObj(apple);
// 这时候可以顺利把appHolder 传入eatFruit
eatFruit(appHolder);
}
}
|
运行结果:
这里我们只是使用了一点小小的魔法,把参数类型改成了GenericHolder<? extends Fruit>
,这样就能将 GenericHolder<Apple>
类型的参数顺利传入了,怎么样?很好用吧,这就是泛型的边界符,用<? extends Fruit>的形式表示。边界符的意思,自然就是定义一个边界,这里用?表示传入的泛型类型不是固定类型,而是符合规则范围的所有类型,用extends关键字定义了一个上边界,也就是说这里的?可以代表任何继承于Fruit的类型,你也许会问,为什么是上边界,好问题,一图胜千言:
从这个图可以很好的看出这个“上边界”的概念了吧。有上边界,自然有下边界,泛型里使用形如<? super Fruit>的方式使用下边界,此时,?只能代表Fruit及其父类。
这两种方式基本上解决了我们之前的问题,但是同时,也有一定的限制。
0.无边界的通配符(Unbounded Wildcards), 就是<?>
, 比如Class<?>
。无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
1.固定上边界的通配符(Upper Bounded Wildcards):<? extends T>
不能往里存,只能往外取
可以指定某个类型及其子类类型. 要声明使用该类通配符, 采用 <? extends T>
的形式, 这里的T就是该泛型的上边界。
不要太疑惑,其实很好理解,因为编译器只知道容器里的是Fruit或者Fruit的子类,但不知道它具体是什么类型,所以存的时候,无法判断是否要存入的数据的类型与容器种的类型一致,所以会拒绝set操作。
2.固定下边界的通配符(Lower Bounded Wildcards):<? super T>
往外取只能赋值给Object变量,不影响往里存
可以指代某个类型及其父类类型. 要声明使用该类通配符, 采用<? super E>
的形式, 这里的E就是该泛型的下边界
因为编译器只知道它是Fruit或者它的父类,这样实际上是放松了类型限制,Fruit的父类一直到Object类型的对象都可以往里存,但是取的时候,就只能当成Object对象使用了。
所以如果需要经常往外读,则使用<? extends T>
,如果需要经常往外取,则使用<? super T>
。
协变与逆变
定义
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
f(⋅)是 **逆变(contravariant)**的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是 **协变(covariant)**的,当A≤B时有f(A)≤f(B)成立;
f(⋅)是 **不变(invariant)**的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
数组是协变的
Java中数组是协变的,可以向子类型的数组赋予基类型的数组引用,请看下面代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
try {
fruit[0] = new Fruit();
} catch (Exception e) {
System.out.println(e);
}
try {
fruit[0] = new Orange();
} catch (Exception e) {
System.out.println(e);
}
}
}
|
main()中第一行创建了一个Apple数组,并将其赋值给一个Fruit数组引用。编译器允许你把Fruit放置到这个数组中,这对于编译器是有意义的,因为它是一个Fruit引用——它有什么理由不允许将Fruit对象或者任何从Fruit继承出来的对象(例如Orange),放置到这个数组中呢?
可能有同学会疑惑,明明Fruit[]引用的是一个Apple数组,编译器看不出来吗?还允许往里面放Fruit和Orange类的对象。你要站在编译器的角度看问题,编译器可没有人这么聪明。现代编译器大多采用的是上下文无关文法(编译器:老子归约一句是一句),符号表中存储的标识符fruit是Fruit[]类型(不然咱还怎么多态),在以后的解析过程中编译器看到fruit只会认为是Fruit[]类型。
不过,尽管编译器允许了这样做,运行时的数组机制知道它处理的是Apple[],因此会在向数组中放置异构类型时抛出异常。程序的运行结果如下。
1
2
|
java.lang.ArrayStoreException: generics.Fruit
java.lang.ArrayStoreException: generics.Orange
|
泛型是不变的
当我们使用泛型容器来替代数组时,看看会发生什么。
1
2
3
|
public class NonCovariantGenerics {
List<Fruit> flist = new ArrayList<Apple>(); // 编译错误
}
|
直接在编译时报错了。与数组不同,泛型没有 内建的协变类型。这是因为数组在语言中是完全定义的,因此内建了编译期和运行时的检查,但是在使用泛型时,类型信息在编译期被擦除了(如果你不知道什么是擦除,可以去看这篇文章补补课类型擦除),运行时也就无从检查。因此,泛型将这种错误检测移入到编译期。
通配符引入协变、逆变
泛型的协变和逆变是什么?对应于Java当中,协变对应的就是<? extends XXX>
,而逆变对应的就是<? super XXX>
。
泛型的协变
泛型协变的使用
当我们有一个有方法,方法的签名定义成为如下的方式
1
|
public static void test(List<Number> list)
|
这时,如果我们想要给test方法传入一个List<Double>
或者是List<Integer>
可以吗?很显然不行,因为传递参数,肯定是要传递它的子类才行,但是List<Double>
或者是List<Integer>
是它的子类吗?很明显不是,这时我们就需要用到泛型的协变。
我们将方法的参数变成如下的这种形式
1
|
public static void test(List<? extends Number> list)
|
Java泛型是不变的,可有时需要实现协变,在两个类型之间建立某种类型的向上转型关系,怎么办呢?
这时,如果我们想要给test方法传入一个List<Double>
或者是List<Integer>
可以吗?很显然不行,因为传递参数,肯定是要传递它的子类才行,但是List<Double>
或者是List<Integer>
是它的子类吗?很明显不是,这时通配符派上了用场,我们就需要用到泛型的协变。
我们将方法的参数变成如下的这种形式
1
|
public static void test(List<? extends Number> list)
|
这时,我们的泛型,就只需要传入一个是Number的子类型的泛型即可。因为Integer和Double,它们都是Number的子类,因此很明显是合法的。
1
2
|
test(new ArrayList<Integer>());
test(new ArrayList<Double>());
|
在test方法中:
- 1.如果我们想要去获取集合当中的某个元素时,因为约定了元素的所有类型都得是Number类型极其子类的,因此我们获取的元素一定可以用它们的共同父类Number去进行接收。
- 2.但是当我们想要往集合当中添加元素时,竟然无法往list当中添加元素?奇奇怪怪的!而且关键我们的list,只要求元素的类型是Number或者它的子类类型。但是我们加入的是1,是个Intger类型,很明显是符合规范的呀!
1
2
3
4
|
public static void test(List<? extends Number> list) {
Number number = list.get(0); // right
list.add(1); // error
}
|
泛型协变存在的问题
泛型的协变,不能让我们往集合当中添加元素。那么为什么不能添加呢?
要知道为什么,我们首先需要了解Java当中桥接方法的来由。
Java当中桥接方法的来由
我们首先定义如下的自定义ArrayList类,并重写了它的add方法,
1
2
3
4
5
6
7
|
public class MyArrayList extends ArrayList<Double> {
@Override
public boolean add(Double e) {
return super.add(e);
}
}
|
首先,我们肯定知道ArrayList类中的add方法的原型是下面这样的
1
|
public boolean add(E e)
|
在Java当中,是在编译时去进行类型擦除的,在运行时并无泛型类型一说。也就是说,该原型方法,会被抹掉成为
1
|
public boolean add(Object e)
|
但是,我们定义了自己的ArrayList,我们自己的add方法的原型为
1
|
public boolean add(Double e)
|
这个两个方法的签名并不相同,但是当使用下的代码创建一个ArrayList时:
1
2
|
ArrayList<Double> list = new MyArrayList();
list.add(1.0);
|
它实际调用的方法的原型是public boolean add(Object e)
,但是我们子类中的重写的方法的原型时什么?public booleab add(Double e)
。
也就是说,通过父类的方法调用的和子类重写的方法,并不是同一个方法,因为它们连方法签名都不同。这时候,就需要要一个方式,将public booleab add(Object e)
转到public booleab add(Double e)
当中去执行。这时候,就会涉及到桥接方法的存在了。
Java的实现方式是:通过在Javac编译器编译时,为我们生成一个public boolean add(Object e)
这样的方法,而这个方法当中,要做的实际上就是调用public booleab add(Double e)
这个方法。
1
2
3
|
public boolean add(Object o) {
return add((Double) o);
}
|
通过桥接方法的方式,就可以让我们能在针对泛型方法进行重写时,可以被JVM执行到。
为什么泛型协变时,不允许添加元素呢
例1
1
2
3
4
5
6
7
8
|
public class GenericsAndCovariance {
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<Apple>();
flist.add(new Apple()); // 编译错误
flist.add(new Fruit()); // 编译错误
flist.add(new Object()); // 编译错误
}
}
|
现在flist的类型是<? extends Fruit>
,extends指出了泛型的上界为Fruit,<? extends T>
称为子类通配符,意味着某个继承自Fruit的具体类型。使用通配符可以将ArrayList<Apple>
向上转型了,也就实现了协变。
然而,事情变得怪异了,观察上面代码,你再也不能往容器里放入任何东西,甚至连Apple都不行。
原因在于,List<? extends Fruit>
也可以合法的指向一个List<Orange>
,显然往里面放Apple、Fruit、Object都是非法的。编译器不知道List<? extends Fruit>
所持有的具体类型是什么,所以一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力。
类比数组,尽管你可以把Apple[]向上转型成Fruit[],然而往里面添加Fruit和Orange等对象都是非法的,会在运行时抛出ArrayStoreException异常。
例2
当我们使用下面的代码创建了一个我们自定义的MyArrayList对象。
1
|
ArrayList<Double> list = new MyArrayList();
|
这时,我们调用test方法
test方法对于list的泛型定义为<? entends Number>
,理论上应该是可以往里面放入任何Number
子类类型的元素的。但是别忘了,我们MyArrayList中对于方法的定义,是下面这样子的!
1
2
3
4
5
6
7
|
public boolean add(Object e) {
return add((Double)e);
}
public boolean add(Double e) {
// ......
}
|
如果我们往集合当中添加一个Integer类型的1,走到桥接方法当中时会有(Double)e
这样的强制类型转换,这不就是抛出了ClassCastException
异常了吗?很明显,是不允许我们这样干的。
结论
因此Java的做法就是,泛型把类型检查移到了编译期,在编译期就去禁止这种做法,协变过程丢掉了类型信息,避免产生运行时的ClassCastException
,编译器拒绝所有不安全的操作。
有的人也许会说
1
|
ArrayList<Double> list = new MyArrayList();
|
我们创建list时,不是约束了泛型类型为Double
了吗,为什么test方法内就不能默认它是Double的泛型呢?问题就是:我写test方法时,我怎么知道你传递的是Double
类型的泛型,玩意别人传递的是Integer的泛型呢?所以很明显是行不通的。
从Java字节码的角度去看桥接方法
我们可以看到,Javac编译器,在对Java代码进行编译时,其实针对add方法去生成了两个方法,而它们的访问标识符并不相同。我们自己的方法的访问标识符为0x0001[public]
,而Javac编译器为我们生成的桥接方法的返回值,为0x1041[pubic synthetic bridge]
,多了两个访问标识符synthetic
和bridge
。
我们打开桥接方法的code字节码
我们来分析下字节码
- 1.
aload_0
,众所周知,就是从LocalVariableTable(局部变量表)获取this对象的引用,并压栈。
- 2.
aload_1
,自然就是将传入的元素e的引用压栈。
- 3.
checkcast #3 <java/lang/Double>
,自然是检查能否执行强制类型转换。
- 4.
invokevirtual #4 <com/wanna/generics/java/MyArrayList.add : (Ljava/lang/Double;)Z>
,做到实际上就是从常量池的4号元素当中拿到要执行的方法,也就是我们自己实现的方法。invokevirtual
就是执行目标方法,没毛病。
- 5.
ireturn
,自然就是返回一个int类型的值,为什么是int类型?而不是boolean类型?因为Java当中,在存放到局部变量表和栈中的情况下,int/byte/boolean/char,都是使用的int的形式存放的,占用一个局部变量表的槽位。
我们通过分析得到的信息和我们之前的分析一致,就是通过桥接方法桥接一下,去调用我们自己实现的方法。我们接下来,尝试使用反射的方式去获取到add方法有几个,方法信息是什么。
1
2
3
4
|
Arrays.stream(MyArrayList.class.getMethods()).filter(method -> method.getName().equals("add") && method.getParameterCount() == 1).forEach(method -> {
System.out.printf("方法名为:%s,方法的返回值类型为:%s,方法的参数列表为:%s%n",
method.getName(), method.getReturnType(), Arrays.toString(method.getParameterTypes()));
});
|
代码的最终执行结果为
1
2
|
方法名为:add,方法的返回值类型为:boolean,方法的参数列表为:[class java.lang.Double]
方法名为:add,方法的返回值类型为:boolean,方法的参数列表为:[class java.lang.Object]
|
也就是说,生成的桥接方法,是我们可以通过反射拿到的,它是一个真实的方法。
通过反射拿到Method
之后,我们还可以通过访问标识符判断该方法是否是桥接方法。
1
2
|
method.isBridge()
method.isSynthetic()
|
判断桥接方法,实际上,在Spring框架当中的反射工具类(ReflectionUtils
)当中就有用到,用来判断一个方法是否是用户定义的方法。
泛型逆变
泛型逆变的使用
我们还可以走另外一条路,就是逆变。
泛型逆变的泛型形式是: <? super XXX>
,它的作用是赋值给它的约束容器的泛型类型,只能是 XXX
以及它的父类。
那么我们可以往容器里放入它的子类吗?也许会说,上面不是都说了需要放入的是 XXX
以及它的父类吗,那肯定是不能放入它的子类的呀!但是我们需要想到一个问题,那就是 XXX
的所有子类,其实都是可以隐式转换为 XXX
类型,或者可以直接说,它的子类就是 XXX
类型。
例1
1
2
3
4
5
6
7
|
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit()); // 编译错误
}
}
|
我们重用了关键字super指出泛型的下界为Apple,<? super T>
称为超类通配符,代表一个具体类型,而这个类型是Apple的超类。这样编译器就知道向其中添加Apple或Apple的子类型(例如Jonathan)是安全的了。但是,既然Apple是下界,那么可以知道向这样的List中添加Fruit是不安全的。
例2
我们依次定义三个类
1
2
3
4
5
6
7
8
9
10
11
|
static class Person {
}
static class User extends Person {
}
static class Student extends User {
}
|
接着,定义一个使用逆变的泛型参数的方法
1
|
public static void test(List<? super User> list)
|
上面我们说了,可以接收的容器泛型类型是User以及它的父类,也就是说,容器的泛型可以是User也基于是Person。因此,我们可以传入下面这样的容器给test方法。
1
|
test(new ArrayList<Person>());
|
在test方法当中,我们可以执行下面的才做
1
2
|
list.add(new User()); // 放入User
list.add(new Student()); // 放入User的子类
|
泛型逆变会有什么问题
我们需要想想一个问题:我们使用了逆变约定了,接收的容器的泛型类型是User以及User的父类。我们往容器当中放入的元素,可以是User以及User的子类。也就是说,我们获取容器中的元素时,根本不知道是什么类型, 只能用Object去接收从容器中获取的元素类型,因为只是约定了容器的泛型为User和User的父类,而Object也是它的父类,因此我们甚至可以传入一个容器类型为 ArrayList<object></object>
,我们根本无法决定元素类型的上限,只能用Object去进行接收。
1
|
final Object object = list.get(0);
|
现在又有一个问题:之前协变时,会出现因为执行桥接方法时,发生类型转换异常,在逆变当中会出现这种情况吗?
我们仔细想想,接收的容器泛型类型为User以及User的父类,而可以往容器里存放的是User以及User的子类,也就是说,我们放入到容器中的元素类型,比你原来约束的类型还严格,因为:“User以及User的子类"一定是"User以及User的父类"的子类。也就是说,逆变当中,并不会因为桥接方法中进行的类型导致 ClassCastException
,所以允许add。
对于协变和逆变,有这样的一个原则:称为PECS(Producer Extends Consumer Super)。也就是说:
- 1.Extends应该用在生产者的情况,也就是要根据泛型类型去返回对象的形式。
- 2.Super应该用在消费者的情况,应该传入一个泛型类型的容器,应该利用该容器对数据进行处理,但是不能根据泛型去进行返回,如果要进行返回,只能返回Object,但是这就失去了泛型的意义。
1
2
3
4
5
6
7
|
public static <T> void testCS(List<? super T> list) { // Consumer Super
list.add(...);
}
public static <T> T testPE(List<? extends T> list) { // Producer Extends
return list.get(0);
}
|
PECS
上面说的可能有点绕,那么总结下:什么使用extends,什么时候使用super。《Effective Java》给出精炼的描述: producer-extends, consumer-super(PECS)。
说直白点就是,从数据流来看,extends是限制数据来源的(生产者),也就是要根据泛型类型去返回对象的形式。而super是限制数据流入的(消费者),应该传入一个泛型类型的容器,应该利用该容器对数据进行处理,但是不能根据泛型去进行返回,如果要进行返回,只能返回Object,但是这就失去了泛型的意义。
例如上面SuperTypeWildcards类里,使用<? super Apple>
就是限制add方法传入的类型必须是Apple及其子类型。
仿照上面的代码,我写了个ExtendTypeWildcards类,可以看出<? extends Apple>
限制了get方法返回的类型必须是Apple及其父类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class ExtendTypeWildcards {
static void readFrom(List<? extends Apple> apples) {
Apple apple = apples.get(0);
Jonathan jonathan = apples.get(0); // 编译错误
Fruit fruit = apples.get(0);
}
public static <T> void testCS(List<? super T> list) { // Consumer Super
list.add(...);
}
public static <T> T testPE(List<? extends T> list) { // Producer Extends
return list.get(0);
}
}
|
例子
框架和库代码中到处都是PECS,下面我们来看一些具体的例子,加深理解。
- java.util.Collections的copy方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// Collections.java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
|
copy方法限制了拷贝源src必须是T或者是它的子类,而拷贝目的地dest必须是T或者是它的父类,这样就保证了类型的合法性。
这里我们贴出一小段Rxjava2.0中map函数的源码。
1
2
3
4
5
|
// Observable.java
public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
ObjectHelper.requireNonNull(mapper, "mapper is null");
return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
}
|
Function函数将<? super T>
类型转变为<? extends R>
类型(类似于代理模式的拦截器),可以看出extends和super分别限制输入和输出,它们可以是不同类型。
自限定的类型
理解自限定
Java泛型中,有一个好像是经常性出现的惯用法,它相当令人费解。
1
|
class SelfBounded<T extends SelfBounded<T>> { // ...
|
SelfBounded类接受泛型参数T,而T由一个边界类限定,这个边界就是拥有T作为其参数的SelfBounded,看起来是一种无限循环。
先给出结论:这种语法定义了一个基类,这个基类能够使用子类作为其参数、返回类型、作用域。为了理解这个含义,我们从一个简单的版本入手。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// BasicHolder.java
public class BasicHolder<T> {
T element;
void set(T arg) { element = arg; }
T get() { return element; }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
// CRGWithBasicHolder.java
class Subtype extends BasicHolder<Subtype> {}
public class CRGWithBasicHolder {
public static void main(String[] args) {
Subtype st1 = new Subtype(), st2 = new Subtype();
st1.set(st2);
Subtype st3 = st1.get();
st1.f();
}
}
/* 程序输出
Subtype
*/
|
新类Subtype接受的参数和返回的值具有Subtype类型而不仅仅是基类BasicHolder类型。所以自限定类型的本质就是: 基类用子类代替其参数。这意味着泛型基类变成了一种其所有子类的 公共功能模版,但是在所产生的类中将使用确切类型而不是基类型。因此,Subtype中,传递给set()的参数和从get() 返回的类型都确切是Subtype。
自限定与协变
自限定类型的价值在于它们可以产生 协变参数类型——方法参数类型会随子类而变化。其实自限定还可以产生 协变返回类型,但是这并不重要,因为JDK1.5引入了协变返回类型。
协变返回类型
下面这段代码子类接口把基类接口的方法重写了,返回更确切的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// CovariantReturnTypes.java
class Base {}
class Derived extends Base {}
interface OrdinaryGetter {
Base get();
}
interface DerivedGetter extends OrdinaryGetter {
Derived get();
}
public class CovariantReturnTypes {
void test(DerivedGetter d) {
Derived d2 = d.get();
}
}
|
继承自定义类型基类的子类将产生确切的子类型作为其返回值,就像上面的get()一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// GenericsAndReturnTypes.java
interface GenericsGetter<T extends GenericsGetter<T>> {
T get();
}
interface Getter extends GenericsGetter<Getter> {}
public class GenericsAndReturnTypes {
void test(Getter g) {
Getter result = g.get();
GenericsGetter genericsGetter = g.get();
}
}
|
协变参数类型
在非泛型代码中,参数类型不能随子类型发生变化。方法只能 重载不能 重写。见下面代码示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// OrdinaryArguments.java
class OrdinarySetter {
void set(Base base) {
System.out.println("OrdinarySetter.set(Base)");
}
}
class DerivedSetter extends OrdinarySetter {
void set(Derived derived) {
System.out.println("DerivedSetter.set(Derived)");
}
}
public class OrdinaryArguments {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedSetter ds = new DerivedSetter();
ds.set(derived);
ds.set(base);
}
}
/* 程序输出
DerivedSetter.set(Derived)
OrdinarySetter.set(Base)
*/
|
但是,在使用自限定类型时,在子类中只有一个方法,并且这个方法接受子类型而不是基类型为参数。
1
2
3
4
5
6
7
8
9
10
11
12
|
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T args);
}
interface Setter extends SelfBoundSetter<Setter> {}
public class SelfBoundAndCovariantArguments {
void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
s1.set(sbs); // 编译错误
}
}
|
捕获转换
<?>
被称为无界通配符,无界通配符有什么作用这里不再详细说明了,理解了前面东西的同学应该能推断出来。无界通配符还有一个特殊的作用,如果向一个使用<?>
的方法传递原生类型,那么对编译期来说,可能会推断出实际的参数类型,使得这个方法可以回转并调用另一个使用这个确切类型的方法。这种技术被称为捕获转换。下面代码演示了这种技术。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public class CaptureConversion{
static <T> void f1(Holder<T> holder){
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder){
f1(holder);
}
// @SuppressWarnings("unchecked")
public static void main(String[] args){
Holder raw = new Holder<Integer>(1);
f1(raw);//Unchecked invocation f1(Holder) of the generic method f1(Holder<T>) of type
f2(raw);//No warnings
Holder rawBasic = new Holder();
rawBasic.set(new Object());//Type safety: The method set(Object) belongs to the raw type Holder. References to generic type Holder<T> should be parameterized
f2(rawBasic);//No warnings
//Upcast to Holder<?>, still figures out:
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
}
/* 程序输出
Integer
Integer
Object
Double
*/
|
获转换允许我们在处理通配符时绕开编译器的限制。当f2() 调用 f1() 时,它知道这么做是安全的,因为它自身的 holder 参数对一些未知的 V 而言一定是Holder<V>
。同时类型参数 T 被引入到方法签名中并且没有绑定到其他任何类型参数,它也可以表示任何未知类型,因此,某些未知 T 的 Holder<T>
也可能是某些未知 V 的 Box<V>
。
f1()中的类型参数都是确切的,没有通配符或者边界。在f2()中,Holder参数是一个无界通配符,因此它看起来是未知的。但是,在f2()中,f1()被调用,参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。
捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。
注意:不能从f2()中返回T,因为T对于f2()来说是未知的。
一般来说,带有通配符的 API 比带有泛型方法的 API 更简单,在更复杂的方法声明中类型名称的增多会降低声明的可读性。因为在需要时始终可以通过专有的捕获转换来恢复名称,这个方法可以保持 API 整洁,同时不会删除有用的信息。
捕获转换十分有趣,但是非常受限。
引入泛型后Java的类型体系
在引入泛型之前,Java只有所谓的原始类型(raw types),即在此之前所有的类类型都可以通过Class类进行抽象,Class 类的一个具体对象就代表一个指定的原始类型。
在引入泛型之后,为了将泛型类跟原始类型区分,Java引入了Type类型体系,在新的Type类型体系下,包含了
Class(原生类型)、ParameterizedType(参数化类型)、TypeVariable(类型变量类型)、WildcardType(包含通配符的类型)、GenericArrayType(泛型数组类型)、
泛型的相关类型
ParameterizedType
ParameterizedType(参数化类型)形如Class<Type…>,比如 Collection 就是一个参数化类型。
TypeVariable
TypeVariable 表示的是类型变量类型,比如List类型是参数化类型,这个参数化类型的第1个位置的参数类型是 T,这里的T类型是类型变量类型。
GenericArrayType
GenericArrayType表示的是参数化类型或者类型变量的数组类型,形如:A[]或T[]的类型。
WildcardType
包含通配符的类型,形如 ? 、 ? extends Number, ? super Long
比如List<? extends User>类型是一个WildcardType类型。
泛型的编译期检查
这里简单演示下,由泛型参数带来的编译时检查的特性。
在下面的代码中,我们创建了一个List,并添加Integer对象。
1
2
3
4
5
6
|
public class GenericTest{
public static void main(String[] args) {
List array = new ArrayList<>();
array.add(1);
}
}
|
因为List接口是一个参数化类型
1
2
3
4
5
|
public interface List<E> extends Collection<E> {
//...
boolean add(E e);
}
|
我们可以使用泛型改造一下最初的代码,我们希望List只接收Integer类型的对象,因此我们可以在定义List类型时加上泛型参数
1
2
3
4
5
6
7
8
9
10
11
|
package sample1;
import java.util.ArrayList;
import java.util.List;
public class GenericTest{
public static void main(String[] args) {
List<Integer> array = new ArrayList<>();
array.add("1");
}
}
|
很明显 array.add(“1”) 是一段错误的代码,如果能够正常编译通过,那么在运行时可能会带来意想不到的结果,比如当我们从List中取出参数时 强转成成Integer类型会出现类型转化的异常。
我们先尝试使用 javac 编译文件,幸好编译器提前给出了代码的错误
1
2
3
4
5
6
7
8
9
10
11
12
13
|
GenericTest.java:10: 错误: 对于add(int), 找不到合适的方法
array.add(1);
^
方法 Collection.add(String)不适用
(参数不匹配; int无法转换为String)
方法 List.add(String)不适用
(参数不匹配; int无法转换为String)
方法 AbstractCollection.add(String)不适用
(参数不匹配; int无法转换为String)
方法 AbstractList.add(String)不适用
(参数不匹配; int无法转换为String)
方法 ArrayList.add(String)不适用
(参数不匹配; int无法转换为String)
|
类型擦除
引言C++模板与Java泛型
C++模板
下面是使用模板的C++示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include <iostream>
class HasF {
public:
void f() { std::cout << "HasF::f()" << std::endl; }
};
template<class T> class Manipulator {
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
int main(int argc, const char * argv[]) {
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
return 0;
}
|
程序运行无误,输出
当我们调用一个模板时,C++编译器用实参来为我们推断模板实参,并为我们 实例化(instantiate)一个特定版本的代码。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新"实例”。被编译器生成的版本通常称为模板的 实例。
对于上面代码,在编译时,编译器会将T替换成HasF并生成模板实例,类似于这样。
1
2
3
4
5
6
|
class Manipulator {
HasF obj;
public:
Manipulator(HasF x) { obj = x; }
void manipulate() { obj.f(); }
};
|
Java泛型有点不太一样
用C++编写这种代码很简单,因为当模板实例化时,模板代码知道其模板参数的类型,Java泛型就不同了。下面是HasF的Java版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// HasF.java
public class HasF {
public void f() { System.out.println("HasF.f()"); }
}
// Manipulation.java
class Manipulator<T> {
private T obj;
public Manipulator(T x) { obj = x; }
public void manipulate() { obj.f(); } // 编译错误
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator = new Manipulator(hf);
manipulator.manipulate();
}
}
|
上面代码不能编译,报错
1
2
|
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method f() is undefined for the type T
|
从上面报错信息可以看出,编译器认为类型T没有f()方法。这是由于Java的泛型是使用擦除来实现的,意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
从表面上看,Java的泛型类类似于C++的模板类,唯一明显的不同是Java没有专用的template关键字,但是,其实 这两种机制着本质的区别。
在第一个示例中,博主演示了 C++ 和 Java 中的泛型在编译时的不同表现。其中的关键就在于泛型 T 能否调用方法 f。C++ 认为这是可行的,即使我们没有对 T 做出任何约束。Java 认为这是不可行的,因为 T 没有受到约束。这种区别的实质是啥,实质是 C++ 里面的模版是个爹,是除了类、接口这这两大爹之外的一个新爹,T 是规则的制定者。一个类如果想做 T,就需要实现 T 在模板里面用到的所有方法,即类需要实现模板。实现模板是一种对类的约束,这种约束与继承父类、实现接口是处于同一层面的。Java 不一样,它没有模版这个爹,类永远只对接口、父类负责,T 是规则的遵守者,所以不能凭空调用对 T 来说并不存在的 f。
本文为对Java泛型技术类型擦除部分的一个总结
介绍
类型擦除机制(编译时擦除为Object)
- 使用泛型的时候加上的类型参数,泛型类型只有在 静态类型检查期间才出现,在此之后,会在编译的时候,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。(在生成的Java字节码中是不包含泛型中的类型信息的)。这个过程就称为类型擦除。
优势
- 实现简单
- 容易Backport,最开始的版本是不支持泛型的,Java 1.5 才开始引入泛型,为了兼容以前的库而不得不使用擦除法,是一种折中
- 运行期节省一些类型所占的内存空间
缺陷
- 实现的泛型远不如真泛型灵活和强大
- 基本类型无法作为泛型实参(因为编译时被擦除为Object类型),只能用包装类型,装箱开箱有开销
- 泛型类型无法当做真实的类型使用,因为编译后的泛型为Object类型
- 泛型类型无法用方法重载,因为编译后都是List list
1
2
|
public void print(List<Integer> list){ }
public void print(List<String> list){ }
|
- 静态方法无法引用类泛型参数(可以给静态方法单独加上泛型参数),因为只有类实例化的时候才会知道泛型参数,而静态方法不需要持有类的实例
- 类型强转时的开销(Object强转到对应的类型)
代码片段1(if一致)
1
2
3
4
5
|
Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);
//输出:true
|
尽管ArrayList<String>
和ArrayList<Integer>
看上去或在使用中,是完全不同的类型,但是上面的程序会的的确确认为它们是相同的类型,会输出true。ArrayList<String>
和ArrayList<Integer>
在运行时事实上是相同的类型。这两种类型都被擦除成它们的“原生”类型,即ArrayList。无论何时,编写泛型代码时,必须提醒自己“它的类型被擦除了”。
这就是Java泛型的类型擦除造成的!
因为不管是ArrayList<Integer>()
还是ArrayList<String>()
,都在编译器被编译器擦除成了ArrayList。那编译器为什么要做这件事?原因也和大多数的Java让人不爽的点一样——兼容性。由于泛型并不是从Java诞生就存在的一个特性,而是等到SE5才被加入的,所以为了兼容之前并未使用泛型的类库和代码,不得不让编译器擦除掉代码中有关于泛型类型信息的部分,这样最后生成出来的代码其实是『泛型无关』的,我们使用别人的代码或者类库时也就不需要关心对方代码是否已经『泛化』,反之亦然。
在编译器层面做的这件事(擦除具体的类型信息),使得Java的泛型先天都存在一个让人非常难受的缺点:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
如果想要运行上面类型擦除引言部分Java版本的HasF,必须协助泛型类,给定泛型的边界,以告知编译器只能接收遵循这个边界的类型。这里重用了 extends关键字(与类的继承有点类似,但又不完全相同),给出类型的 上界。之所以称为上界,是通过继承树来考虑的,对于继承树父节点在上,子节点在下,那么extends关键字就限定了类型最多能上了继承树的什么地方,也就是上界。由于有了边界,下面的代码就可以编译了。
1
2
3
4
5
|
public class Manipulator2<t extends hasf> {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
}
|
泛型类型参数将擦除到它的第一个边界(可以有多个边界)。编译时,Java编译器会将T擦除成HasF,就好像在类的声明中用HasF替换了T一样。
代码片段2(Type为泛型)
1
2
3
4
5
6
7
|
List<Integer> list = new ArrayList<Integer>();
Map<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
//输出:
[E] [K, V]
|
关于getTypeParameters()的解释:
Returns an array of TypeVariable objects that represent the type variables declared by the generic declaration represented by this GenericDeclaration object, in declaration order. Returns an array of length 0 if the underlying generic declaration declares no type variables.
我们期待的是得到泛型参数的类型,但是实际上我们只得到了一堆占位符。
代码片段3(泛型不能实例化)
1
2
3
4
5
6
7
|
public class Main<T> {
public T[] makeArray() {
// error: Type parameter 'T' cannot be instantiated directly
return new T[5];
}
}
|
我们无法在泛型内部创建一个T类型的数组,原因也和之前一样,T仅仅是个占位符,并没有真实的类型信息,实际上,除了new表达式之外,instanceof操作和转型(会收到警告)在泛型内部都是无法使用的,而造成这个的原因就是之前讲过的编译器对类型信息进行了擦除。
同时,面对泛型内部形如T var;的代码时,记得多念几遍:它只是个Object,它只是个Object……
代码片段4(内部一致性,边界动作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Main<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Main<String> m = new Main<String>();
m.set("findingsea");
String s = m.get();
System.out.println(s);
}
}
//输出:findingsea
|
虽然有类型擦除的存在,使得编译器在泛型内部其实完全无法知道有关T的任何信息,但是编译器可以保证重要的一点:
内部一致性,也是我们放进去的是什么类型的对象,取出来还是相同类型的对象,这一点让Java的泛型起码还是有用武之地的。
代码片段四展现就是编译器确保了我们放在t上的类型的确是T(即便它并不知道有关T的任何类型信息)。这种确保其实做了两步工作:
set() 处的类型检验
get() 处的类型转换
这两步工作也成为边界动作。
代码片段5(内部一致性,输入输出)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class Main<T> {
public List<T> fillList(T t, int size) {
List<T> list = new ArrayList<T>();
for (int i = 0; i < size; i++) {
list.add(t);
}
return list;
}
public static void main(String[] args) {
Main<String> m = new Main<String>();
List<String> list = m.fillList("findingsea", 5);
System.out.println(list.toString());
}
}
//输出:[findingsea, findingsea, findingsea, findingsea, findingsea]
|
代码片段5同样展示的是泛型的内部一致性。
字节码分析
我们简单了解了方法中的泛型的使用会在编译期进行类型检查,下面我们进一步研究下在字节码层面泛型的信息。
在方法体中使用声明并创建了2个泛型对象
1
2
3
4
5
6
7
8
9
10
|
package sample1;
import java.util.ArrayList;
import java.util.List;
public class GenericTest{
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
}
}
|
使用javac
命令 编译该java文件 生成 GnericTest.class
如果是在Intellij环境下,可以直接打开该class文件
可以看到在class文件中,方法内部并不存在泛型信息,我们可以简单的得出结论:在方法体中的泛型信息在编译后会被擦除。
我们稍微修改一下代码,在代码中尝试向List 列表中插入数据后取出元素
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package sample1;
import java.util.ArrayList;
import java.util.List;
public class GenericTest<T>{
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
strList.add("hello");
String s = strList.get(0);
}
}
|
再次编译该代码,并使用Intellij打开class文件,此时可以看到,调用List类型的add方法时,编译器并没有做出任何改动,但是在取出元素时,编译器会自动强转成String类型。
从字节码的层级来看,实际上编译器所做的额外工作是在取出元素后,加入了一个 checkcast class java/lang/String(类型转化)指令。
读者可以通过 javap -v GenericTest
查看 GnericTest类文件可以看到下面的指令
1
2
3
|
27: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
32: checkcast #7 // class java/lang/String
35: astore_3
|
擦除的补偿(解决擦除问题)
问题示例
擦除丢失了在泛型代码中执行某些操作的能力。任何运行时都需要知道确切的类型信息的操作都无法工作。
1
2
3
4
5
6
7
8
|
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
if (arg instanceof T) {} // 编译错误
T var = new T(); // 编译错误
T[] array = new T[SIZE]; // 编译错误
}
}
|
如上看到的,但凡是涉及到确切类型信息的操作,在泛型内部都是无法工作的。那是否有办法绕过这个问题来编程,答案就是:
显式地传递类型标签。
下面给出一些方法解决上面的问题。
引入类型标签,使用动态的isInstance()代替instanceof
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class Building {}
class House extends Building {}
public class ClassTypeCapture<t> {
Class<t> kind;
public ClassTypeCapture(Class<t> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<building> ctt1 = new ClassTypeCapture<>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<house> ctt2 = new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
</house></building></t></t></t>
|
运行结果
1
2
3
4
|
true
true
false
true
|
代码片段6(Class.newInstance)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class Main<T> {
public T create(Class<T> type) {
try {
return type.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
Main<String> m = new Main<String>();
String s = m.create(String.class);
}
}
|
代码片段6展示了一种用类型标签生成新对象的方法,但是这个办法很脆弱,因为这种办法要求对应的类型必须有默认构造函数,遇到Integer类型的时候就失败了,而且这个错误还不能在编译器捕获。
进阶的方法可以用限制类型的显式工厂和模板方法设计模式来改进这个问题,具体可以参见《Java编程思想 (第4版)》P382。
用工厂方法或模版方法创建类型实例
下面的两段代码是学习设计模式的好材料。先来看工厂方法模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
interface FactoryI<T> { T create(); }
class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
}
}
class IntegerFactory implements FactoryI<Integer> {
public Integer create() {
return new Integer(0);
}
}
class Widget {
public static class Factory implements FactoryI<Widget> {
public Widget create() {
return new Widget();
}
}
}
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widget>(new Widget.Factory());
}
}
|
模板方法模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
abstract class GenericWithCreate<T> {
final T element;
public GenericWithCreate() { element = create(); }
abstract T create();
}
class X {}
class Creater extends GenericWithCreate<X> {
X create() { return new X(); }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
public class CreatorGeneric {
public static void main(String[] args) {
Creater c = new Creater();
c.f();
}
}
|
代码片段7(用ArrayList代替数组,或者是传入类型标记)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[])Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
Integer[] ia = gai.rep();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
//public class Main<T> {
//
// public T[] create(Class<T> type) {
// return (T[]) Array.newInstance(type, 10);
// }
//
// public static void main(String[] args) {
// Main<String> m = new Main<String>();
// String[] strings = m.create(String.class);
// }
//}
|
代码片段7展示了对泛型数组的擦除补偿,本质方法还是通过显式地传递类型标签,通过Array.newInstance(type, size)来生成数组,同时也是最为推荐的在泛型内部生成数组的方法。
真的完全擦除了吗
在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。
字节码分析签名
泛型签名信息
在上文中 方法中使用泛型的例子中,我们发现在方法中的泛型在编译后会被擦除。那么在泛型类、泛型方法中的泛型信息也会被擦除吗?实际上Java在引入泛型时,考虑了这个问题,为了能够在Class文件中保存泛型信息,在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。
编写一个简单的泛型示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import java.util.ArrayList;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class GenericTest<T extends Number, K > {
private T param1;
private K param2;
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
strList.add("hello");
String s = strList.get(0);
}
private T genericMethod() {
return null;
}
private T genericMethod1(T p1, K p2,String s) {
return null;
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
}
}
|
运行
编译生成 GenericTest.class文件
运行
1
|
javap -v -p GenericTest
|
查看GenericTest.class文件中的字节码信息
以下是生成的GenericTest.class的字节码信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
public class GenericTest<T extends java.lang.Number, K extends java.lang.Object> extends java.lang.Object
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#35 // java/lang/Object."<init>":()V
#2 = Class #36 // java/util/ArrayList
#3 = Methodref #2.#35 // java/util/ArrayList."<init>":()V
#4 = String #37 // hello
#5 = InterfaceMethodref #38.#39 // java/util/List.add:(Ljava/lang/Object;)Z
#6 = InterfaceMethodref #38.#40 // java/util/List.get:(I)Ljava/lang/Object;
#7 = Class #41 // java/lang/String
#8 = Class #42 // GenericTest
#9 = Class #43 // java/lang/Object
#10 = Utf8 param1
#11 = Utf8 Ljava/lang/Number;
#12 = Utf8 Signature
#13 = Utf8 TT;
#14 = Utf8 param2
#15 = Utf8 Ljava/lang/Object;
#16 = Utf8 TK;
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 genericMethod
#24 = Utf8 ()Ljava/lang/Number;
#25 = Utf8 ()TT;
#26 = Utf8 genericMethod1
#27 = Utf8 (Ljava/lang/Number;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Number;
#28 = Utf8 (TT;TK;Ljava/lang/String;)TT;
#29 = Utf8 sort
#30 = Utf8 (Ljava/util/List;Ljava/util/Comparator;)V
#31 = Utf8 <T:Ljava/lang/Object;>(Ljava/util/List<TT;>;Ljava/util/Comparator<+TT;>;)V
#32 = Utf8 <T:Ljava/lang/Number;K:Ljava/lang/Object;>Ljava/lang/Object;
#33 = Utf8 SourceFile
#34 = Utf8 GenericTest.java
#35 = NameAndType #17:#18 // "<init>":()V
#36 = Utf8 java/util/ArrayList
#37 = Utf8 hello
#38 = Class #44 // java/util/List
#39 = NameAndType #45:#46 // add:(Ljava/lang/Object;)Z
#40 = NameAndType #47:#48 // get:(I)Ljava/lang/Object;
#41 = Utf8 java/lang/String
#42 = Utf8 GenericTest
#43 = Utf8 java/lang/Object
#44 = Utf8 java/util/List
#45 = Utf8 add
#46 = Utf8 (Ljava/lang/Object;)Z
#47 = Utf8 get
#48 = Utf8 (I)Ljava/lang/Object;
{
private T param1;
descriptor: Ljava/lang/Number;
flags: ACC_PRIVATE
Signature: #13 // TT;
private K param2;
descriptor: Ljava/lang/Object;
flags: ACC_PRIVATE
Signature: #16 // TK;
public GenericTest();
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 5: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, 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: new #2 // class java/util/ArrayList
11: dup
12: invokespecial #3 // Method java/util/ArrayList."<init>":()V
15: astore_2
16: aload_1
17: ldc #4 // String hello
19: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
24: pop
25: aload_1
26: iconst_0
27: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
32: checkcast #7 // class java/lang/String
35: astore_3
36: return
LineNumberTable:
line 12: 0
line 13: 8
line 14: 16
line 15: 25
line 16: 36
private T genericMethod();
descriptor: ()Ljava/lang/Number;
flags: ACC_PRIVATE
Code:
stack=1, locals=1, args_size=1
0: aconst_null
1: areturn
LineNumberTable:
line 19: 0
Signature: #25 // ()TT;
private T genericMethod1(T, K, java.lang.String);
descriptor: (Ljava/lang/Number;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Number;
flags: ACC_PRIVATE
Code:
stack=1, locals=4, args_size=4
0: aconst_null
1: areturn
LineNumberTable:
line 23: 0
Signature: #28 // (TT;TK;Ljava/lang/String;)TT;
public static <T extends java.lang.Object> void sort(java.util.List<T>, java.util.Comparator<? extends T>);
descriptor: (Ljava/util/List;Ljava/util/Comparator;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 28: 0
Signature: #31 // <T:Ljava/lang/Object;>(Ljava/util/List<TT;>;Ljava/util/Comparator<+TT;>;)V
}
Signature: #32 // <T:Ljava/lang/Number;K:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "GenericTest.java"
|
类上的泛型签名信息
由于GenericTest类声明了泛型类型 T、K 因此,在打印的字节码内容尾部有 signature属性为该类记录泛型签名信息 s
1
|
Signature: #33 // <T:Ljava/lang/Number;K:Ljava/lang/Object;>Ljava/lang/Object;
|
成员变量上的泛型签名信息
类的成员变量 param1 param2 类型为泛型,因此 字段信息中会有 **signature属性记录该字段的泛型签名信息
1
2
3
4
5
6
7
8
9
|
private T param1;
descriptor: Ljava/lang/Number;
flags: ACC_PRIVATE
Signature: #14 // TT;
private K param2;
descriptor: Ljava/lang/Object;
flags: ACC_PRIVATE
Signature: #17 // TK;
|
这里的TT TK 第一个T表示该类型是泛型类型,T后面跟随着泛型的标识。
方法上的泛型签名信息
类的成员方法 genericMethod()方法返回类型为泛型 和 genericMethod1(T p1, K p2)
方法的参数及返回类型为泛型类,因此,在字节码中这两个方法的信息中也会有Signature记录该方法包含泛型的签名信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
private T genericMethod();
descriptor: ()Ljava/lang/Number;
flags: ACC_PRIVATE
Code:
stack=1, locals=1, args_size=1
0: aconst_null
1: areturn
LineNumberTable:
line 19: 0
Signature: #26 // ()TT;
private T genericMethod1(T, K, java.lang.String);
descriptor: (Ljava/lang/Number;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Number;
flags: ACC_PRIVATE
Code:
stack=1, locals=4, args_size=4
0: aconst_null
1: areturn
LineNumberTable:
line 23: 0
Signature: #29 // (TT;TK;Ljava/lang/String;)TT;
|
方法的泛型签名由两部分组成,分别是方法参数和方法返回类型;下面会简单的解析下这两个方法的签名组成.
首先方法的签名信息由两部分组成,分别是方法的参数部分和返回类型部分。
1.genericMethod方法
1
|
private T genericMethod()
|
genericMethod()
方法的签名信息为 ()TT;
方法参数部分的签名为(),因为该方法没有任何参数
方法的返回类型为泛型类型T,因此方法返回类型部分的签名为TT
2.genericMethod1方法
1
|
private T genericMethod1(T p1, K p2,String s)
|
该方法的参数部分签名为(TT;TK;Ljava/lang/String;)
,因为方法分别有3个参数,其中泛型化类型T在方法签名中的标识为 TT ,泛型类型K的标识为 TK ,String类型为 Ljava/lang/String
,类型直接以“;”作为分隔。(引用数据类型在字节码中的标识的格式为LClassName,其中ClassName为类的全路径名称,全路径名称中的“.”号由“/”代替,最终方法签名的参数部分为(TT;TK;Ljava/lang/String;)
。
该方法的返回类型为泛型T,因此返回类型部分为TT
静态方法的上的泛型签名信息
泛型静态方法中,泛型的类型不能直接引用类上的泛型类型,静态方法如果要使用泛型,必须自己定义泛型类型,比如这里 sort方法的泛型类型T是在通过方法的 声明的,这里的泛型类型T跟类的上的泛型类型T并不是同一个。
我们看下该静态方法的签名信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
public static <T extends java.lang.Object> void sort(java.util.List<T>, java.util.Comparator<? super T>);
descriptor: (Ljava/util/List;Ljava/util/Comparator;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokeinterface #8, 2 // InterfaceMethod java/util/List.sort:(Ljava/util/Comparator;)V
7: return
LineNumberTable:
line 28: 0
line 29: 7
Signature: #32 // <T:Ljava/lang/Object;>(Ljava/util/List<TT;>;Ljava/util/Comparator<-TT;>;)V
|
签名信息为
1
|
<T:Ljava/lang/Object;>(Ljava/util/List<TT;>;Ljava/util/Comparator<-TT;>;)V
|
注意这里签名信息的开头部分,在函数参数的标识部分的左边提前描述了泛型类型T的签名信息 <T:Ljava/lang/Object;>。 另外函数参数
Comparator<? super T>在方法签名中的标识为Ljava/util/Comparator<-TT;>
,如果这里的super变为extends则签名信息变为Ljava/util/Comparator<+TT;>
。因为super操作符在字节码中的标识为符号 -,extends操作符在字节码中的标识为符号 +。
获取签名信息
附加的签名信息特定场景下反射可以获取
-
获取泛型类型
我们已经介绍了泛型的相关类型,以及泛型在字节码中的信息。为了在运行时获取泛型的类型,Java提供相应的api实现。
首先我们上文介绍了,包括原生类型及泛型类型,所有的类型都可以用Type接口表示。 因此我写了一个工具方法打印所有具体Type类型的具体信息。有关这些Type类型的具体api信息可以直接查看jdk上这些类的注释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
public static void printTypeInfo(Type argumentType) {
if (argumentType instanceof Class<?>) {
System.out.println("原生类型-> " + ((Class) argumentType).getCanonicalName());
} else if (argumentType instanceof ParameterizedType) {
System.out.println("参数化类型-> " + ((ParameterizedType) argumentType).toString());
Type[] actualTypeArguments = ((ParameterizedType) argumentType).getActualTypeArguments();
System.out.println(" 参数化类型中的泛型参数信息如下:");
for (int i = 0; i < actualTypeArguments.length; i++) {
Type actualTypeArgument = actualTypeArguments[i];
System.out.println(" 第" + i + "个泛型参数类型为");
System.out.print(" ");
printTypeInfo(actualTypeArgument);
}
} else if (argumentType instanceof GenericArrayType) {
System.out.println("泛型类型数组-> " + ((GenericArrayType) argumentType).toString());
System.out.print(" 泛型类型数组存储的类型为");
printTypeInfo(((GenericArrayType) argumentType).getGenericComponentType());
} else if (argumentType instanceof TypeVariable) {
System.out.println("类型变量类型-> " + ((TypeVariable) argumentType).getName());
} else if (argumentType instanceof WildcardType) {
System.out.println("包含通配符的类型-> " + ((WildcardType) argumentType).toString());
Type[] upperBounds = ((WildcardType) argumentType).getUpperBounds();
if (upperBounds != null) {
System.out.println(" 通配符的上界包括:");
for (int j = 0; j < upperBounds.length; j++) {
System.out.println(" " + upperBounds[j].getTypeName());
}
}
Type[] lowerBounds = ((WildcardType) argumentType).getLowerBounds();
if (lowerBounds != null) {
System.out.println(" 通配符的下界包括:");
for (int j = 0; j < lowerBounds.length; j++) {
System.out.println(" " + lowerBounds[j].getClass());
}
}
}
}
|
以下这些api获取的泛型数据的来源就是class文件字节码中 类及方法上的signature的信息。
Gson中泛型签名的应用
new TypeToken<List>() { }.getType()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Test
public void test5() {
Gson gson = new Gson();
User user1 = new User(10, "bob");
User user2 = new User(12, "jack");
ArrayList<User> users = new ArrayList<>();
users.add(user1);
users.add(user2);
//序列化(Java对象转换为字节序列的过程)
String json = gson.toJson(users);
System.out.println(json);
//反序列化(把字节序列恢复为Java对象的过程)
List<User> userList = gson.fromJson(json,
new TypeToken<List<User>>() {
}.getType());
System.out.println(userList);
}
/*
* [{"age":10,"name":"bob"},{"age":12,"name":"jack"}]
* [User(age=10, name=bob), User(age=12, name=jack)]
*/
|
- Gson中的源码
创建子类可以获取到父类的泛型信息,所以需要创建一个匿名子类
new TypeToken<List>() { }.getType()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/*
客户端创建一个空的匿名子类。
这样做会将类型参数嵌入到匿名类的类型层次结构中,这样我们就可以在运行时重建它,而不用考虑擦除。
创建子类可以获取到父类的泛型信息,所以需要创建一个匿名子类new TypeToken<List<User>>() { }.getType()
*/
protected TypeToken() {
this.type = getSuperclassTypeParameter(getClass());//此处获取type泛型的信息
this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
this.hashCode = type.hashCode();
}
//获取泛型类型
static Type getSuperclassTypeParameter(Class<?> subclass) {
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
}
ParameterizedType parameterized = (ParameterizedType) superclass;
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
}
|
泛型混淆,签名问题,混淆后签名找不到了,导致反射后拿不到
-
保留签名信息
1
|
-keepattributes Signature
|
-
Kotlin中
1
|
-keep class kotlin.Metadata {*;}
|
retrofit库对泛型的应用
之前写过一篇Retrofit框架的文章,简单的介绍了Retrofit库的组成部分,其中Retrofit一个很优雅的部分就是内部自动会跟你局接口方法所定义的Model类型自动做JSON类型转换。
我们可以简单演示下这里的泛型应用场景,获取方法返回类型上的泛型参数的具体类型。
在下面的例子中,我简单仿写了使用了Retrofit库在项目中的工程代码,在main方法打印Service接口中方法的返回泛型类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class GenericTest{
static interface Call<T> {
}
static class User {
}
static class Post {
}
static interface Service {
Call<User> login(String account, String password);
Call<List<Post>> getPosts();
}
public static void main(String[] args) {
System.out.println("开始打印Service接口的方法信息");
Method[] methods = Service.class.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
//方法的getReturnType返回的是Class类型,如果希望能够获取到泛型信息,则应该使用JDK 1.5 之后的 getGenericReturnType方法
Class<?> returnType = method.getReturnType();
Type genericReturnType = method.getGenericReturnType();
System.out.print(method.getName() + "方法的返回");
printTypeInfo(genericReturnType);
System.out.println();
}
}
public static void printTypeInfo(Type argumentType) {
if (argumentType instanceof Class<?>) {
System.out.println("原生类型-> " + ((Class) argumentType).getCanonicalName());
} else if (argumentType instanceof ParameterizedType) {
System.out.println("参数化类型-> " + ((ParameterizedType) argumentType).toString());
Type[] actualTypeArguments = ((ParameterizedType) argumentType).getActualTypeArguments();
System.out.println(" 参数化类型中的泛型参数信息如下:");
for (int i = 0; i < actualTypeArguments.length; i++) {
Type actualTypeArgument = actualTypeArguments[i];
System.out.println(" 第" + i + "个泛型参数类型为");
System.out.print(" ");
printTypeInfo(actualTypeArgument);
}
} else if (argumentType instanceof GenericArrayType) {
System.out.println("泛型类型数组-> " + ((GenericArrayType) argumentType).toString());
System.out.print(" 泛型类型数组存储的类型为");
printTypeInfo(((GenericArrayType) argumentType).getGenericComponentType());
} else if (argumentType instanceof TypeVariable) {
System.out.println("类型变量类型-> " + ((TypeVariable) argumentType).getName());
} else if (argumentType instanceof WildcardType) {
System.out.println("包含通配符的类型-> " + ((WildcardType) argumentType).toString());
Type[] upperBounds = ((WildcardType) argumentType).getUpperBounds();
if (upperBounds != null) {
System.out.println(" 通配符的上界包括:");
for (int j = 0; j < upperBounds.length; j++) {
System.out.println(" " + upperBounds[j].getTypeName());
}
}
Type[] lowerBounds = ((WildcardType) argumentType).getLowerBounds();
if (lowerBounds != null) {
System.out.println(" 通配符的下界包括:");
for (int j = 0; j < lowerBounds.length; j++) {
System.out.println(" " + lowerBounds[j].getClass());
}
}
}
}
}
|
执行结果中可以看到我们最终获取了方法的返回类型上的泛型参数中的原生类型 User类及Post类,拿到具体的类型后,Retrofit框架内部就可以做Json转换了(实际上大部分Json转换库如Gson只需要获取到Type类型就可以了)。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
开始打印Service接口的方法信息
login方法的返回类型为参数化类型-> GenericTest.GenericTest$Call<GenericTest$User>
参数化类型中的泛型参数信息如下:
第0个泛型参数类型为
原生类型-> GenericTest.User
getPosts方法的返回类型为参数化类型-> GenericTest.GenericTest$Call<java.util.List<GenericTest$Post>>
参数化类型中的泛型参数信息如下:
第0个泛型参数类型为
参数化类型-> java.util.List<GenericTest$Post>
参数化类型中的泛型参数信息如下:
第0个泛型参数类型为
原生类型-> GenericTest.Post
|
回顾
在全文中我们首先简单介绍了泛型的概念、泛型的类型系统,之后从字节码角度解读了泛型在字节码层面的信息,最后回到应用层,我们介绍了泛型相关的反射api,并使用这些api完成一个小需求。
Java 泛型的引入为开发者在很多场景下带来了很多的灵活度。如果希望进一步的掌握泛型的使用,一方面可以在平时的开发过程中思考在哪些场景下可以引入泛型参数,活学活用;另一方面可以研究系统的类库比如Class类、集合类中泛型的使用,也可以研究一些开源框架比如Retrofit库。
参考
https://www.jianshu.com/p/2bfbe041e6b7
https://www.it610.com/article/5763404.htm
https://www.likecs.com/show-204186098.html
https://www.jianshu.com/p/be3edf42cfb2
https://blog.csdn.net/yu540135101/article/details/112998209
《Java编程思想 第4版》
《Java核心技术 第10版》
《深入理解Java虚拟机 第2版》
Java编程思想(部分Demo)