现在的位置: 首页 > 综合 > 正文

编译期间确定类型安全——泛型(Generics)

2012年12月08日 ⁄ 综合 ⁄ 共 3282字 ⁄ 字号 评论关闭

  泛型是提供给Javac编译器使用的。可以限定集合中输入的类型,让编译器挡住原始程序的非法输入,编译器编译带类型说明的集合时会去掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样,由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其它类型的数据,例如,用反射得到集合,再调用其add方法即可。

  ArrayList<E>类定义和ArrayList<Integer>类引用中涉及如下术语:

  整个称为ArrayList<E>泛型类型,ArrayList<E>中的E称为类型变量或类型参数,整个ArrayList<Integer>称为参数化的类型,ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数,ArrayList<Integer>中的<>念着typeof,ArrayList称为原始类型。

  参数化类型与原始类型的兼容性:
  参数化类型可以引用一个原始类型的对象,编译报告警告,例如,  

  Collection<String> c = new Vector ();

  原始类型可以引用一个参数化类型的对象,编译报告警告,例如,

  Collection c = new Vector<String>();

  参数化类型不考虑类型参数的继承关系:

  Vector<String> v = new Vector<Object>()    // 错误
  Vector<Object> v = new Vector<String>()    // 也错误

  类型擦除

  正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型类似于C++中的模板,但是这种相似性仅限于表面,Java中的泛型基本上都是在编译器这个层次来实现的。属于编译器执行类型检查和类型诊断,然后生成普通的非泛型的字节码,也就是在生成的Java字节代码中是不包含泛型中的类型信息的,使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这种实现技术称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

  很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:

  • 泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class;
  • 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是MyException类型的。也就无法执行与异常对应的catch语句。

  实例分析

  了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的List<Object>和List<String>为例来具体分析:

  public void inspect(List<Object> list) {    
      for (Object obj : list) {        
          System.out.println(obj);    
      }    
      list.add(1); //这个操作在当前方法的上下文是合法的。 
  }
  public void test() {    
      List<String> strs = new ArrayList<String>();    
      inspect(strs); //编译错误 
  }  

  这段代码中,inspect()方法接受List<Object>作为参数,当在test方法中试图传入List<String>的时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List<String>的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。   

  通配符与上下界

  在使用泛型类的时候,既可以指定一个具体的类型,如List<String>就声明了具体的类型是String;也可以用通配符?来表示未知类型,如List<?>就声明了List中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List<?>所声明的就是所有类型都是可以的。但是List<?>并不等同于List<Object>。List<Object>实际上确定了List中包含的是Object及其子类,在使用的时候都可以通过Object来进行引用。而List<?>则其中所包含的元素类型是不确定。其中可能包含的是String,也可能是 Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。正因为类型未知,就不能通过new ArrayList<?>()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于 List<?>中的元素确总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。考虑下面的代码:

  public void wildcard(List<?> list) {
      list.add(1);    //编译错误 
  }

  如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。

  因为对于List<?>中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如List<? extends Number>说明List中可能包含的元素类型是Number及其子类。而List<? super Number>则说明List中包含的是Number及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List<? extends Number>的时候,就可以使用Number类的intValue等方法。

  开发自己的泛型类

  泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如 MyClass<X, Y, Z>。 每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。

    class ClassTest<X extends Number, Y, Z> {    
        private X x;    
        private static Y y; //编译错误,不能用在静态变量中    
        public X getFirst() {
            return x;    
        }    
        public void wrong() {        
            Z z = new Z(); //编译错误,不能创建对象    
        }
    } 

 

参考资料:

  http://www.infoq.com/cn/articles/cf-java-generics

抱歉!评论已关闭.