java学习脚印: 泛型(Generic)认识之二
上接,《java学习脚印: 泛型(Generic)认识之一》。
1.泛型在java语言中如何表示 ?
事实上,在虚拟机中并没有泛型,只有普通的类和方法。java语言通过擦除(type erasure)类型变量,及相应的支持机制来表达泛型。
首先让我们对类型擦除有个简单认识,稍后将会给出c++泛型和java泛型的实例对比,来加深理解。
1.1 类型擦除的初步认识
例如在上一篇《java学习脚印: 泛型(Generic)认识之一》 中的Pair<T>,
擦除类型后表示如下所示。
程序清单: 1-1 Pair.java
//after type erasure
public class Pair {
//should make fields null before initialization
public Pair() {
this.first = null;
this.second = null;
}
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return this.getClass().getName()+"[ "+first.toString()
+","+second.toString()+" ]";
}
private Object first;
private Object second;
}
这里类型变量T被Object所替换,上面的Pair类即是Pair<T>的原始类型。
关于擦除的规则见1.3 .
1.2 为什么引入擦除机制?
1.2.1 泛型在语言中的表达
在任何语言之中,编译器编译泛型时通常有两种选择:
1)代码专有化(Code specialization)
编译器为每个泛型类和泛型方法的实例产生一个新的表示。例如整数,字符串,日期列表(这里可以把列表,表达为c++中的vector)都产生一个不同的代码。
c++语言的模板中就是采用代码专有化的。
c++编译器为每个模板的实例产生可执行的代码,这样做的风险就是所谓的“模板代码膨胀”。例如vector<int>和vector<string>,就会有两份不同代码。在c++中模板代码膨胀并非不可避免,通常有经验的程序员就可以避免。
2)代码共享(Code sharing)
编译器只产生泛型类和方法的一个表示,然后把不同的实例映射到这个唯一表示,并在需要时执行类型检查和转换。通过这种机制实现代码共享。
当在集合中的数据是引用或者指针时,代码专有化方式显得尤其浪费。因为引用或者指针的存储空间大小相同,在内部具有相同的表示,没有必要为一个整数或者字符串列表来产生两份大部分都相同的代码。
这两个列表都可以在内部表示为引用任意类型的列表。编译器需要做的就是,在泛型类或方法中传递这些引用时添加一些类型转换。
在java中,大部分类型是引用,可以认为java选择代码共享技术来支持泛型是很自然的事情了。
总之,在java语言中选择第二种方案,即使用代码共享技术支持泛型。
java语言,对泛型类或方法产生唯一的字节码表示,并把泛型类或者方法的实例映射到这个唯一表示。编译器负责将包含有泛型类或者方法定义和使用的代码编译成虚拟机可解释的字节码。这种从泛型类或者方法的实例映射到唯一字节码表示的技术称为类型擦除(type erasure)。
1.2.2 例说泛型在c++与java语言中特点的对比
为了加深理解,这里做一个实验,对比c++与java语言之中表达泛型的差异。
我们通过编写一个利用列表(c++中vector,java中ArrayList,这两个类型都是用一组连续空间来装同种元素的容器)添加字符串和整数的例子来观察两种语言的两种机制。
1.2.2.1 泛型在c++的代码专有化特点
首先来看c++中的泛型特点,以vector类为例。
程序清单1-2 如下所示:
#include <iostream>
#include <vector>
#include <string>
int main()
{
std::vector<std::string> strVector ;
strVector.push_back("hello");
strVector.push_back("world");
std::cout<<"get string from vector: "
<<strVector.front()<<std::endl;
std::vector<int> intVector;
intVector.push_back(1);
intVector.push_back(2);
std::cout<<"get int from vector: "
<<intVector.front()<<std::endl;
return 0;
}
编译该程序: g++ cppVector.cpp -o cppVector
然后反编译该程序: objdump -dC ./cppVector >cppVector.txt查看cppVector.txt文件,我们可以搜索到两份vector实例的部分。
1)vector<string>的代码:
455 08048dda <std::vector<std::string, std::allocator<std::string> >::vector()>:
456 8048dda: 55 push %ebp
457 8048ddb: 89 e5 mov %esp,%ebp
...
540 08048eba <std::vector<std::string, std::allocator<std::string> >::front()>:
541 8048eba: 55 push %ebp
542 8048ebb: 89 e5 mov %esp,%ebp
543 8048ebd: 83 ec 28 sub $0x28,%esp
...
2)vector<int>的代码
556 08048ee2 <std::vector<int, std::allocator<int> >::vector()>:
557 8048ee2: 55 push %ebp
558 8048ee3: 89 e5 mov %esp,%ebp
....
641 08048fc2 <std::vector<int, std::allocator<int> >::front()>:
642 8048fc2: 55 push %ebp
643 8048fc3: 89 e5 mov %esp,%ebp
644 8048fc5: 83 ec 28 sub $0x28,%esp
645 8048fc8: 8d 45 f4 lea -0xc(%ebp),%eax
可见,c++采用代码专有化方式的确产生了不同实例的两份代码。
1.2.2.2 泛型在java中的代码共享特点
来看java中泛型特点,以ArrayList为例。
程序清单1-3 javaArrayList.java
import java.util.ArrayList;
public class javaArrayList {
public static void main(String[] args) {
ArrayList<String> strArrayList = new ArrayList<>();
strArrayList.add("hello");
strArrayList.add("world");
System.out.println("get string from "
+ "ArrayList: "+strArrayList.get(0));
ArrayList<Integer> intArrayList = new ArrayList<>();
intArrayList.add(Integer.valueOf(1));
intArrayList.add(Integer.valueOf(2));
System.out.println("get int from "
+ "ArrayList: "+intArrayList.get(0));
}
}
反编译该代码: javap -c javaArrayList >javaArrayList.txt
来看产生的反编译文件。
1)strArrayList.add("hello")方法
9: ldc #4 // String hello
17 11: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
18 14: pop
19 15: aload_1
20 16: ldc #6 // String world
21 18: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
2)intArrayList.add(Integer.valueOf(1))方法
64: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
43 67: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
44 70: pop
45 71: aload_2
46 72: iconst_2
47 73: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
48 76: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
可见,这里并没有产生两份不同的代码,调用的都是ArrayList.add(java.lang.Object)方法来添加元素,也就是java使用代码共享来表达泛型。
1.3 类型擦除规则
1)如果类型变量没有限定类型,则替换为Object,例如<T>则替换为Object。
2)如果类型变量有限定类型,则替换为限定类型列表中最左边的类型。
例如 <T extends Comparable & Serializable> 则替换为 Comparable。
可以参见2.1来理解。
2. java泛型的支持
要通过代码共享来表达泛型,需要擦除机制,同时还需要类型转换和桥方法的辅助才能完成,下面简要讲述。
2.1 编译器添加的隐式类型转换
1)没有或者只有一个限定类型时的转换。
例如上节的Pair<T>类,编译器将会利用Object来替换T擦除类型变量,见1.1。同时若客户端如下代码:
Pair<String> stu = new Pair<>("jack","20130304");
System.out.println(stu.getSecond());
stu.setFirst("Smith");
System.out.println(stu);
则将进行如下转换:
Pair stu = new Pair("jack","20130304");//remove String and use raw type
System.out.println((String)stu.getSecond());//cast to String
stu.setFirst("Smith");//no need cast
System.out.println(stu);
这样就能保证原本擦除之后的getSecond()取出的Object转换为String对象。
2)多个限定类型时的擦除与转换。
这里取http://www.angelikalanger.com一个例子做参考。
擦除前后的类文件如下图所示,左则代码为擦除前,右侧代码为擦除后:
总结:
1)类型变量没有限定时,擦除时用Object替换;有一个或者多个限定类型时,用限定列表最左边即第一个限定类型来替换。
2)没有限定类型或者单个限定时,类型转换只需要负责一种类型的转换。
3)多个限定类型时,第一个限定类型的方法可以直接调用,例如Callable的call方法,但是其他类型需要进行类型转换,例如Runnable的run 方法。
这里((Runnable)task1).run()转换后再调用run方法,同时task2.call返回类型需要进行转换,转换为Long类型。
4)set修改器的参数没有必要进行转换,例如Pair<T>中的setFirst(Object first)就不会转换;
但是get访问器参数需要进行转换。
2.2 编译器添加的桥方法(bridge method)
类型擦除会引起一些复杂问题,当一个类从参数化的类继承或者实现参数话的接口时,编译器会自动添加桥方法来消除这些负面问题。
1)实现参数化接口时添加桥方法
我们以http://www.angelikalanger.com网站一个NumericValue类为例。
程序清单:2-1 GenericsDemo1.java
package com.learningjava;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
public class GenericsDemo1 {
public static void main (String[ ] args) {
LinkedList <NumericValue> numberList = new LinkedList <NumericValue> ();
numberList .add(new NumericValue((byte)0));
numberList .add(new NumericValue((byte)1));
NumericValue y = Collections.max( numberList );
System.out.println(y);
}
}
interface Comparable <A> {
public int compareTo( A that);
}
final class NumericValue implements Comparable <NumericValue> {
private byte value;
public NumericValue (byte value) { this.value = value; }
public byte getValue() { return value; }
@Override
public String toString() {return this.getClass().getName()+"["+value+"]";}
public int compareTo( NumericValue that){ return this.value - that.value; }
}
class Collections {
public static <A extends Comparable<A>> A max(Collection<A> xs) {
Iterator <A> xi = xs.iterator();
A w = xi.next();
while (xi.hasNext()) {
A x = xi.next();
if (w.compareTo(x) < 0) w = x;
}
return w;
}
}
在这部分代码中,NumericValue实现了参数化的Comparable接口,我们通过反编译看看它的方法:javap -p com/learningjava/NumericValue.class
显示如下:
Compiled from "GenericsDemo1.java"
final class com.learningjava.NumericValue implements com.learningjava.Comparable<com.learningjava.NumericValue> {
private byte value;
public com.learningjava.NumericValue(byte);
public byte getValue();
public java.lang.String toString();
public int compareTo(com.learningjava.NumericValue);
public int compareTo(java.lang.Object);//bridge method
}
这是因为从Comparable接口继承而来的方法是 :
public int compareTo(Object that);这个方法与添加的compareTo()方法实现了重载,但是从NumericValue本身来看,它需要完成的是compareTo(compareTo that)方法,因而编译器产生了这个桥方法并将其实现为:
public int compareTo(Object that) {return this.compareTo((NumericValue)that);}
这样就不会产生混淆了。
注意两点:
1)一般情况下,你无法触发compareTo(Object that)方法,代码:
NumericValue value = new NumericValue((byte)0);
value.compareTo(value); // 可以
value.compareTo("abc"); // 编译错误
即使你使用反射技术调用了compareTo(Object that)方法也会进行类型检查,如果类型不匹配会发生ClassCastException错误。
2)桥方法由编译器自己生成,不要试图自己编写compareTo(Object that)方法,否则将会产生错误信息如下:
Name clash: The method compareTo(Object) of type NumericValue has the same erasure as compareTo(A) of type Comparable<A> but does not override it.
2)继承参数化类时添加桥方法
我们选取Oracle官网上面的一个简单例子来说明问题。
程序清单: 2-2 GenericDemo2.java
package com.learningjava;
public class GenericsDemo2 {
public static void main(String[] args) {
MyNode mn = new MyNode(5);
Node<Integer> n = mn;
n.setData(Integer.valueOf(6));
System.out.println(n.getData());
}
}
class Node<T> {
private T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
this.data = data;
}
public T getData() {return data;}
}
class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
super.setData(data);
}
public Integer getData() {return super.getData();}
}
Node<T>以及MyNode 擦除之后如下所示:
class Node {
private Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
this.data = data;
}
public Object getData() {return data;}
}
class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
super.setData(data);
}
public Integer getData() {return (Integer)super.getData();}
}
我们看到MyNode将继承Node类的两个方法:
public void setData(Object data)
public Object getData() 方法。
main函数中代码:
MyNode mn = new MyNode(5);
Node<Integer> n = mn;
n.setData(Integer.valueOf(6));
System.out.println(n.getData());
擦除后为:
MyNode mn = new MyNode(5);
Node n = mn;
n.setData(Integer.valueOf(6));//call Node.setData(Object data)
System.out.println((Integer)n.getData());
由于多态原因,n.setData(Integer.valueOf(6));只会调用Node类的setData(Object data)
方法,而我们希望调用的是setData(Integer data)方法,这就是说类型擦除与多态发生了冲突,因此必须由编译器产生桥方法来实现这一转换。
编译器将会产生如下的桥方法:
public void setData(Object data) {
this.setData((Integer)data);
}
解决了这个冲突问题。
注意体味这一过程的擦除过程,以及为什么要添加桥方法。
总之,当类继承参数化类或者实现参数化接口并且类型擦除改变了其继承方法的签名时产生的方法。
个人体会,桥方法主要是为了解决调用时的一致性问题,比如子类继承泛型后产生了两个同名方法,那么就要保持这两个方法操作的实质内容是一致的。