Java -- 泛型表达式和泛型方法调用中的类型擦除(三)
承接上一篇中的内容。当我们在使用泛型操作时,编译器会给我们的某些操作自动地、隐式地插入强制类型转换。先看一个示例代码:
/**
* Generic class without limited type variable
*
* @author xzm
* @param <T>
* Generic type
*/
class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
}
public Pair(T f, T s) {
first = f;
second = s;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
Pair<String> pair = new Pair<String>();
String first = pair.getFirst();//实际有两个处理:调用原始类型的getFirst();加入强制类型转换
System.out.println(first);
由于类型擦除,编译器中只有Pair的原始类型,这时调用getFirst()方法返回的应是一个Object类型的象。但我们看到第二条语句,可以直接将getFirst()返回的结果赋给String类型,而无需我们手动做强制类型转换。要明确的是,不是这里不需要强制类型转换,而是编译器已经替我们完成了这一动作。我们平常使用List等进行存取时,可能并没有太在意这种隐式处理。
类似地情况也出现在泛型中方法调用这一情景下,Java编译器也默默地给我们完成了一些隐式工作:
/**
* Generic class without limited type variable
*
* @author xzm
* @param <T>
* Generic type
*/
class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
}
public Pair(T f, T s) {
first = f;
second = s;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
/**
* Although we extends Pair<Date> with type info: Date, it will be erased by
* complier and use it's raw type
*
* In fact, raw type of a generic class only determined by it's declaration,
* whether used limited type variable for the type;etc, Pair<T> or Pair<T
* extends Comparable>.
*
* Pair<Date> or Pair<String> can't effect Pair's raw type.This point is very
* important.
*
* @author xzm
*
*/
class StringGeneric extends Pair<String> {
public StringGeneric(String string, String string2) {
super(string,string2);
}
public void setSecond(String second) {
System.out.println("setSecond in StringGeneric");
if (second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
public String getSecond() {
return super.getSecond();
}
}
Pair<String> str = new StringGeneric("first","null");//多态
str.setSecond("second");
我们简单地实现了一个StringGeneric类,继承Pair<String>;并覆写了setSecond()方法。这是如果我们通过反射获取并打印StringGeneric中声明过的方法,会发现多出了两个以Object为类型的方法:
Method[] mm = str.getClass().getDeclaredMethods();
for (Method m : mm) {
System.out.println(m);
}
Output:
public void test.java.core.StringGeneric.setSecond(java.lang.Object) //*** 编译器合成,显然与String类型的方法不一样
public void test.java.core.StringGeneric.setSecond(java.lang.String)
public java.lang.Object test.java.core.StringGeneric.getSecond() //*** 编译器合成,显然与String类型的方法不一样
public java.lang.String test.java.core.StringGeneric.getSecond()
str的引用类型是Pair<String>,实际指向的对象类型是StringGeneric。因为Pair<String>中只有setSecond(Object obj)方法,所以根据继承和多态原则,str也应该调用StringGeneric的setSecond(Object obj)方法。这个方法是编译器在StringGeneric中合成的一个“桥方法”。查看字节码:
// class version 52.0 (52)
// access flags 0x20
// signature Ltest/java/core/Pair<Ljava/lang/String;>;
// declaration: test/java/core/StringGeneric extends test.java.core.Pair<java.lang.String>
class test/java/core/StringGeneric extends test/java/core/Pair {
// compiled from: Test.java
// access flags 0x1
public <init>(Ljava/lang/String;Ljava/lang/String;)V
...
INVOKESPECIAL test/java/core/Pair.<init> (Ljava/lang/Object;Ljava/lang/Object;)V
...
// access flags 0x1
public setSecond(Ljava/lang/String;)V
...
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "setSecond in StringGeneric"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
...
INVOKEVIRTUAL test/java/core/StringGeneric.getFirst ()Ljava/lang/Object;
CHECKCAST java/lang/String
INVOKEVIRTUAL java/lang/String.compareTo (Ljava/lang/String;)I
...
INVOKESPECIAL test/java/core/Pair.setSecond (Ljava/lang/Object;)V
...
LOCALVARIABLE this Ltest/java/core/StringGeneric; L0 L4 0
LOCALVARIABLE second Ljava/lang/String; L0 L4 1
...
// access flags 0x1
public getSecond()Ljava/lang/String;
L0
LINENUMBER 144 L0
ALOAD 0
INVOKESPECIAL test/java/core/Pair.getSecond ()Ljava/lang/Object;
CHECKCAST java/lang/String
ARETURN
L1
LOCALVARIABLE this Ltest/java/core/StringGeneric; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1041
public synthetic bridge setSecond(Ljava/lang/Object;)V //bridge method
...
CHECKCAST java/lang/String
INVOKEVIRTUAL test/java/core/StringGeneric.setSecond (Ljava/lang/String;)V
...
// access flags 0x1041
public synthetic bridge getSecond()Ljava/lang/Object; // bridge method
...
INVOKEVIRTUAL test/java/core/StringGeneric.getSecond ()Ljava/lang/String;
...
}
桥方法StringGeneric::setSecond(Object obj)实际间接调用StringGeneric::setSecond(String str)方法,这跟我们预期的结果是相符的。
我们也许从上述打印StringGeneric类中的声明方法中,发现了StringGeneric::getSecond()方法的特殊之处:存在两个getSecond(),区别只是返回值类型不同。我们知道Java函数中的重载是不以返回值类型做其签名信息的,而Java方法的重写又必须保持函数的整个声明一致;但此时代码是没有错误的。实际桥方法只是编译器内部的行为,Java代码中是肯定不允许出现签名信息一致的两个方法的;但对于虚拟机调用方法来说,函数的返回值类型也被当成了函数签名信息的一部分,这样虚拟机就可以正确处理仅仅是返回值类型不同的两种方法的字节码了。
PS:Eclipse上有一个很好用的查看Java字节码的插件:bytecode。装了这个 插件,妈妈就再也不用担心我们查看字节码要使用小黑框了;免去了我们修该类文件后还有重新敲命令的繁琐步骤,推荐一下哈。