“三妹啊,听哥慢慢给你讲啊。”我说。

    Java 在 1.5 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来很不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。

    看下面这段简单的代码。

    “三妹,你能想象到在没有泛型之前该怎么办吗?”

    “嗯,想不到,还是二哥你说吧。”

    嗯,我们可以使用 Object 数组来设计 类。

    1. class Arraylist {
    2. private Object[] objs;
    3. private int i = 0;
    4. public void add(Object obj) {
    5. objs[i++] = obj;
    6. }
    7. public Object get(int i) {
    8. return objs[i];
    9. }
    10. }

    然后,我们向 Arraylist 中存取数据。

    1. Arraylist list = new Arraylist();
    2. list.add("沉默王二");
    3. list.add(new Date());
    4. String str = (String)list.get(0);

    “三妹,你有没有发现这两个问题?”

    • Arraylist 可以存放任何类型的数据(既可以存字符串,也可以混入日期),因为所有类都继承自 Object 类。
    • 从 Arraylist 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。

    “嗯嗯,是的呢。”三妹说。

    对比一下,你就能明显地感受到泛型的优秀之处:使用类型参数解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。

    “二哥,那怎么才能设计一个泛型呢?”

    “三妹啊,你一个小白只要会用泛型就行了,还想设计泛型啊?!不过,既然你想了解,那么哥义不容辞。”

    首先,我们来按照泛型的标准重新设计一下 Arraylist 类。

    1. class Arraylist<E> {
    2. private Object[] elementData;
    3. private int size = 0;
    4. public Arraylist(int initialCapacity) {
    5. this.elementData = new Object[initialCapacity];
    6. }
    7. public boolean add(E e) {
    8. elementData[size++] = e;
    9. return true;
    10. }
    11. E elementData(int index) {
    12. return (E) elementData[index];
    13. }
    14. }

    一个泛型类就是具有一个或多个类型变量的类。Arraylist 类引入的类型变量为 E(Element,元素的首字母),使用尖括号 <> 括起来,放在类名的后面。

    然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。

    1. Arraylist<String> list = new Arraylist<String>();
    2. list.add("沉默王三");
    3. String str = list.get(0);

    Date 类型也可以的。

    1. Arraylist<Date> list = new Arraylist<Date>();
    2. list.add(new Date());
    3. Date date = list.get(0);

    其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。

    不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。

    现在,我们来调用一下泛型方法。

    1. Arraylist<String> list = new Arraylist<>(4);
    2. list.add("沉");
    3. list.add("默");
    4. list.add("王");
    5. list.add("二");
    6. String [] strs = new String [4];
    7. strs = list.toArray(strs);
    8. for (String str : strs) {
    9. System.out.println(str);
    10. }

    在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。

    1. class Wanglaoer {
    2. public String toString() {
    3. return "王老二";
    4. }
    5. }
    6. class Wanger extends Wanglaoer{
    7. public String toString() {
    8. return "王二";
    9. }
    10. }
    11. class Wangxiaoer extends Wanger{
    12. public String toString() {
    13. return "王小二";
    14. }
    15. }

    我们使用限定符 extends 来重新设计一下 Arraylist 类。

    1. class Arraylist<E extends Wanger> {
    2. }

    当我们向 Arraylist 中添加 Wanglaoer 元素的时候,编译器会提示错误:Arraylist 只允许添加 Wanger 及其子类 Wangxiaoer 对象,不允许添加其父类 Wanglaoer

    1. Arraylist<Wanger> list = new Arraylist<>(3);
    2. list.add(new Wanger());
    3. list.add(new Wanglaoer());
    4. // The method add(Wanger) in the type Arraylist<Wanger> is not applicable for the arguments
    5. // (Wanglaoer)
    6. list.add(new Wangxiaoer());

    也就是说,限定符 extends 可以缩小泛型的类型范围。

    “哦,明白了。”三妹若有所思的点点头,“二哥,听说虚拟机没有泛型?”

    “三妹,你功课做得可以啊。哥可以肯定地回答你,虚拟机是没有泛型的。”

    “怎么确定虚拟机有没有泛型呢?”三妹问。

    “只要我们把泛型类的字节码进行反编译就看到了!”用反编译工具将 class 文件反编译后,我说,“三妹,你看。”

    1. // Jad home page: http://www.kpdus.com/jad.html
    2. // Decompiler options: packimports(3)
    3. // Source File Name: Arraylist.java
    4. package com.cmower.java_demo.fanxing;
    5. class Arraylist
    6. {
    7. public Arraylist(int initialCapacity)
    8. {
    9. size = 0;
    10. elementData = new Object[initialCapacity];
    11. }
    12. public boolean add(Object e)
    13. {
    14. elementData[size++] = e;
    15. return true;
    16. }
    17. Object elementData(int index)
    18. {
    19. return elementData[index];
    20. }
    21. private Object elementData[];
    22. private int size;
    23. }

    类型变量 <E> 消失了,取而代之的是 Object !

    “既然如此,那如果泛型类使用了限定符 extends,结果会怎么样呢?”三妹这个问题问的很巧妙。

    来看这段代码。

    反编译后的结果如下。

    1. // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
    2. // Jad home page: http://www.kpdus.com/jad.html
    3. // Decompiler options: packimports(3)
    4. // Source File Name: Arraylist2.java
    5. package com.cmower.java_demo.fanxing;
    6. // Referenced classes of package com.cmower.java_demo.fanxing:
    7. // Wanger
    8. class Arraylist2
    9. {
    10. public Arraylist2(int initialCapacity)
    11. {
    12. size = 0;
    13. elementData = new Object[initialCapacity];
    14. }
    15. public boolean add(Wanger e)
    16. {
    17. elementData[size++] = e;
    18. return true;
    19. }
    20. Wanger elementData(int index)
    21. {
    22. return (Wanger)elementData[index];
    23. }
    24. private Object elementData[];
    25. private int size;
    26. }

    “你看,类型变量 <E extends Wanger> 不见了,E 被替换成了 Wanger”,我说,“通过以上两个例子说明,Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 Object)”

    “二哥,类型擦除会有什么问题吗?”三妹又问了一个很有水平的问题。

    “三妹啊,你还别说,类型擦除真的会有一些问题。”我说,“来看一下这段代码。”

    1. public class Cmower {
    2. public static void method(Arraylist<String> list) {
    3. System.out.println("Arraylist<String> list");
    4. }
    5. public static void method(Arraylist<Date> list) {
    6. System.out.println("Arraylist<Date> list");
    7. }
    8. }

    在浅层的意识上,我们会想当然地认为 Arraylist<String> listArraylist<Date> list 是两种不同的类型,因为 String 和 Date 是不同的类。

    但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”):

    大致的意思就是,这两个方法的参数类型在擦除后是相同的。

    有句俗话叫做:“百闻不如一见”,但即使见到了也未必为真——泛型的擦除问题就可以很好地佐证这个观点。

    “哦,明白了。二哥,听说泛型还有通配符?”

    “三妹啊,哥突然觉得你很适合作一枚可爱的程序媛啊!你这预习的功课做得可真到家啊,连通配符都知道!”

    通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 extends 限定子类,也可以使用关键字 super 限定父类。

    我们来看下面这段代码。

    1. class Arraylist<E> {
    2. private int size = 0;
    3. public Arraylist(int initialCapacity) {
    4. this.elementData = new Object[initialCapacity];
    5. }
    6. public boolean add(E e) {
    7. elementData[size++] = e;
    8. return true;
    9. }
    10. public E get(int index) {
    11. return (E) elementData[index];
    12. }
    13. public int indexOf(Object o) {
    14. if (o == null) {
    15. for (int i = 0; i < size; i++)
    16. if (elementData[i]==null)
    17. return i;
    18. } else {
    19. for (int i = 0; i < size; i++)
    20. if (o.equals(elementData[i]))
    21. return i;
    22. }
    23. return -1;
    24. }
    25. public boolean contains(Object o) {
    26. return indexOf(o) >= 0;
    27. }
    28. public String toString() {
    29. StringBuilder sb = new StringBuilder();
    30. for (Object o : elementData) {
    31. if (o != null) {
    32. E e = (E)o;
    33. sb.append(e.toString());
    34. sb.append(',').append(' ');
    35. }
    36. }
    37. return sb.toString();
    38. }
    39. public int size() {
    40. return size;
    41. }
    42. public E set(int index, E element) {
    43. E oldValue = (E) elementData[index];
    44. elementData[index] = element;
    45. return oldValue;
    46. }
    47. }

    1)新增 indexOf(Object o) 方法,判断元素在 Arraylist 中的位置。注意参数为 Object 而不是泛型 E

    2)新增 contains(Object o) 方法,判断元素是否在 Arraylist 中。注意参数为 Object 而不是泛型 E

    3)新增 toString() 方法,方便对 Arraylist 进行打印。

    4)新增 set(int index, E element) 方法,方便对 Arraylist 元素的更改。

    因为泛型擦除的原因,Arraylist<Wanger> list = new Arraylist<Wangxiaoer>(); 这样的语句是无法通过编译的,尽管 Wangxiaoer 是 Wanger 的子类。但如果我们确实需要这种 “向上转型” 的关系,该怎么办呢?这时候就需要通配符来发挥作用了。

    利用 <? extends Wanger> 形式的通配符,可以实现泛型的向上转型,来看例子。

    1. Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
    2. list2.add(null);
    3. // list2.add(new Wanger());
    4. // list2.add(new Wangxiaoer());
    5. Wanger w2 = list2.get(0);
    6. // Wangxiaoer w3 = list2.get(1);

    list2 的类型是 Arraylist<? extends Wanger>,翻译一下就是,list2 是一个 Arraylist,其类型是 Wanger 及其子类。

    注意,“关键”来了!list2 并不允许通过 add(E e) 方法向其添加 Wanger 或者 Wangxiaoer 的对象,唯一例外的是 null

    “那就奇了怪了,既然不让存放元素,那要 Arraylist<? extends Wanger> 这样的 list2 有什么用呢?”三妹好奇地问。

    虽然不能通过 add(E e) 方法往 list2 中添加元素,但可以给它赋值。

    1. Arraylist<Wanger> list = new Arraylist<>(4);
    2. Wanger wanger = new Wanger();
    3. list.add(wanger);
    4. Wangxiaoer wangxiaoer = new Wangxiaoer();
    5. list.add(wangxiaoer);
    6. Arraylist<? extends Wanger> list2 = list;
    7. Wanger w2 = list2.get(1);
    8. System.out.println(w2);
    9. System.out.println(list2.indexOf(wanger));
    10. System.out.println(list2.contains(new Wangxiaoer()));

    Arraylist<? extends Wanger> list2 = list; 语句把 list 的值赋予了 list2,此时 list2 == list。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 get()indexOf()contains()。想一想,如果可以向 list2 添加元素的话,这 3 个方法反而变得不太安全,它们的值可能就会变。

    利用 <? super Wanger> 形式的通配符,可以向 Arraylist 中存入父类是 的元素,来看例子。

    需要注意的是,无法从 Arraylist<? super Wanger> 这样类型的 list3 中取出数据。

    “三妹,关于泛型,这里还有一篇很不错的文章,你等会去看一下。”我说。

    “好的,二哥。”