Java基础(1)-泛型

10 March 2011

 

一、Basic FAQ:

1. What are Java Generics?

     Java泛型是什么?

     Java Generics ara language feature that allows for definition and use of generic types and methods.

     Java泛型指的是一种允许定义通用类型和方法的语言特性。

2. What is the primary purpose of Java Generics?

     Java泛型最初的目的是什么?

     Java Generics are invented primarily for implementation of generic collection.

     Java泛型最初是为了实现通用集合类设计的。

3.What is the benefit of using Java generics?

     使用Java泛型的好处???

     Early error detection at compile time.

     能够在编译期检查类型错误。

4.What does type-safe mean?

     什么叫做类型安全

     In Java, a program is considered type-safe if it compiles without errors and warnings and does not raise any unexpected ClassCastException at runtime.

     在Java里,类型安全指的是:如果一个程序编译期未发现Error或Warning,那么在运行时就不会报ClassCastException。

 

奇怪的特性

     泛型有些特性,令人困扰,如下:

 

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

 

协变(convariant)

     Within the type system of a programming language, covariance and contravariance refers to the ordering of types from narrower to wider and their interchangeability or equivalence in certain situations (such as parameters, generics, and return types).     ——Wikipedia(偶不会翻译*A*)

     简单来说,可协变表示子类型的引用可以赋值给父类性,不可协变则表示子类型引用不能赋值给父类型。以数组和泛型为例:

     数组是可协变的,因为Number[] a = new Integer[]();是成立的。

     泛型是不可协变的,因为List<Number> ln = new ArrayList<Integer>();编译会出错。

 

 

     由于数组是可协变的,而泛型不可协变,导致数组能够协变而泛型不能协变的另一个后果是,不能实例化泛型类型的数组(new List<String>[3]是不合法的),除非类型参数是一个未绑定的通配符(new List<?>[3]是合法的)。

    可是,为什么泛型不能协变呢?——因为这样会破坏类型安全

     假设可以协变,那么

     List<Integer> li = new ArrayList<Integer>();

     List<Number> ln = li;

     ln.add(new Float(3.14));

     发生了什么?本来是放Integer的List里面加入了Float!。

 

类型擦除

      Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

编译器如何处理泛型?

     通常情况下,一个编译器处理泛型有两种方式:

     1.Code specialization。在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型list,可能需要针对string,integer,float产生三份目标代码。

     2.Code sharing。对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。

     C++中的模板(template)是典型的Code specialization实现。C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。这样会导致代码膨胀(code bloat),不过有经验的C++程序员可以有技巧的避免代码膨胀。

     Code specialization另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。

     Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

什么是类型擦除?

     类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。

     类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。

     类型擦除的主要过程如下:

     1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。

     2.移除所有的类型参数。

     如

interface Comparable <A> { 
  public int compareTo( A that); 
} 
final class NumericValue implements Comparable <NumericValue> { 
  priva te byte value;  
  public  NumericValue (byte value) { this.value = value; }    
  public  byte getValue() { return value; }    
  public  int compareTo( NumericValue t hat) { return this.value - that.value; } 
} 
-----------------
class Collections {  
  public static <A extends Comparable<A>>A max(Collection <A> xs) { 
    Iterator <A> xi = xs.iterator(); 
    A w = xi.next(); 
    while (xi.hasNext()) { 
      A x = xi.next(); 
      if (w.compareTo(x) < 0) w = x; 
    } 
    return w; 
  } 
} 
final class Test { 
  public static void main (String[ ] args) { 
    LinkedList <NumericValue> numberList = new LinkedList <NumericValue> (); 
    numberList .add(new NumericValue((byte)0));  
    numberList .add(new NumericValue((byte)1));  
    NumericValue y = Collections.max( numberList );  
  } 
}
 

经过类型擦除后的类型为

     interface Comparable { 
  public int compareTo( Object that); 
} 
final class NumericValue implements Comparable { 
  priva te byte value;  
  public  NumericValue (byte value) { this.value = value; }  
  public  byte getValue() { return value; }  
  public  int compareTo( NumericValue t hat)   { return this.value - that.value; } 
  public  int compareTo(Object that) { return this.compareTo((NumericValue)that);  } } 
-------------
class Collections {  
  public static Comparable max(Collection xs) { 
    Iterator xi = xs.iterator(); 
    Comparable w = (Comparable) xi.next(); 
    while (xi.hasNext()) { 
      Comparable x = (Comparable) xi.next(); 
      if (w.compareTo(x) < 0) w = x; 
    } 
    return w; 
  } 
} 
final class Test { 
  public static void main (String[ ] args) { 
    LinkedList numberList = new LinkedList(); 
    numberList .add(new NumericValue((byte)0));  ,
    numberList .add(new NumericValue((byte)1));  
    NumericValue y = (NumericValue) Collections.max( numberList );  
  } 
}
 

 

第一个泛型类Comparable <A>擦除后 A被替换为最左边界Object。Comparable<NumericValue>的类型参数NumericValue被擦除掉,但是这直接导致NumericValue没有实现接口Comparable的compareTo(Object that)方法,于是编译器充当好人,添加了一个桥接方法。

第二个示例中限定了类型参数的边界<A extends Comparable<A>>A,A必须为Comparable<A>的子类,按照类型擦除的过程,先讲所有的类型参数ti换为最左边界Comparable<A>,然后去掉参数类型A,得到最终的擦除后结果。

类型擦除带来的问题

     正是由于类型擦除的隐蔽存在,直接导致了众多的泛型灵异问题。

Q1.用同一泛型类的实例区分方法签名?——NO!

{code}

import java.util.*;

 

public class Erasure{

 

    public void test(List<String> ls){

        System.out.println("Sting");

    }

    public void test(List<Integer> li){

        System.out.println("Integer");

    }

}

{code}

编译该类,

what the f**k! 参数类型明明不一样啊,一个List<String>,一个是List<Integer>,但是,偷偷的说,type erasure之后,它就都是List了⋯⋯

Q2. 同时catch同一个泛型异常类的多个实例?——NO!

同理,如果定义了一个泛型一场类GenericException<T>,千万别同时catch GenericException<Integer> 和GenericException<String>,因为他们是一样一样滴⋯⋯

Q3.泛型类的静态变量是共享的?——Yes!

猜猜这段代码的输出是什么?

import java.util.*;
public class StaticTest{
    public static void main(String[] args){
        GT<Integer> gti = new GT<Integer>();
        gti.var=1;
        GT<String> gts = new GT<String>();
        gts.var=2;
        System.out.println(gti.var);
    }
}
class GT<T>{
    public static int var=0;
    public void nothing(T x){}
}
 

答案是——2!

 

Just remember:

1.虚拟机中没有泛型,只有普通类和普通方法

2.所有泛型类的类型参数在编译时都会被擦除

3.创建泛型对象时请指明类型,让编译器尽早的做参数检查(Effective Java,第23条:请不要在新代码中使用原生态类型)

4.不要忽略编译器的警告信息,那意味着潜在的ClassCastException等着你。

 

 

参考文档

《Java深度历险-泛型》http://www.infoq.com/cn/articles/cf-java-generics

《Java Generics FAQ》http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html

《Java Gotchas》http://www.ibm.com/developerworks/java/library/j-jtp01255.html

 

 

 

 



blog comments powered by Disqus