Java泛型
泛型
基本概念
什么是泛型?为什么要使用泛型?
泛型,即 “ 参数化类型 “。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。 那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。 也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型
,它的意思是把具体的类型泛化,编码的时候 (编译器) 用符号来指代类型,在使用的时候(运行时),再确定它的类型。
泛形的基本术语
以 ArrayList 为例:<>
念着typeof
- ArrayList 中的 E 称为
类型参数变量
- ArrayList 中的 Integer 称为
实际类型参数
- 整个称为 ArrayList
泛型类型
- 整个 ArrayList 称为
参数化的类型(ParameterizedType)
泛型基础
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。(如果一个类多处都要用到同一个泛型,这时可以把泛形定义在类上 (即类级别的泛型))
注意:静态方法不能使用类定义的泛形,而应单独定义泛形。
泛型类的最基本写法:
1
2
3
4
5
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
// .....
}
}
一个最普通的泛型类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
- 泛型的类型参数只能是类类型,不能是简单类型。
- 不能对确切的泛型类型使用 instanceof 操作。如下面的操作是非法的,编译时会出错。
1
2
if(ex_num instanceof Generic<Number>) {
}
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
1
2
3
4
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
当实现泛型接口的类,未传入泛型实参时:
1
2
3
4
5
6
7
8
9
10
11
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
当实现泛型接口的类,传入泛型实参时:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
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)];
}
}
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
Java 程序中的 普通方
法、构造方法
和 静态方法
中都可以使用泛型。
方法使用泛形前,必须对泛形进行声明,
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。
- 在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到 Object。
- 在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*不指定泛型的时候*/
Integer add = add(1, 3);
Number add1 = add(2.4, 3);
int i = add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number f = add(1, 1.2F);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
Object o = add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object
/*指定泛型的时候*/
int a = 泛型擦除.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
int b = 泛型擦除.<Integer>add(1, 2.2F);//编译错误,指定了Integer,不能为Float
Number c = 泛型擦除.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Floa
// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}
泛型数组
在 Java 中是 “ 不能创建一个确切的泛型类型的数组 “ 的。
下面的这个例子是不可以的:
1
List<String>[] ls = new ArrayList<String>[10];// 编译出错
而使用通配符创建泛型数组是可以的,如下面这个例子:
1
2
3
List<?>[] ls = new ArrayList<?>[10];
List<String>[] ls3 = new ArrayList[10]; // 编译警告:Uncheck assignment
泛型特性
泛型与多态
1
2
3
4
5
6
TextView textView = new Button(context);
// 👆 1. 这是多态。
List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = buttons;
// 👆 2. 多态用在这里会报错 incompatible types: List<Button> cannot be converted to List<TextView>
- Button 是继承自 TextView 的,根据 Java 多态的特性,第一处赋值是正确的。
- 但是到了 List 的时候 IDE 就报错了,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List 和 List 类型并不一致,也就是说,子类的泛型(List)不属于泛型(List)的子类。
泛型不支持多态;Java 的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。
在 Java 里用数组做类似的事情,是不会报错的,这是因为数组并没有在编译时擦除类型:
1
TextView[] textViews = new TextView[10];
但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。ava 提供了「泛型通配符」
? extends
和? super
来解决这个问题。
泛型擦除
Java 的泛型是伪泛型,因为,在编译期间,所有的泛型信息都会被擦除掉(类型擦除(type erasure)
)。
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
具体见 泛型擦除.md
章节
泛型通配符
见 [PECS]泛型中的通配符super T和extend T区别(协变、逆变).md
章节
? 无限定的通配符
相当于 ? extends Object
,表示对类型没有什么限制,可以把?看成所有类型的父类
1
2
ArrayList<T> al=new ArrayList<T>(); 指定集合元素只能是T类型
ArrayList<?> al=new ArrayList<?>();集合元素可以是任意类型
? extend 上界,协变
上界;协变;? extend T
只能 get,只能向外提供数据,相对于外界来说是作为生产者(Producer extends)
? super 下界,逆变
下界;逆变;? super T
只能 add,只能从外界获取数据,相对于外界来说是作为消费者(Consumer super)
反射获取泛型真实类型
当我们对一个泛型类进行反射时,需要的到泛型中的真实数据类型,来完成如 json 反序列化的操作。此时需要通
过 Type 体系来完成。Type 接口包含了一个实现类 (Class) 和四个实现接口,他们分别是:
- TypeVariable 泛型类型变量,可以泛型上下限等信息;
- ParameterizedType 具体的泛型类型,可以获得元数据中泛型签名类型 (泛型真实类型)
- GenericArrayType 当需要描述的类型是泛型类的数组时,比如 List[],Map[],此接口会作为 Type 的实现。
- WildcardType 通配符泛型,获得上下限信息
TypeVariable
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
public class TestType <K extends Comparable & Serializable, V> {
K key;
V value;
public static void main(String[] args) throws Exception {
// 获取字段的类型
Field fk = TestType.class.getDeclaredField("key");
Field fv = TestType.class.getDeclaredField("value");
TypeVariable keyType = (TypeVariable)fk.getGenericType();
TypeVariable valueType = (TypeVariable)fv.getGenericType();
// getName 方法
System.out.println(keyType.getName()); // K
System.out.println(valueType.getName()); // V
// getGenericDeclaration 方法
System.out.println(keyType.getGenericDeclaration()); // class com.test.TestType
System.out.println(valueType.getGenericDeclaration()); // class com.test.TestType
// getBounds 方法
System.out.println("K 的上界:"); // 有两个
for (Type type : keyType.getBounds()) { // interface java.lang.Comparable
System.out.println(type); // interface java.io.Serializable
}
System.out.println("V 的上界:"); // 没明确声明上界的, 默认上界是 Object
for (Type type : valueType.getBounds()) { // class java.lang.Object
System.out.println(type);
}
}
}
ParameterizedType
1
2
3
4
5
6
7
8
9
10
11
12
public class TestType {
Map<String, String> map;
public static void main(String[] args) throws Exception {
Field f = TestType.class.getDeclaredField("map");
System.out.println(f.getGenericType()); // java.util.Map<java.lang.String, java.lang.String>
ParameterizedType pType = (ParameterizedType) f.getGenericType();
System.out.println(pType.getRawType()); // interface java.util.Map
for (Type type : pType.getActualTypeArguments()) {
System.out.println(type); // 打印两遍: class java.lang.String
}
}
}
GenericArrayType
1
2
3
4
5
6
7
8
public class TestType<T> {
List<String>[] lists;
public static void main(String[] args) throws Exception {
Field f = TestType.class.getDeclaredField("lists");
GenericArrayType genericType = (GenericArrayType) f.getGenericType();
System.out.println(genericType.getGenericComponentType());
}
}
WildcardType
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestType {
private List<? extends Number> a; // 上限
private List<? super String> b; //下限
public static void main(String[] args) throws Exception {
Field fieldA = TestType.class.getDeclaredField("a");
Field fieldB = TestType.class.getDeclaredField("b");
// 先拿到范型类型
ParameterizedType pTypeA = (ParameterizedType) fieldA.getGenericType();
ParameterizedType pTypeB = (ParameterizedType) fieldB.getGenericType();
// 再从范型里拿到通配符类型
WildcardType wTypeA = (WildcardType) pTypeA.getActualTypeArguments()[0];
WildcardType wTypeB = (WildcardType) pTypeB.getActualTypeArguments()[0];
// 方法测试
System.out.println(wTypeA.getUpperBounds()[0]); // class java.lang.Number
System.out.println(wTypeB.getLowerBounds()[0]); // class java.lang.String
// 看看通配符类型到底是什么, 打印结果为: ? extends java.lang.Number
Gson反序列化
System.out.println(wTypeA);
}
}
Gson 反序列化
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
static class Response<T> {
T data;
int code;
String message;
@Override
public String toString() {
return "Response{" +
"data=" + data +
", code=" + code +
", message='" + message + '\'' +
'}';
}
public Response(T data, int code, String message) {
this.data = data;
this.code = code;
this.message = message;
}
}
static class Data {
String result;
public Data(String result) {
this.result = result;
}
@Override
public String toString() {
return "Data{" +
"result=" + result +
'}';
}
}
public class Test {
public static void main(String[] args) {
Response<Data> dataResponse = new Response(new Data("数据"), 1, "成功");
Gson gson = new Gson();
String json = gson.toJson(dataResponse);
System.out.println(json);
//为什么TypeToken要定义为抽象类?
Response<Data> resp = gson.fromJson(json, new TypeToken<Response<Data>>() {
}.getType());
System.out.println(resp.data.result);
}
}
在进行 GSON 反序列化时,存在泛型时,可以借助 TypeToken 获取 Type 以完成泛型的反序列化。
但是为什么 TypeToken 要被定义为抽象类呢?
因为只有定义为抽象类或者接口,这样在使用时,需要创建对应的实现类,此时确定泛型类型,编译才能够将泛型
signature 信息记录到 Class 元数据中。
虚拟机是如何实现泛型的?
泛型思想早在 C++ 语言的模板(Template)中就开始生根发芽,在 Java 语言处于还没有出现泛型的版本时,只能通过 Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。,由于 Java 语言里面所有的类型都继承于 java.lang.Object,所以 Object 转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个 Object 到底是个什么类型的对象。在编译期间,编译器无法检查这个 Object 的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多 ClassCastException 的风险就会转嫁到程序运行期之中。
泛型技术在 C#和 Java 之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的 IL 中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的 CLR 中,都是切实存在的,List<int>与 List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
Java 语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
由于 Java 泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP 组织对虚拟机规范做出了相应的修改,引入了诸如 Signature、LocalVariableTypeTable 等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature 是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名 [3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别 49.0 以上版本的 Class 文件的虚拟机都要能正确地识别 Signature 参数。
另外,从 Signature 属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
泛型擦除介绍
Java 的泛型是伪泛型,因为,在编译期间,所有的泛型信息都会被擦除掉(类型擦除(type erasure)
)。
- 泛型信息只存在代码编译阶段,在进⼊ JVM 之前,与泛型关的信息都会被擦除掉
- 在类型擦除的时候,如果泛型类⾥的类型参数没有指定上限,则会被转成 Object 类型,如果指定了上限,则会被转换成对应的类型上限。
- Java 中的泛型基本上都是在编译器这个层次来实现的。⽣成的 Java 字节码中是不包含泛型中的类型信息的。使⽤泛型的时候加上的类型参数,会在编译器编译的时候擦除掉,这个过程就称为类型擦除
泛型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
如在代码中定义的 List
案例 1:
1
2
3
4
5
6
7
8
9
public class Test4 {
public static void main(String[] args) {
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add("abc");
ArrayList<Integer> arrayList2=new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass()==arrayList2.getClass()); // true
}
}
在这个例子中,我们定义了两个 ArrayList 数组,不过一个是 ArrayList 泛型类型,只能存储字符串。一个是 ArrayList 泛型类型,只能存储整形。最后,我们通过 arrayList1 对象和 arrayList2 对象的 getClass 方法获取它们的类的信息,最后发现结果为 true。说明泛型类型 String 和 Integer 都被擦除掉了,只剩下了原始类型。
案例 2:
1
2
3
4
5
6
7
8
9
public class Test4 {
public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
ArrayList<Integer> arrayList3=new ArrayList<Integer>();
arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i=0;i<arrayList3.size();i++) {
System.out.println(arrayList3.get(i));
}
}
在程序中定义了一个 ArrayList 泛型类型实例化为 Integer 的对象,如果直接调用 add 方法,那么只能存储整形的数据。不过当我们利用反射调用 add 方法的时候,却可以存储字符串。这说明了 Integer 泛型实例在编译之后被擦除了,只保留了原始类型,在运行时也可以添加。
类型擦除引起的问题及解决方法
先检查,在编译
Java 编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。
1
2
3
4
5
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("123");
arrayList.add(123); // 编译错误
}
检查编译的对象和引用传递的问题
1
2
ArrayList<String> arrayList1 = new ArrayList(); // 第一种 情况
ArrayList arrayList2 = new ArrayList<String>();// 第二种 情况
本来类型检查就是编译时完成的。new ArrayList() 只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用 arrayList1 来调用它的方法,比如说调用 add() 方法。所以 arrayList1 引用能完成泛型类型的检查。
类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test10 {
public static void main(String[] args) {
ArrayList<String> arrayList1=new ArrayList();
arrayList1.add("1");//编译通过
arrayList1.add(1);//编译错误
String str1=arrayList1.get(0);//返回类型就是String
ArrayList arrayList2=new ArrayList<String>();
arrayList2.add("1");//编译通过
arrayList2.add(1);//编译通过
Object object=arrayList2.get(0);//返回类型就是Object
new ArrayList<String>().add("11");//编译通过
new ArrayList<String>().add(22);//编译错误
String string=new ArrayList<String>().get(0);//返回类型就是String
}
}
自动类型转换
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?看下 ArrayList 和 get 方法:
1
2
3
4
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
看以看到,在 return 之前,会根据泛型变量进行强转。
写了个简单的测试代码:
1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
ArrayList<Date> list=new ArrayList<Date>();
list.add(new Date());
Date myDate=list.get(0);
}
}
然后反编了下字节码,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(java.lang.String[]);
Code:
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<init
:()V
7: astore_1
8: aload_1
9: new #19 // class java/util/Date
12: dup
13: invokespecial #21 // Method java/util/Date."<init>":()
16: invokevirtual #22 // Method java/util/ArrayList.add:(L
va/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokevirtual #26 // Method java/util/ArrayList.get:(I
java/lang/Object;
25: checkcast #19 // class java/util/Date
28: astore_2
29: return
看第 22 ,它调用的是 ArrayList.get() 方法,方法返回值是 Object,说明类型擦除了。然后第 25,它做了一个 checkcast 操作,即检查类型 #19
, 在在上面找 #19
引用的类型,他是
9: new #19
// class java/util/Date
是一个 Date 类型,即做 Date 类型的强转。
所以不是在 get 方法里强转的,是在你调用的地方强转的。
类型擦除与多态的冲突和解决方法
现在有这样一个泛型类:
1
2
3
4
5
6
7
8
9
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
一个子类继承它:
1
2
3
4
5
6
7
8
9
10
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在这个子类中,我们设定父类的泛型类型为 Pair<Date>
,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:
将父类的泛型类型限定为 Date,那么父类里面的两个方法的参数都为 Date 类型:
1
2
3
4
5
6
7
8
9
class Pair {
private Date value;
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
}
可是由于种种原因,虚拟机并不能将泛型类型变为 Date,只能将类型擦除掉,变为原始类型 Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。
于是 JVM 采用了一个特殊的方法,来完成这项功能,那就是桥方法。
首先,我们用 javap -c className 的方式反编译下 DateInter 子类的字节码,结果如下:
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
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>"
:()V
4: return
public void setValue(java.util.Date); //我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue(); //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn
public java.lang.Object getValue(); //编译时由编译器生成的巧方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法
;
4: areturn
public void setValue(java.lang.Object); //编译时由编译器生成的巧方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法
)V
8: return
}
从编译的结果来看,我们本意重写 setValue 和 getValue 方法的子类,竟然有 4 个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是 Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue 和 getValue 方法上面的@Oveerride 只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
不过,要提到一点,这里面的 setValue 和 getValue 这两个桥方法的意义又有不同。
泛型类型变量不能是基本数据类型
不能用类型参数替换基本类型。就比如,没有 ArrayList,只有 ArrayList。因为当类型擦除后,ArrayList 的原始类型变为 Object,但是 Object 类型不能存储 double 值,只能引用 Double 的值。
运行时类型查询
1
2
3
4
5
6
7
ArrayList<String> arrayList = new ArrayList<String>(); // 因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。
// 那么,运行时进行类型查询的时候使用下面的方法是错误的
if( arrayList instanceof ArrayList<String>)
// Java限定了这种类型查询的方式
if( arrayList instanceof ArrayList<?>)
泛型类型的不能实例化
Ref
- java 泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题 https://blog.csdn.net/lonelyroamer/article/details/7868820
讲解的很详细
泛型通配符之? super 和? extend
泛型中的通配符
通配符?
用通配符? ,让泛型具备可变性
? extend T
上界通配符/协变
1
2
3
List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = buttons;
// 👆 多态用在这里会报错 incompatible types: List<Button> cannot be converted to List<TextView>
上面将 buttons 赋值给 textViews 编译器会提示错误,这是因为泛型具有不可变性,List 和 List 是两种不同的类型。
怎么解决?
1
2
3
List<Button> buttons = new ArrayList<Button>();
// 👇
List<? extends TextView> textViews = buttons;
这个 ? extends
叫做 「上界通配符」
,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。
在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。
它有两层意思:
- 其中 ? 是个通配符,表示这个 List 的泛型类型是一个未知类型。
- extends 限制了这个未知类型的上界,也就是泛型类型必须满足这个 extends 的限制条件,这里和定义 class 的 extends 关键字有点不一样:
- 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是 TextView。
- 它还有 implements 的意思,即这里的上界也可以是 interface。
下面也是可以的:
1
2
3
List<? extends TextView> textViews = new ArrayList<TextView>(); // 👈 本身
List<? extends TextView> textViews = new ArrayList<Button>(); // 👈 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // 👈 间接子类
在使用了上界通配符之后,List 的使用上有没有什么问题:
1
2
3
4
List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // 👈 get 可以
textViews.add(textView);
// 👆 add 会报错,no suitable method found for add(TextView)
List<? extends TextView>
的泛型类型是个未知类型 ?,编译器也不确定它是啥类型,只是有个限制条件。
- get()
由于它满足 ? extends TextView 的限制条件,所以 get 出来的对象,肯定是 TextView 的子类型,根据多态的特性,能够赋值给 TextView,啰嗦一句,赋值给 View 也是没问题的。
- add
List<? extends TextView>
由于类型未知,它可能是 List,也可能是 List;对于前者,显然我们要添加 TextView 是不可以的- 实际情况是编译器无法确定到底属于哪一种,无法继续执行下去,就报错了。
那我干脆不要 extends TextView ,只用通配符 ?
呢?
这样使用 List<?>
其实是 List<? extends Object>
的缩写。
1
2
3
4
5
6
List<Button> buttons = new ArrayList<>();
List<?> list = buttons;
Object obj = list.get(0);
list.add(obj); // 👈 这里还是会报错
和前面的例子一样,编译器没法确定 ? 的类型,所以这里就只能 get 到 Object 对象。
同时编译器为了保证类型安全,也不能向 List<?>
中添加任何类型的对象,理由同上。
由于 add 的这个限制,使用了 ? extends
泛型通配符的 List,只能够向外提供数据被消费,从这个角度来讲,向外提供数据的一方称为「生产者 Producer」。对应的还有一个概念叫「消费者 Consumer」,对应 Java 里面另一个泛型通配符 ? super。
另外一个案例:
1
2
3
List<? extends Number> numbers = new ArrayList<Number>(); // correct
List<? extends Number> numbers2 = new ArrayList<Integer>(); // correct
List<? extends Number> numbers3 = new ArrayList<Object>(); // wrong
? super T
下界通配符/逆变
1
List<? super Button> buttons = new ArrayList<TextView>();
这个 ? super 叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。
- 通配符 ? 表示 List 的泛型类型是一个未知类型。
- super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件。
- super 我们在类的方法里面经常用到,这里的范围不仅包括 Button 的直接和间接父类,也包括下界 Button 本身。
- super 同样支持 interface。
下面几种情况都是可以的:
1
2
3
List<? super Button> buttons = new ArrayList<Button>(); // 👈 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 👈 间接父类
使用了下界通配符的 List,我们再看看它的 get 和 add 操作:
1
2
3
4
List<? super Button> buttons = new ArrayList<TextView>();
Object object = buttons.get(0); // 👈 get 出来的是 Object 类型
Button button = ...
buttons.add(button); // 👈 add 操作是可以的
使用下界通配符 ? super
的泛型 List,只能读取到 Object 对象,一般没有什么实际的使用场景,通常也只拿它来添加数据,对于 List<? super Button>
从外取数据,也就是消费已有的 List<? super Button>
,往里面添加 Button,因此这种泛型类型声明称之为「消费者 Consumer」。
添加元素,可以添加为 T 及 T 的子类。
1
2
3
4
5
List<? super Fruit> fruits2 = new ArrayList<Fruit>();
// fruits2.add(new Fruit()); // ok
// fruits2.add(new Apple()); // ok
// fruits2.add(new Orange()); // ok
// fruits2.add(new Object()); // 编译错误
小结
Java 的泛型本身是不支持协变和逆变的。
- 可以使用泛型通配符
? extends
来使泛型支持协变,但是「只能读取不能修改」
,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。 - 可以使用泛型通配符
? super
来使泛型支持逆变,但是「只能修改不能读取」
,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。
这被称为 PECS 法则:「Producer-Extends, Consumer-Super」。
PECS 原则
PECS(Producer Extends Consumer Super)
PE extends(? extends),协变,取
频繁往外取内容的,适合用上界 extends
当只想从集合中获取元素,请把这个集合看成生产者,请使用 <? extends T>
,这就是 Producer extends 原则,PECS 原则中的 PE 部分。
CS super(? super),逆变, 存
经常往里插入的,适合用下界 super
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
public class PECS {
public static void main(String[] args) {
// System.out.println("Hello PECS");
List<Fruit> fruitList = new ArrayList<Fruit>();
fruitList.add(new Apple());
fruitList.add(new Orange());
// getOutFruits(fruitList); // ok
// getOutFruits2(fruitList); // ok
List<Apple> apples = new ArrayList<Apple>();
apples.add(new Apple());
// getOutFruits(apples); 编译错误
// getOutFruits((List<Fruit>)apples); // 强制类型转换,同样编译错误,不兼容的类型: List<Apple>无法转换为List<Fruit>
// getOutFruits2(apples); // ok
List<? extends Fruit> basket = apples;//按上一个例子,这个是可行的
for (Fruit fruit : basket) {
System.out.println(fruit); // ok
}
// basket.add(new Apple()); // 编译错误
// 存元素如果用fruits
List<? extends Fruit> fruits = new ArrayList<>(); //
// fruits.add(new Fruit()) // 编译错误,认为是List<Fruit>类型
// fruits.add(new Apple()) // 编译错误,认为是List<Apple>类型
// fruits.add(new Orange()) // 编译错误,认为是List<Orange>类型
// List<Fruit>,List<Apple>,List<Orange>是完全不同的类型,用? extends Fruit存元素时编译器就不知道到底是什么类型,编译错误
// 存取元素用 ? super Fruit,fruits当成是生产者,
List<? super Fruit> fruits2 = new ArrayList<>();
fruits2.add(new Fruit());
fruits2.add(new Apple());
fruits2.add(new Orange());
}
public static void getOutFruits2(List<? extends Fruit> basket) {
for (Fruit fruit : basket) {
System.out.println(fruit);
//...do something other
}
System.out.println("--------------");
}
public static void getOutFruits(List<Fruit> basket) {
for (Fruit fruit : basket) {
System.out.println(fruit);
//...do something other
}
System.out.println("--------------");
}
}
- 存
1
2
3
4
5
6
用了<? extends Fruit>相当于告诉编译器,我们的篮子(集合)是用来处理水果以及水果的子类型。因为子类型有许多,我们并没有告诉编译器是哪个子类型。
编译器在这里遇到的问题是,如果add的是Apple类型时,则basket应该是List<Apple>,如果add是Fruit类型,则basket应该是List<Fruit>。而List<Apple>和List<Fruit>前面已经提过,是2个完全没有关系的类型,
所以编译器不知道是哪个子类型将加入集合,不知道到底是List<Apple>还是List<Fruit>,所以编译器只能报错。(注意,这里讨论的都是类型,而不是对象)
另一方面,编译器已经知道集合里全部都是水果的子类型,所以编译器可以保证取出的数据全部是水果。
- 取
1
2
3
4
5
6
用了<? super Apple>相当于告诉编译器,集合接受处理Apple以及Apple的超类型,即Object,Fruit,Apple三个类型。
但编译器并不知道到底是List<Object>,List<Fruit>还是List<Apple>?
编译器只知道,苹果和苹果子类型是可以放进去(也是Fruit的子类型,也是Object的子类型)。这意味着,我们总是可以将一个苹果的子类型放入苹果的超类型的list中。
而取出时的情况是,编译器不知道是按哪个类型取出, 到底是Object,Fruit,Apple中的哪个呢?但是编译器可以选择永远不会错的类型,也就是Object的类型,因为Object是所有类型的超类型。
Ref
- - Kotlin 的泛型 https://kaixue.io/kotlin-generics/
讲解的很不错
- java 泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
https://blog.csdn.net/lonelyroamer/article/details/7868820#> - 你应该知道的泛型 (Generic) 与 PECS 原则
https://www.jianshu.com/p/e5b8cd33ec94