extend(上限)
上限用extends关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。1
2
3
4
5
6
7public void upperBound(List<? extends Date> list, Date date)
{
Date now = list.get(0);
System.out.println("now==>" + now);
//list.add(date); //这句话无法编译
list.add(null);//这句可以编译,因为null没有类型信息
}
为什么会无法编译呢,实际调用时传入的list
可能是java.util.Date
的某个子类的参数化类型。1
2
3
4
5
6public void testUpperBound()
{
List<Timestamp> list = new ArrayList<Timestamp>();
Date date = new Date();
upperBound(list,date);
}
也就是说,现在upperBound
方法中实际的list
是List<Timestamp>
,向它添加一个Date
类型,肯定是不行的。
相反,读取数据时,不管实际的list
是什么类型,但可以知道它至少会返回一个Date
类型,所以用foreach,get
等没有问题。
可以使用泛型方法。1
2
3
4public <T extends Date> void upperBound2(List<T> list, T date) //泛型的方法,需要泛型标识
{
list.add(date);
}
这里方法声明中的T作为一种泛型参数化信息,会存储在java字节码中,T的实际类型由调用时的参数决定的。1
2
3
4
5
6
7
8public void testUpperBound2()
{
List<Timestamp> list = new ArrayList<Timestamp>();
Date date = new Date();
Timestamp time = new Timestamp(date.getTime());
upperBound2(list,time); //T的类型:Timestamp,list添加Timestamp是没有问题的
//upperBound2(list,date);//这句同样无法编译
}
上面代码中的list
的类型参数决定了方法中T的类型,所以会看到注释掉的内容不能编译。而换成下面代码,编译就没有任何问题了。1
2List<Date> list2 = new ArrayList<Date>();
upperBound2(list2,date);
super
下限用super进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。如下面的代码。1
2
3
4
5
6public void lowerBound(List<? super Timestamp> list)
{
Timestamp now = new Timestamp(System.currentTimeMillis());
list.add(now);
//Timestamp time = list.get(0); //不能编译
}
这又为什么不能通过编译呢,看看调用代码。1
2
3
4
5
6public void testLowerBound()
{
List<Date> list = new ArrayList<Date>();
list.add(new Date());
lowerBound(list);
}
lowerBound
方法中的List<? super Timestamp>
表示这个list
的参数类型可能是Timestamp
或Timestamp
的父类。
测试代码里,实际传入的是一个List<Date>
类型。向List<Date>
中add
一个Timestamp
肯定是没有问题的,但list.get()
方法返回的对象类型可能是Date
甚至是Object
,你不能说list.get(0)
返回的就是一个Timestamp
,这里是向下类型转换了,编译器无法处理,所以这里不能编译。
用Java泛型实现的擦拭法解释,编译后会是如下的伪代码。1
2
3
4
5
6
7
8
9
10
11
12public void lowerBound(List list)
{
Timestamp now = new Timestamp(System.currentTimeMillis());
list.add(now);
Timestamp time = (Timestamp)list.get(0); //1
}
public void testLowerBound()
{
List list = new ArrayList();
list.add(new Date());
lowerBound(list);
}
代码1进行了强制类型转换,但实际添加进去的是一个Date
类型,肯定会报ClassCastException
,编译器无法保证向下类型转换的安全,所以这一句自然就无法编译了。
多态特性
- 子类转父类:自动类型转换
- 父类转子类:编译器不能保证转换安全性,首先向上转型,然后在向下转型。
泛型擦除
概述
Java泛型的处理在编译器中进行,编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即类型擦除。
- 虚拟机中没有泛型,只有普通类和普通方法。Java中将泛型类型转换为普通方法,并被转换成字节。
- 泛型类的类型参数在编译时都会被擦除。泛型类被转换成泛型类型的最高父类,然后移除泛型类型。最高父类:其限定类型,无限定的变量用
Object
,限定类型包括:上限、下限 - 创建泛型对象时请指明类型,让编译器尽早的做参数检查(Effective Java,第23条:请不要在新代码中使用原生态类型)
编译器处理泛型
C++的编译器使用Code Specialization
的方式;而Java的编译器在处理泛型时采用的是Code Sharing
的方式。Code specialization
在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码或者二进制代码)。例如,针对一个泛型List
,可能需要针对String
,Integer
,Float
产生三份目标代码。Code sharing
对每个泛型类只生成唯一的一份目标代码,该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。
Code specialization另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。
Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。泛型擦除
Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。1
2
3
4
5
6
7public 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());
}
1 | true |
说明泛型类型String
和Integer
都被擦除掉了,只剩下了原始类型。原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),使用其限定类型替换。
先检查,再编译
类型变量会在编译的时候擦除掉,那为什么往ArrayList<String> arrayList=new ArrayList<String>();
所创建的数组列表arrayList
中,不能使用add()
添加整形呢?不是说泛型变量Integer
会在编译时候擦除变为原始类型Object
吗,为什么不能存别的类型呢?既然类型擦除了,如何保证只能使用泛型变量限定的类型呢?Java是如何解决这个问题的呢?Java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。
泛型类型自动转换
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那List.get()
数据的时候,不需要进行强制类型转换呢?看下ArrayList
和get()
。1
2
3
4public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
1 | public class Tes { |
Javap -verbose Tes1
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
107D:\>javap -verbose Tes
Classfile /D:/Tes.class
Last modified 2016-12-8; size 692 bytes
MD5 checksum b74df56b94251dc7e4e1b3c3cebab208
Compiled from "Tes.java"
public class Tes
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // Tes
#2 = Utf8 Tes
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LTes;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // java/util/ArrayList
#17 = Utf8 java/util/ArrayList
#18 = Methodref #16.#9 // java/util/ArrayList."<init>":()V
#19 = Class #20 // java/util/Date
#20 = Utf8 java/util/Date
#21 = Methodref #19.#9 // java/util/Date."<init>":()V
#22 = Methodref #16.#23 // java/util/ArrayList.add:(Ljava/lang
/Object;)Z
#23 = NameAndType #24:#25 // add:(Ljava/lang/Object;)Z
#24 = Utf8 add
#25 = Utf8 (Ljava/lang/Object;)Z
#26 = Methodref #16.#27 // java/util/ArrayList.get:(I)Ljava/la
ng/Object;
#27 = NameAndType #28:#29 // get:(I)Ljava/lang/Object;
#28 = Utf8 get
#29 = Utf8 (I)Ljava/lang/Object;
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 list
#33 = Utf8 Ljava/util/ArrayList;
#34 = Utf8 myDate
#35 = Utf8 Ljava/util/Date;
#36 = Utf8 LocalVariableTypeTable
#37 = Utf8 Ljava/util/ArrayList<Ljava/util/Date;>;
#38 = Utf8 SourceFile
#39 = Utf8 Tes.java
{
public Tes();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>
":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTes;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<in
it>":()V
7: astore_1
8: aload_1
9: new #19 // class java/util/Date
12: dup
13: invokespecial #21 // Method java/util/Date."<init>":
()V
16: invokevirtual #22 // Method java/util/ArrayList.add:
(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokevirtual #26 // Method java/util/ArrayList.get:
(I)Ljava/lang/Object;
25: checkcast #19 // class java/util/Date
28: astore_2
29: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 20
line 10: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
8 22 1 list Ljava/util/ArrayList;
29 1 2 myDate Ljava/util/Date;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 22 1 list Ljava/util/ArrayList<Ljava/util/Date;>;
}
SourceFile: "Tes.java"
- 第22 ,它调用的是
ArrayList#get()
,方法返回值是Object
,说明类型擦除了。 - 第25,它做了一个
checkcast
操作,即检查类型#19
, 找#19引用的类型,9: new #19 // class java/util/Date
是一个Date
类型,即做Date
类型的强转。所以不是在get()
里强转的,是在调用的地方强转的。1
return (E) elementData[index];
类型擦除和多态冲突,JVM解决方法
1 | class Pair<T> { |
1 | import java.util.Date; |
查询Pair编译字节码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23D:\>javap -c Pair
Compiled from "Pair.java"
class Pair<T> {
Pair();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":
()V
4: return
public T getValue();
Code:
0: aload_0
1: getfield #23 // Field value:Ljava/lang/Object;
4: areturn
public void setValue(T);
Code:
0: aload_0
1: aload_1
2: putfield #23 // Field value:Ljava/lang/Object;
5: return
}
查询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
41D:\>javap -c DateInter
Compiled from "DateInter.java"
class DateInter extends Pair<java.util.Date> {
DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method Pair."<init>":()V
4: return
public void setValue(java.util.Date);
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method Pair.setValue:(Ljava/lang/
Object;)V
5: return
public java.util.Date getValue();
Code:
0: aload_0
1: invokespecial #23 // Method 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
;
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;
)V
8: return
}
查询DateInter
字节码,有2个get()
和2个set()
, setValue(java.lang.Object)
和Object getValue()
是JVM生成的桥接方法,桥接方法的类型都是Object
,DateInter
覆盖Pair
两个方法的就是这两个看不到的桥方法,在DateInter
中定义的setvalue
和getValue
方法上面的@Oveerride
只不过是假象,桥方法的内部实现去调用DateInter
覆写的那两个方法。
虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。Object#getValue()
和Date#getValue()
是同时存在的,可是如果是常规的两个方法,方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。