java泛型

       在知乎在碰到了一篇好文:你见过哪些令你瞠目结舌的 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向下兼容,做出了很大的妥协,但也引出了很多问题。

1.Java泛型原理

1.1 类型擦除

       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,这通常被称为『有界类型』。

1.2 桥方法

       这个其实是类型擦除引出来的问题,不过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后允许返回值只要是原返回值的派生类,就可以重写,使用的也是桥方法来实现的。

2.Java泛型分类

       具体使用时,可以被分为泛型接口、泛型类、泛型方法。使用起来其实都是大同小异,我暂时只找到一个区别,就是泛型方法的参数可以使用< ? extends Comparable>< ? super Comparable>这样的上下限参数类型通配符,而泛型接口、泛型类只能用< T extends Comparable>这样明确且指定上限的类型限定符。

3.Java泛型缺点

       由于为了向前兼容,使用了类型擦除这样的实现,导致出现一系列的问题,开发时需要留意:

3.1 泛型类中不能使用Static变量

       这个很好理解,类型擦除后,泛型类其实就是是同一个类,如ArrayList< String>和ArrayList< Integer>在擦除后都是ArrayList,所以如果使用了静态变量,其实所有的泛型类都是共享的,这与设计是相违背且易乱用和出错的,所以直接禁止使用。

3.2 泛型不支持基本数据类型

       这个上面也提过了,基本数据类型没有一个统一的基类,所以不支持类型擦除。

3.3 instanceof运行时判断

       由于类型擦除后变成了另外一个类,所以我们不能使用如下的方法在运行时判断:

if( arrayList instanceof ArrayList)  

Java限定的这种判断方法:

if( arrayList instanceof ArrayList) 

再具体的判断就做不到了

3.4 异常不能被泛型扩展

public class Problem<T> extends Exception{......} 

因为类型擦除后,抛出的异常就无法找到正确的处理分支,如下所示:

try{  
}catch(Problem e1){  
...  
}catch(Problem e2){  
...  
} 

擦除后会变为:

try{  
}catch(Problem e1){  
...  
}catch(Problem e2){  
...  
} 

3.5 泛型不能实例化

a = new T();

这样写是不行的,因为类型擦除后会新建一个new Object(),真有需要的话可以使用反射来获取实例(需要T有无参构造函数)

public static T createInstance(Class clz){
    return clz.newInstance();
}

4.C++模板

       C++模板的实现,是在编译期,为每一个用到的不同类型的模板类,如List< int>,List< float>等,都生成一个不同的类;如果是模板方法,则生成不同的方法,除了会因此而『模板代码膨胀』外,没有上述Java泛型的缺点,感觉Java为了向下兼容,妥协了很多,这也是没有办法的事。
       而且虽然C++模板的很多特性,Java泛型不支持,但现在的码农界大多不需要语言的特殊技巧,更多注重的是设计、框架、理念,为了用而去用,反而落了下成。当然,看看C++模板在STL、BOOST等框架中大规模的使用,就知道想要进阶成为大神,或者想要编写框架,模板的熟练使用是必须的。

你可能感兴趣的:(java泛型,java,桥方法,c++模板,Java)