java泛型:擦除/桥方法/协变(不要在新代码中使用原生态类型) ---- effective java notes

2,396 阅读7分钟
原文链接: blog.csdn.net

在java中,声明一个或者多个类型参数的类或者接口就是泛型;泛型是java 1.5之后出现的,在我看来比较划时代,一部分java 开发者会体会到,无论在表述还是安全性的方面,泛型的出现解决了Object的尴尬;

  • 泛型的使用域比较广,这里不做赘述,主要围绕 协变 , 擦除 , 进行解释

1.泛型优于原生态类型

  • 泛型是指明类型,或者使用?作为无限通配符的类或者接口,如: List< String > ,List< ? >, List< E >等

  • 原生态类型就是1.5之前的无泛型参数的实现:List等;没有< E >这样的泛型参数;

首先我们来看:原生态使用带来的安全性问题:(以List为例): 原生态:

List list = new ArrayList();
list.add(new Head());


Simple s = (Simple)list.get(0);//error

首先我们的目标list是要存储Simple实例的集合,在list.add(new Head());我们加入Head实例,理论上是不应该的,但是缺乏检测,编译时候不会有error抛出,当取数据时候,由于没有报错,ok我们以为没有错误,但是在运行时候出错了:存的是Head,取得是Simple;这时候会有error:ClassCastException 但是,使用参数化泛型:

List list = new ArrayList();
list.add(new Head());//error

在add时候就会报错,因为java中泛型会在编译器类型检查,这就避免了运行时候的ClassCastException,所以说比之于原生态泛型.参数化泛型是有严格的编译器检查,更加安全;

所以如果使用原生态类型就失去了泛型的安全性和表述方面的所有优势;如果不明确泛型也不要使用原生态,可以使用无限定通配符?;或者在泛型类、泛型方法定义时候用T、E来定义类型

2.泛型的擦除—兼容旧版本没有使用泛型的代码

java中泛型是1.5之后添加的,之前是没有泛型话参数的原生态,所以为了兼容没有使用泛型的代码,java中的泛型是 擦除 的;也就是说java中的泛型是伪泛型;

所谓的泛型擦除就是,在指明泛型之后,在编译器期会根据指明泛型来进行类型检查,举个例子: List< String >中泛型参数是String,当add时候,如果add(1);由于泛型是编译器检查类型,所以编译器就报错;可以这样认为,泛型只作用于编译器;当运行时候,泛型参数会被擦除,来兼容没有泛型的代码;

下面来检验一下类型擦除:

                try {
                ArrayList<Integer> arr=new ArrayList<Integer>();
                arr.add(1);//存储int型 
                arr.getClass().getMethod("add", Object.class).
                invoke(arr, "asd");//利用反射存储String
                for (int i=0;i<arr.size();i++) {
                        Log.i("泛型擦除",arr.get(i) + "\n");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

然后看打印的日志:

06-07 13:52:56.487 13013-13013/m.com.example.my_test I/泛型擦除: 1
06-07 13:52:56.487 13013-13013/m.com.example.my_test I/泛型擦除: asd

很奇怪,不是泛型类型检查么,为什么指明Integer的List可以传入String类型?
这里需要注意,反射是在运行期执行的(所以尽量不要使用反射!!!!),反射可以成功避开编译器的泛型类型检查;
然后运行期间,泛型参数已经擦除,此时的ArrayList< Integer >就变成了ArrayList的原生态,那么只要是Object的子类都可以填充了;

废话不多说,相信到这里,大家可以完全的体会到java中泛型的擦除了;

3.泛型中的桥方法

在java中,举一个子父类使用泛型的例子: parent:

public abstract class Person<E> {


    public abstract void setName(E name);

    public abstract E getName();
}

child:

public class Actor extends Person<String> {


    @Override
    public void setName(String name) {

    }

    @Override
    public String getName() {
        return "LI_HUA";
    }
}

在java中这样继承的例子很多(出于复用封装,解耦的目的,这里不做赘述),前面我们已经知道,泛型会在运行时候擦除,那么这里在运行时候我们的父类泛型参数岂不是会变成Object(注意泛型擦除之后,泛型可以理解为其最高父类,没有明确指明的情况下应当是Object);

Person<Object>

那么所谓的继承在运行期间岂不是建立在Object为参数的继承;而子类的泛型参数还是String,那么所谓的重写岂不是成为重载?
ok 不要担心,这里会有一个 桥方法 出现,我们先来看子类Actor的反编译代码:

public class m.com.example.my_test.Actor extends m.com.example.my_test.Person<java.lang.String> {
  public m.com.example.my_test.Actor();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method m/com/example/my_test/Person."<init>":()V
       4: return

  public void setName(java.lang.String);
    Code:
       0: return

  public java.lang.String getName();
    Code:
       0: ldc           #2                  // String LI_HUA
       2: areturn

  public java.lang.Object getName();
    Code:
       0: aload_0
       1: invokevirtual #3                  // Method getName:()Ljava/lang/String;
       4: areturn

  public void setName(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #4                  // class java/lang/String
       5: invokevirtual #5                  // Method setName:(Ljava/lang/String;)V
       8: return
}

我们猜的没有错在运行时候:

  • public java.lang.String getName();变成了 public java.lang.Object getName();

  • public void setName(java.lang.String);变成了 public void setName(java.lang.Object);

这时候 桥方法 出现了,java.lang.Object getName()会调用java.lang.String getName();
public void setName(java.lang.Object);会调用setName(java.lang.String)

有种偷梁换柱的感觉有木有;虚拟机这样的调用,就解决了泛型擦除和java继承的冲突!

4.协变

java泛型还有一个需要注意的一点;数组不可以存储泛型,这样会有error:ClassCastException的隐患,所以在编译器会有warn;

首先来看协变的概念:如果Sub是Subper的父类,那么Sub[]也是Subper的父类;那么可以的出结论数组是协变的; 但是对于泛型:如果如果Sub是Subper的父类,那么List< Sub >和List< Subper >仅仅是两个不同的泛型而已;没有任何联系;

可能到这里你会想到,java不就是面向对象么,继承不是他的特性么,泛型继承都无法实现,那不是很废么?ok 我们展示一段代码:

Object[] arr = new int[2];
arr[0] = "你好"//ArrayStoreException


List<Object> list = new ArrayList<Integer>();//Incompatible types
list.add("你好");

是的数组可以协变,所以Object[] arr = new int[2];是顺理成章的,但是这样的话,当arr[0] = “你好”时候编译不会出错,运行时候就会error:ArrayStoreException
同样泛型在List< Object > list = new ArrayList< Integer >();//Incompatible types就会由编译器报错;当然这也是泛型编译受检的特性;
对于一个IT programmer来说,编译就提示error是不是要比运行时的error更加舒服呢?

当然简单的例子,你看不到泛型的优势,也体会不到数组存储泛型编译器报错的用意: 那么下面来看这个例子,摘自effective java:

 List<String>[] stringLists = new List<String>[1];   //(1)
 List<Integer> intList = Array.asList(42);           //(2)
 Object[] objects = stringLists;                     //(3)
 objects[0] = intList;                               //(4)
 String s = stringLists[0].get[0]                    //(5)

假设创建泛型数组是成立的(前提):

  • 1.初始化泛型数组,泛型元素泛型为String;

  • 2.初始化泛型集合,泛型为Integer;

  • 3.由于数组是协变的,Object是父类,所以 Object[] objects = stringLists; 也成立

  • 4.objects[0] = intList; 由于泛型是可擦除的;所以无论 List< String >还是List< Integer >运行时都会变为List,那么运行时候第四行不会ArrayStoreException;

  • 5.运行之后会抛出ClassCastException,因为add的是int,取到的是s,自然类型不匹配

到这里是不是觉得很坑;瞒天过海,最后ClassCastException;其实在这里擦除和协变确实都起到一部分频弊的作用,让一个错误代码硬是从第一行run到第五行才抛出error; 如果是这样资源时间会不会大量损耗呢?

所以:不可以定义泛型数组!!!!

到这里就介绍完了,期待reader的宝贵意见! 谢谢 共同进步.