Java那些事:泛型(一)

2014-11-23 23:26:23 · 作者: · 浏览: 1

“让错误尽量在编译被发现”

“你必须知道边界所在,才能成为高手”

---《Thinking in Java》

错误在编译时被发现是十分让人向往的,Java泛型在JDK1.5中的引入目的就是“让编译器承担更多的工作,保证类型的正确”。但是随之而来的是开发社区褒贬不一的观点,就像在异常中说的那样----Java的泛型和异常一样,备受人们争议。

Java泛型带来的优秀产物就是集合框架,相比于以前使用Java集合框架,使用泛型后的集合框架显得十分简洁和安全。随之而来的便是Java泛型给人们带来的种种疑惑,设计者曾说Java泛型的主要设计灵感是来自C++的模板,但是稍微知道C++的人也许都会对Java的泛型感到失望,如何优雅地使用Java泛型进行优秀的程序设计是值得探索的。

1.Java的泛型为什么会这样?

Java的泛型是在Java出现几乎10年后才出现的特性,而此时已经存在大量的旧代码,作为一门广泛使用的生产语言,Java不得不考虑兼容性。为了让使用新特性的人能够逐步的迁移到新的平台,新代码必须和旧代码保持兼容,这是一个十分伟大的动机,不会在一夜之间破坏现有的所有代码!所以Java的设计者们在一个月黑风高的晚上决定使用“擦除机制”来设计泛型!

2.擦除机制

正确理解泛型概念的首要前提是理解擦除机制(type erasure)。Java中的泛型基本上都在编译器这个层次上来实现的。在生成的Java字节代码中是不包含泛型中的类型信息(在运行时时候都是原生类型(raw type))。在编写泛型代码的时的任何类型信息都会被编译器在编译的时候去掉。这个过程称为“类型擦除”。在代码中如:List 与List 等,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的(术语:不可具体化)。

擦除的整个过程也比较容易理解:首先找到用来替换类型参数的具体类。这个具体类不指名则默认Object,如果指定了参数类型的上界,那么就使用这个上界来替换类型参数。同时去掉类型声明,即去掉<>的内容,比如T get()方法声明就编程了Object get(),List 就变成了List。接下来有时候可能会生成一些桥接方法。

3.不可具体化的类型

不可具体化的类型是指:运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。泛型是典型的不可具体化类型,参考ArrayList 中放入取出对象时候的源代码

public boolean add(E e) {
       ensureCapacityInternal(size + 1); // Increments modCount!!
       elementData[size++] = e;
       return true;
    }
 
public E get(int index) {
       rangeCheck(index);
 
       return elementData(index);
    }
 
@SuppressWarnings("unchecked")
    EelementData(int index) {
       return (E) elementData[index];
    }


运行时实际持有Object数组,但是编译的时候是可以有效的判断放入的是否是String类型,从而避免ClassCastException异常。

与之相反的便是”可具体化的类型“,数组便是一个例子。数组总是在运行时才检查他们元素的类型约束。比如

  Object[]father = new Father[10];
                   father[0]= new Son();
                   father[1]= new Integer(100);

这里并没有编译错误,但是运行时会抛出ArrayStoreException异常,和泛型的特点相反。比如如下大家熟悉的代码:

Listlist = new ArrayList
   
    ();
    

这段代码是非法的。

更加让人感到疑惑的是如下的代码:

 		   Father[]father = new Son[10];
                   father[0]= new Son();
                   father[1]= new Father();

这段代码会在第三行抛出ArrayStoreException异常,是的,数组的类型是强制约束。按理来说,一个数组中放置的元素应该都是安全的,父类的数组是可以放置子类型的元素。子类的数组是可以向父类数组转型成功的(这里确实也转型成功了),但是运行时却抛出异常,最终原因就是实际运行的时候数组并不是Father类型,而是Son类型。这里的可具体化和泛型的不可具体化就产生了矛盾(如果你new了一个T并将其转型为Object类型,那么是可以放入任何类型的,编译时期是不能被编译器发现,但是运行时会很有可能会抛出ArrayStroeException或者是ClassCastException异常),于是又是一个月黑风高的晚上,Java的设计者们决定了另外一个一不做二不休的决定-----不能创建泛型数组。

在Java中你不能new T[],也不能通过如这样看似合理的ArrayList asd = new ArrayList []; 代码,因为我是可以将其转型为Object[]类型,然后便可以向其中放入任何类型的类型了,而运行使JVM却对你说了NO,这对Java泛型就是一种赤裸裸的嘲讽!

4.通配符

泛型的通配符让Java的泛型变得更加的灵活和强大(虽然并不是那么强大)。参数化类型是不可变的,也就是说下面这样的代码是不能通过编译的:

 		Listf = new ArrayList<>();
                List
      
       f1 = new ArrayList<>(); f= f1;
       

第三行会报错!看似f1应该是f的子类,但是与直觉相悖。原因还是在”擦除“,由于List中的类都是被Object替代,而List 中均被String替换,这里面如何来协调就是一个问题。

如果想写出更加强大的API,那么通配符就是你应该选择的。如下代码:

public class Seven {
         publicstatic void main(String[] args) throws InstantiationException, IllegalAccessException{
                   Stack
           
            stack = new Stack<>();
                   List
            
             list = new ArrayList<>(); list.add(10); list.add(100); list.add(1000); Iterable
             
              iter = list; stack.pushAll(iter); } } class Stack
              
               { privateE e; publicvoid push(E e){ } /** * 这里是有通配符来提高API的灵活性 * @param i */ publicvoid pushAll(Iterable
                i){ for(Ee:i) push(e); } }
              
             
            
           


其中Stack类在创建实例的时候使用了Number类型作为其类型参数,那么在创建pushAll这个方法的时候,如果使用pushAll(Iterable i)来作为签名,由于类型擦除机制,Java将在编译这一层面上对你SayNo!这里就引出PECS原则,即:如果参数化类型表示一个T生产者(Producer),就使用 ,如果它是一个T的消费者(Coms