Java泛型

extend(上限)

上限用extends关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

1
2
3
4
5
6
7
public 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
6
public void testUpperBound()  
{
List<Timestamp> list = new ArrayList<Timestamp>();
Date date = new Date();
upperBound(list,date);
}

也就是说,现在upperBound方法中实际的listList<Timestamp>,向它添加一个Date类型,肯定是不行的。
相反,读取数据时,不管实际的list是什么类型,但可以知道它至少会返回一个Date类型,所以用foreach,get等没有问题。
可以使用泛型方法。

1
2
3
4
public <T extends Date> void upperBound2(List<T> list, T date)  //泛型的方法,需要泛型标识
{
list.add(date);
}

这里方法声明中的T作为一种泛型参数化信息,会存储在java字节码中,T的实际类型由调用时的参数决定的。

1
2
3
4
5
6
7
8
public 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
2
List<Date> list2 = new ArrayList<Date>();
upperBound2(list2,date);

super

下限用super进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。如下面的代码。

1
2
3
4
5
6
public 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
6
public void testLowerBound()  
{
List<Date> list = new ArrayList<Date>();
list.add(new Date());
lowerBound(list);
}

lowerBound方法中的List<? super Timestamp>表示这个list的参数类型可能是TimestampTimestamp的父类。
测试代码里,实际传入的是一个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
12
public 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,编译器无法保证向下类型转换的安全,所以这一句自然就无法编译了。

多态特性

  1. 子类转父类:自动类型转换
  2. 父类转子类:编译器不能保证转换安全性,首先向上转型,然后在向下转型。

    泛型擦除

    概述

    Java泛型的处理在编译器中进行,编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即类型擦除。
  • 虚拟机中没有泛型,只有普通类和普通方法。Java中将泛型类型转换为普通方法,并被转换成字节。
  • 泛型类的类型参数在编译时都会被擦除。泛型类被转换成泛型类型的最高父类,然后移除泛型类型。最高父类:其限定类型,无限定的变量用Object,限定类型包括:上限、下限
  • 创建泛型对象时请指明类型,让编译器尽早的做参数检查(Effective Java,第23条:请不要在新代码中使用原生态类型)

    编译器处理泛型

    C++的编译器使用Code Specialization的方式;而Java的编译器在处理泛型时采用的是Code Sharing的方式。

    Code specialization

    在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码或者二进制代码)。例如,针对一个泛型List,可能需要针对StringIntegerFloat产生三份目标代码。

    Code sharing

    对每个泛型类只生成唯一的一份目标代码,该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。
    Code specialization另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。
    Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

    泛型擦除

    Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。
    Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
    1
    2
    3
    4
    5
    6
    7
    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());
    }
1
true

说明泛型类型StringInteger都被擦除掉了,只剩下了原始类型。原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),使用其限定类型替换。

先检查,再编译

类型变量会在编译的时候擦除掉,那为什么往ArrayList<String> arrayList=new ArrayList<String>();所创建的数组列表arrayList中,不能使用add()添加整形呢?不是说泛型变量Integer会在编译时候擦除变为原始类型Object吗,为什么不能存别的类型呢?既然类型擦除了,如何保证只能使用泛型变量限定的类型呢?Java是如何解决这个问题的呢?Java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。

泛型类型自动转换

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那List.get()数据的时候,不需要进行强制类型转换呢?看下ArrayListget()

1
2
3
4
public E get(int index) {  
RangeCheck(index);
return (E) elementData[index];
}

1
2
3
4
5
6
7
8
public class Tes {

public static void main(String[] args) {
ArrayList<Date> list = new ArrayList<Date>();
list.add(new Date());
Date myDate = list.get(0);
}
}

Javap -verbose Tes

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
D:\>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
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
11
12
import java.util.Date;

class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}

查询Pair编译字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
D:\>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
41
D:\>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生成的桥接方法,桥接方法的类型都是ObjectDateInter覆盖Pair两个方法的就是这两个看不到的桥方法,在DateInter中定义的setvaluegetValue方法上面的@Oveerride只不过是假象,桥方法的内部实现去调用DateInter覆写的那两个方法。
虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。
Object#getValue()Date#getValue()是同时存在的,可是如果是常规的两个方法,方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

附件

DateInter.class
Pair.class
Pair_2.class
Pair_3.class
Tes.class

# Java

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×