前几次讲座,我们在程序里面看到了Generics,今天我们就来专门谈一谈。
先看Generics的作用:They were designed to extend Java's type system to allow “a type or method to operate on objects of various types while providing compile-time type safety”。这里明白地指明了Generics泛型的两个用处,一是允许一个类或者方法操纵不同类型的对象,二是提供编译时类型安全。这是在Java5里面引进来的。这是Java设计团队与时皆进的结果,又是跟历史妥协的结果,基本达到目的,但是远不完美,业界毁誉参半。

为了了解这个概念,我们从头来,先看没有Generics的时候我们怎么写程序的。
先写一个简单的程序,打印数据数组和字符串数组,代码如下(OverwriteTest.java):

public class OverwriteTest {
    public static void printArray(Double[] dArray) {
        for (Double d : dArray) {
            System.out.println(d);
        }
    }
    public static void printArray(String[] sArray) {
        for (String s : sArray) {
            System.out.println(s);
        }
    }
    public static void main(String args[]) {
        Double[] dArray = { 1.618, 2.71828, 3.14159 };
        String[] sArray = { "I", "love", "beijing", "tiananmen" };
        printArray(dArray); 
        printArray(sArray); 
    }
}

这是初学Java的时候常见的例子,打印数组,很简单。刚开头提供一个printArray(Double[])用来打印数据数组,然后老师会说:那要打印一个字符串数组要怎么办呢?要不要写一个printStringArray()方法?不用的,同学们,我们可以用到方法重载的特性,还是用printArray()这个方法,但是给不一样的参数就可以了,如printArray(String[])。
这是一个好办法,不用写多个不一样的方法名了,利用不同的参数进行重载。不过其实还是要写很多方法的,只不过名称一样。有没有一种办法能真的只写一个方法呢?有的,用Object来替代是一种办法,另一种办法就是利用Generics,我们把上面的程序改写一下。代码如下(GenericMethodTest.java):

public class GenericMethodTest {
    public static <E> void printArray(E[] eArray) {
        for (E e : eArray) {
            System.out.println(e);
        }
    }
    public static void main(String args[]) {
        Integer[] iArray = { 1, 2, 3, 4, 5 };
        Double[] dArray = { 1.618, 2.71828, 3.14159 };
        String[] sArray = { "I", "love", "beijing", "tiananmen" };

        printArray(iArray);
        printArray(dArray); 
        printArray(sArray); 
    }
}

我们用Generics观念改写了printArray(),不用对每一种数据类型都增加一个同名方法了,而是用了一种通用的抽象类型E(可以随意起名的,习惯上常用的有E, T, K,V等)。

    public static <E> void printArray(E[] eArray) {
        for (E e : eArray) {
...

仔细看一下泛型方法的定义方式,在方法返回值之前声明了一个,表明这个方法中用的E是一个通用的抽象类型。后面的参数定义和方法体里面的使用都直接用E就可以了。简单来说,就是把以前写的具体的类型如String, Double之类的统一用E代替。main()里面我们试了三种具体的数据类型,都可以打印出来。
这个真好啊,接着我们就会有点纳闷,这个究竟是如何做到的呢?我们可以试着把生成的.class文件反编译一下,
printArray()方法的反编译结果是:

  public static <E> void printArray(E[] eArray) {
    for (Object e : eArray) {
      System.out.println(new Object[] { e });
    }
  }

原来在编译之后,它把抽象数据类型E全部变成了Object,就是Java里面的对象之母。其实在没有Generics之前,我们自己也用Object来实现泛化。你们可以自行把上面的printArray()代码的参数改成Object[],一样的结果。
同样的,如果类里面既有泛型方法又有普通的方法,那么调用的时候会用到普通的方法。如printArray(E[])和printArray(String[]),调用printArray({“I”,”love”,”beijing”})的时候是调用的普通方法printArray(String[]),原因也是因为编译之后printArray(E[])其实变成了printArray(Object[])。
好,我们使一个坏,在定义printArray(E[])的同时非要再定义一个printArray(Object[]),看会出什么结果。试验的结果如下:
编译出错:Erasure of method printArray(E[]) is the same as another method in type GenericMethodTest。
现在清楚了,原来编译器玩了一个手脚,表面上提供了泛型,其实内部用了Object。这种技术称之为erasure(类型擦除)。这么做的主要考量是不想变动虚拟机,保持向后的兼容性。
如果应用中只简单地应用到这里,大体就这么理解没错。不过事情并不是这么简单,接着往下研讨,还有好多知识(以及坑)等待着我们。

除了泛型方法,还有泛型类。我们先看一个例子,代码如下(GenericTest.java):

import java.util.ArrayList;
import java.util.List;
public class GenericTest {
    public static void main(String[] args) {
        List<String> name = new ArrayList<String>();
        List<Number> number = new ArrayList<Number>();
        name.add("Alice");
        number.add(234);
        getData(name);
        getData(number);
   }
   public static void getData(List<?> data) {
      System.out.println("data :" + data.get(0));
   }
}

我们用了现成的泛型类List和ArrayList。这两个类再Java5之后有了泛型版本。在使用的时候跟以前的写法稍有不同, Listname = new ArrayList();在声明和创建的时候都加上了类型标记。表示这个List里面包含的是Number(Java7之后,可以简写为:Listname = new ArrayList<>();)。
回想一下,没有泛型的时候,我们是怎么弄的?程序示例如下:

List arrayList = new ArrayList();
arrayList.add(100);
arrayList.add("Test String");
for(int i = 0; i< arrayList.size();i++){
    Number n = (Number)arrayList.get(i);
}

定义了一个ArrayList,往里面放number,又放String,最后打印出来,报错!因为ArrayList里面是可以放任何东西的,编译器不检查,只有到运行时出错。这个时候我们说类型不安全了。泛型类解决了这个问题,在编译的时候就能检查到这种错误。如果对于ArrayList的泛型版本,我们写numberList.add("314");,编译出错:The method add(Number) in the type Listis not applicable for the arguments (String)。这样保证放进去的是number,拿出来的时候不用进行类型转换了,也是number。有了这种机制,我们的程序安全多了。对于集合类,里面要放一堆东西,泛型就特别有用。

我们再看怎么自己定义一个泛型类。

public class GenericClass<T> {
    private T t;
    public void put(T t) {
        this.t = t;
    }
    public T get() {
        return this.t;
    }
    public static void main(String[] args) {
        GenericClass<Integer> iClass = new GenericClass<Integer>();
        GenericClass<String> sClass = new GenericClass<String>();
        iClass.put(new Integer(10));
        System.out.println(iClass.get());
        sClass.put(new String("test."));
        System.out.println(sClass.get());
    }
}

语法好简单,就是在定义类的时候在类名后面跟上(T也是随意的,习惯上有几个常用的字母。)有了泛型类,使用的时候既能支持不同的数据类型,又可以类型安全,很好很强大。正好就是本讲座最开头说过的那句话-Generics的作用。
刚才定义泛型方法的时候讲过编译器其实是转成了Object,那对泛型类是不是这样呢?我们看一下。反编译之后,看到main()里面编程了这样:

public static void main(String[] args) {
    GenericClass sClass = new GenericClass();

    sClass.put(new String("test."));
    System.out.println((String)sClass.get());
}

注意:
第一句,我们写的是GenericClasssClass = new GenericClass();,编译后变成了GenericClass sClass = new GenericClass();其实就是普通的类。
最后一句,我们写的时候是sClass.get(),编译后变成了(String)sClass.get()。编译器自动加上了类型转换和检查。
所以啊,实际上,泛型就是一个编译时的处理,内部用了Object,加上了类型转换和检查。代码之下,了无秘密。

在前面的程序中,我们定义了两个对象:

    GenericClass<Integer> iClass = new GenericClass<Integer>();
    GenericClass<String> sClass = new GenericClass<String>();

那iClass和sClass算一个类还是两个类呢?我们可以看一下:
增加如下几行代码:

    Class GenericClassIntegerCls = iClass.getClass();
    Class GenericClassStringCls = sClass.getClass();
    System.out.println(GenericClassIntegerCls);
    System.out.println(GenericClassStringCls);
    if (GenericClassIntegerCls.equals(GenericClassStringCls)) {
        System.out.println("the same");
    }

运行结果显示“the same”,确实是同样的一个类。意味着运行时的泛型类型都擦除了。泛型带来的许多坑都是由这个造成的,初学者会有很多迷惑。
有趣的问题来了,既然Java泛型只是一个编译时,编译之后类型被擦除了,那么可不可以用别的办法骗一下,把一个不同类型的值塞给某一个类型?我们记得reflection吧?它是运行时动态起作用的,可以骗过泛型编译。比如说ArrayList, 我们直接用list.add(100)试图加入一个数值的时候,编译报错:

The method add(int, String) in the type List<String> is not applicable for the arguments (int)。

不让做。我们用reflection就可以做了:

Class<?> clz = list.getClass();
Method m;
m = clz.getMethod("add", Object.class);
m.invoke(name, 100);

大家可以试试看。实际代码我们不建议这样。

虽然到现在为止例子都是一个类型参数,泛型类其实是可以多个参数的。我们看一个例子,代码如下(Pair.java):

public class Pair<K,V> {
    private K key;
    private V value;
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    } 
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

上面的Pair泛型类需要两个类型参数K和V。
使用的程序如下(Test.java):

public class Test {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
    public static void main(String[] args){
        Pair<Integer,String> p1 = new Pair<>(1,"aaa");
        Pair<Integer,String> p2 = new Pair<>(1,"bbb");
        System.out.println(Test.compare(p1, p2));
    }
}

其实,泛型基本的内容就是这些。确实达到了“泛”的目的,更加通用,更加接近人的思维。我以前的讲座说过,“问题空间”和“解决空间”的相似度决定了系统实现的复杂度,更加接近于人的思维结构的技术总是会有前途的。
这里,我想额外多说几句话。IT界发展很快,可以说日新月异,每天都有不同的技术不同的框架不同的语言不同的论文不同的工具涌现,“乱花渐欲迷人眼”,这个时候,我们怎么分清楚主流支流弄明白是大的技术潮流还是小的技术改进甚至是昙花一现的泡沫,就显得很重要和迫切,不然就在知识技能的快速迭代中迷失了,好多人说程序设计是一个年轻人的事,过了35岁甚至30岁就不好干了,吃的是青春饭。其实,这是没有理解技术的本质造成的误解。技术发展是很快,但是理论,算法,数学模型,方法论,这些基础的还是相对稳定的,我们要抓住这个实质,去跟进去比较去取舍,这样就能立于不败之地。有人说过,“时髦技术如一朝风月,基础知识如万古长空”。

下面我们看一下泛型比较绕的地方。还是上面的例子,我们增加一个方法:

    public static void process(GenericClass<Number> obj){
        System.out.println(obj.get());
    }

这个process()的参数是一个GenericClass,我们知道Number为Integer的父类,那么我们可不可以传递GenericClass过去呢?试了一下,编译报错:The method process(GenericClass) in the type GenericClassis not applicable for the arguments (GenericClass)。答案是不能。
这里要特别强调,划重点了,同学们。Integer是Number的子类,但是GenericClass却不是GenericClass的子类。这有一个术语叫invariant。
自然,我们也不能写另外一个重载方法process(GenericClass),因为类型擦除的原因,者会被认为与process(GenericClass)是一样的。
难道要改方法名吗?当然可以,不过这也太笨了吧。Java自然不会这么笨,这个时候,可以使用通配符。代码如下:

    public static void process(GenericClass<?> obj){
        System.out.println(obj.get());
    }

?通配符可以匹配任何类型。这样就可以传入GenericClass也可以传入GenericClass。

解决了一个问题,又会引来一个新的问题。?通配所有类型,那么传入GenericClass也可以了,如果这个process()方法是要进行计算,就会出问题了。能不能让它指定传入的是某一类的类型呢?答案是可以,代码如下:

    public static void process(GenericClass<? extends Number> obj){
        System.out.println(obj.get());
    }

规定了参数可以传入Number及其子类。这个时候,如果写代码的时候给了一个GenericClass,编译报错:

The method getVal(GenericClass<? extends Number>) in the type GenericClass<T> is not applicable for the arguments (GenericClass<String>)。

我们回过头继续探讨Covariant(协变)的问题,我们知道了由于这个原因,编译器规定GenericClass不能传给GenericClass。但是有些时候我们需要这种能力,那有没有可能想一点办法变相实现呢?我们研究一下。
先看普通的代码:

    static List<String> str = Arrays.asList("Test String");
    static List<Object> obj = Arrays.asList(new Object());
    static class Reader<T> {
        T read(List<T> list) {
            return list.get(0);
        }
    }
    static void test() {
        Reader<Object> objReader = new Reader<Object>();
        Object o = objReader.read(obj);
        //String s = objReader.read(str);
    }

这段代码的意图很简单,就是提供一个通用的Reader,从List中读取数据,希望可以支持List又可以支持List。但是在String s = objReader.read(str);这行时,编译器会报错:The method read(List) in the type .Reader is not applicable for the arguments (List)。
我们可以利用?的上下界技巧,来达到目的,我们修改一下,给这个程序一个协变版本,代码如下(CovariantReading.java):

 

import java.util.Arrays;
import java.util.List;
public class CovariantReading {
    static List<String> str = Arrays.asList("Test String");
    static List<Object> obj = Arrays.asList(new Object());
    static class CovariantReader<T> {
        T read(List<? extends T> list) {
            return list.get(0);
        }
    }
    static void cotest() {
        CovariantReader<Object> objReader = new CovariantReader<Object>();
        Object o = objReader.read(obj);
        String s = (String) objReader.read(str);
    }
    public static void main(String[] args) {
        cotest();       
    }
}

我们声明了T read(List list),表示只要是某个类型及其子类的容器中都可以读出数据来。
同样,我们也可以写出协变版本的writer,代码如下(CovariantWriting.java):

import java.util.ArrayList;
import java.util.List;
public class CovariantWriting {
    static List<String> str = new ArrayList<String>();
    static List<Object> obj = new ArrayList<Object>();
    static class CovariantWriter<T> {
        void write(List<? super T> list, T item) {
            list.add(item);
        }
    }
    static void cotest() {
            CovariantWriter<String> strWriter = new CovariantWriter<String>();
            strWriter.write(obj, "Test");
            strWriter.write(str, "Test");
    }
    public static void main(String[] args) {
        cotest();       
    }
}

我们通过write(List list, T item)声明了把一个某类型的item写到该类型或者父类型的容器中。
这种方式,有点变相实现了父子关系在泛型下的继承,看起来是这样。

另外,在泛型下,类型不支持原始类型,如int, long这些,必须是Object。其原因就是泛型的类型擦除,全部弄成了Object,如果要支持int, long,就会要考虑int,long和Object复制兼容,比较麻烦,在Java5版本发布之前来不及了,就搁置了。这一点,是Java设计团队的一个著名的偷懒的实例。

还有一个经典的问题就是泛型数组。不好理解,弄得人头大。
在Java里面,不让定义类似这样的泛型数组new List[10]。这是刚开始接触Java泛型时最不能理解的地方,很恼火。这么写List[] name = new ArrayList[10];编译报错:Cannot create a generic array of ArrayList。
原因呢,有一段说明特别好,摘抄如下:

Covariant: It means you can assign subclass type array to its superclass array reference. For instance, Object objectArray[] = new Integer[10]; // it will work fine
Invariant: It means you cannot assign subclass type generic to its super class generic reference because in generics any two distinct types are neither a subtype nor a supertype. For instance, List<Object> objectList = new ArrayList<Integer>(); // won't compile
Because of this fundamental reason, arrays and generics do not fit well with each other.

我曾经想用别的方式说明这个原因,但是看去看来,还是觉得这个说明最好。让我再解释,我还是这句话。

有代码可以更加清晰。Sun的文档说“不能创建一个确切的泛型类型的数组”,举例假设可以会带来什么问题:

1) List<Integer> arrayOfIdList[] = new ArrayList<Integer>[10];// Suppose generic array creation is legal.
2) List<String> nameList = new ArrayList<String>();
3) Object objArray[] = arrayOfIdList; // that is allowed because arrays are covariant
4) objArray[0] = nameList;
5) Integer id = objArray[0].get(0);

但是用?可以。

1) List<?> arrayOfIdList[] = new ArrayList<?>[10];
2) List<Integer> nameList = new ArrayList<Integer>();
3) Object objArray[] = arrayOfIdList; 
4) objArray[0] = nameList;
5) Integer id = (Integer)objArray[0].get(0);

这个让人很恼火。如果我们真的要一个泛型数组,那么应该怎么办呢?有办法的。我们可以用Array的newInstance生成泛型数组。还是用的reflection绕过。

GenericClass<Integer>[] lClass = (GenericClass<Integer>[])Array.newInstance(GenericClass.class, 10);

我的介绍就到此为止,我觉得这已经是一个进阶了解的大体内容了。

泛型是在Java出现了一些年之后再发展出来的,要考虑历史兼容性,Java兼容性的目标是向后兼容(backward compatibility),以前的代码还能继续使用,因此限制了手脚。泛型的这些奇奇怪怪的问题就是明显的例子。对比C++,Java的泛型有诸多限制和不好理解的地方,以至于Bruce Eckel大师曾经写过一篇文章说:这不是泛型。但是我们要考虑Java设计团队的苦衷,考虑历史,非不为也,实不能也。泛型不泛型,并没有确切的标准。Java团队也有了路线图,继续完善泛型,根据我的了解,不遥远的未来一定会更好地支持泛型数组以及原始类型。
技术总是在前人的基础上不断进步的,但是我们要以历史的眼光看待这个进程,不可轻易否定前人,对前人的不足要有现场之理解。古人言志:“为往圣继绝学”,一代一代技术人,薪火相传,站在前人的肩膀上前行,加深认知,改变世界。