在知乎在碰到了一篇好文:你见过哪些令你瞠目结舌的 Android 代码技巧?,其中很多答主都提到了一个Java的技巧,利用泛型来减少强制转换的使用,如下:
public T $(int id) {
return (T) super.findViewById(id);
}
public T $(View view, int id) {
return (T) view.findViewById(id);
}
这样在使用时,可以省去强制转换,让编译器来帮我们决定类型。而且由于是编译期间做的泛型擦除,所以实际运行中不会有性能损失,真是居家旅行杀人灭口必备良药啊。这种好东西以前一直以为C++才有,之前用到List<>也没怎么深究,赶紧恶补一下。
补过了原理,感觉比C++的模板要简单不少,为了实现JVM向下兼容,做出了很大的妥协,但也引出了很多问题。
Java是通过类型擦除来实现泛型的。在实际的字节码中,不会有泛型相关信息,但是会多出相关的强制类型转换,用例子来说明好懂一点:
public static void main(String args[]){
List list = new ArrayList();
list.add(1);
Integer a = list.get(0);
System.out.println(a);
}
很简单的一个例子,看看擦除后会变成什么样子:(可以用JD-GUI来看.class文件)
public static void main(String[] args) {
List list = new ArrayList();
list.add(Integer.valueOf(1));
Integer a = (Integer)list.get(0);
System.out.println(a);
}
可以看到,最终代码没有任何泛型信息,都已经被擦除,只不过编译器帮我们加了一个强制转换Integer a = (Integer)list.get(0);而我们在写代码时,就可以少写这一步了。而List其实只有一个类,它可以储存各种类型的数据的原因,是擦除后List存储的数据类型变成了Object,这样就可以存储所有Object的子类。
原来如此,明白了原理后,List不能存放基本数据类型也就找到原因了,因为它们都不是Object的子类。
一般来说,泛型擦除后,都会变成最基本的Object,但我们可以添加上限,来限定传入的数据类型:
public static > int foo(T a,T b){
return a.compareTo(b);
}
如上面的代码,我们使用extends关键字将传入的类型限定为实现Comparable接口,实际上的作用就是将擦除后的类型设置为Comparable,这通常被称为『有界类型』。
这个其实是类型擦除引出来的问题,不过Java提供了解决方式,这里讲一下原理。
public class Base<T> {
T data;
public void setValue(T value){
data = value;
}
public T getValue(){
return data;
}
}
如果有上面的泛型类,在类型擦除后会变为:
public class Base {
Object data;
public void setValue(Object value){
data = value;
}
public Object getValue(){
return data;
}
}
如果我们想写一个类继承Base,并重写两个方法,按普通类的写法:
public class Derived extends Base<String>{
@Override
public void setValue(String value){
data = value;
}
@Override
public String getValue(){
return data;
}
}
但按代码来分析,Derived类的函数是void setValue(String),而Base类型擦除后的方法是void setValue(Object),传入的参数类型不一样,是不能重写的,应该是重载,所以,按正常来说,Derived应该存在4个函数:
public void setValue(Object value);
public Object getValue();
public void setValue(String value);
public String getValue();
那我们考虑下面多态的用法:
public static void main(String args[]){
Base b = new Derived();
b.setValue("Hello world");
System.out.println(b.getValue());//打印Hello world Derived
}
这时,理论来说,调用的应该是Base的public Object getValue();函数,因为Derived类并没有重写这个函数。但我们的执行结果却是不一样的,打印出了:Hello world Derived,也就是调用了Derived类的public void setValue(Object value),就如同真得重载了一样。Java是如何做到的呢?
翻一下Java生成的字节码,看看编译器作了什么解决了这个问题:
javap -c Derived.class
public class com.trioly.storm.Derived extends com.trioly.storm.Base<java.lang.String> {
public com.trioly.storm.Derived();
Code:
0: aload_0
1: invokespecial #1 // Method com/trioly/storm/Base."":()V
4: return
public void setValue(java.lang.String);
Code:
0: aload_0
1: new #2 // class java/lang/StringBuilder
4: dup
5: invokespecial #3 // Method java/lang/StringBuilder."":()V
8: aload_1
9: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: ldc #5 // String Derived
14: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
20: putfield #7 // Field data:Ljava/lang/Object;
23: return
public java.lang.String getValue();
Code:
0: aload_0
1: getfield #7 // Field data:Ljava/lang/Object;
4: checkcast #8 // class java/lang/String
7: areturn
public java.lang.Object getValue();
Code:
0: aload_0
1: invokevirtual #9 // Method getValue:()Ljava/lang/String;调用真正的方法public java.lang.String getValue()
4: areturn
public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #8 // class java/lang/String
5: invokevirtual #10 // Method setValue:(Ljava/lang/String;)V,调用真正的方法public void setValue(java.lang.String)
8: return
}
可以看到,Derived类的字节码中,多了两个函数:
public java.lang.Object getValue();
public void setValue(java.lang.Object);
这两个函数就是编译器添加的桥方法,真正重写Base类方法的就是它们,而桥方法做的事情很简单,只是做了下类型检查,然后去调用Derived类的真正方法。这样,上面的测试程序也就讲得通了。而jvm也没有做什么修改,实现了对下兼容,只是编译器多做了些事情而已。
当然,桥方法不只在这里用到了。Java1.5以后新增了一些特性,也是用桥方法来实现了。如原先想要重写基类方法,参数与返回值必须完全一样,但1.5后允许返回值只要是原返回值的派生类,就可以重写,使用的也是桥方法来实现的。
具体使用时,可以被分为泛型接口、泛型类、泛型方法。使用起来其实都是大同小异,我暂时只找到一个区别,就是泛型方法的参数可以使用< ? extends Comparable>< ? super Comparable>这样的上下限参数类型通配符,而泛型接口、泛型类只能用< T extends Comparable>这样明确且指定上限的类型限定符。
由于为了向前兼容,使用了类型擦除这样的实现,导致出现一系列的问题,开发时需要留意:
这个很好理解,类型擦除后,泛型类其实就是是同一个类,如ArrayList< String>和ArrayList< Integer>在擦除后都是ArrayList,所以如果使用了静态变量,其实所有的泛型类都是共享的,这与设计是相违背且易乱用和出错的,所以直接禁止使用。
这个上面也提过了,基本数据类型没有一个统一的基类,所以不支持类型擦除。
由于类型擦除后变成了另外一个类,所以我们不能使用如下的方法在运行时判断:
if( arrayList instanceof ArrayList)
Java限定的这种判断方法:
if( arrayList instanceof ArrayList>)
再具体的判断就做不到了
public class Problem<T> extends Exception{......}
因为类型擦除后,抛出的异常就无法找到正确的处理分支,如下所示:
try{
}catch(Problem e1){
...
}catch(Problem e2){
...
}
擦除后会变为:
try{
}catch(Problem e1){
...
}catch(Problem e2){
...
}
a = new T();
这样写是不行的,因为类型擦除后会新建一个new Object(),真有需要的话可以使用反射来获取实例(需要T有无参构造函数)
public static T createInstance(Class clz){
return clz.newInstance();
}
C++模板的实现,是在编译期,为每一个用到的不同类型的模板类,如List< int>,List< float>等,都生成一个不同的类;如果是模板方法,则生成不同的方法,除了会因此而『模板代码膨胀』外,没有上述Java泛型的缺点,感觉Java为了向下兼容,妥协了很多,这也是没有办法的事。
而且虽然C++模板的很多特性,Java泛型不支持,但现在的码农界大多不需要语言的特殊技巧,更多注重的是设计、框架、理念,为了用而去用,反而落了下成。当然,看看C++模板在STL、BOOST等框架中大规模的使用,就知道想要进阶成为大神,或者想要编写框架,模板的熟练使用是必须的。