XiaoShang66’s Blog

[TOC]

我们之前已经用过泛型类和泛型接口: ArrayListComparable

泛型可以参数化类型,这个能力使我们可以定义带泛型类型的类或方法,随后编译器会用具体的类型替换它.

使用泛型的主要优点是能够在编译时而不是运行时检测出错误.泛型类或方法允许用户指定可以和这些类或方法一起工作的对象类型,如果试图使用一个不相容的对象,编译器就会检测出这个错误.

动机和优点

  • 在JDK1.5之后,java允许定义泛型类,泛型接口和泛型方法

    比如Comparable接口在JDK1.5之前,不是泛型接口,让我们对比JDK1.5前后的Comparable接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    before JDK1.5
    java.lang

    public interface {
    public int compareTo(Object o)
    }

    after JDk1.5
    java.lang

    public interface Comparable<T> {
    public int compareTo(T o)
    }

    其中<T>表示形式泛型类型,随后可以用一个实际具体类型来替换它.替换泛型类型被称为泛型实例化,按照惯例,像T,E这种单个大写字母用于表示形式泛型类型.

  • 是否是泛型类对程序的影响:

    1
    2
    Comparable c = new Date();
    System.out.println(c.compareTo("red"));

    在JDK1.5之前,Comparable不是泛型接口,上面的代码可以正常编译,但是在运行的时候会报错.因为没有机制在编译的时候来检测两种类型是否匹配.在JDK1.5之后,Comparable被定义为泛型接口,在编译的时候编译器就会比较两者类型,具体可以看最上面的代码,两个泛型T不一样,那么编译时就会报错.

    在编译时报错的程序显然要比运行时报错的程序更加可靠,而且会更具有普适性,因为在运行时引入的参数可能正好不符合corner case,正好跑成功了,但是其实有一些corner case由于没有考虑上而凉凉,而编译时报错代表程序本身就有纰漏,也就是带入普适的情况仍然有问题.

    上述就是使用泛型的基本好处,当类型不匹配时在编译时就会报错.

  • 对于一些方法也是从JDK1.5开始变成泛型方法,与之前有所改变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    java.util.ArrayList

    +ArrayList(o: E): void

    +add(o: E): void

    +add(index: int, o: E): void

    +clear(): void

    +contains(o: Object): int

    +get(index: int): E

    +indexOf(o: Object): int

    +isEmpty(): boolean

    +lastIndexOf(o: Object): int

    +remove(o: Object): boolean

    +size(): int

    +remove(index: int): boolean

    +set(index: int, o: E): E

    在JDK1.5之前,上面的o: E全部都是o: Object

    那么创建一个ArrayList后:

    ArrayList<T> list = new ArrayList();

    要添加别的类型的东西的时候,编译时就会报错

    list.add(E);

  • 泛型类型必须是引用类型,即Object,String之类的,不能是基本类型

    即类似ArrayList<int> list = new ArrayList();是错误的

    为了给int值创建一个ArrayList,必须用:

    ArrayList<Integer> list = new ArrayList();

    这样就能往里面加int

定义泛型类和接口

  • 可以为类或者接口定义泛型,当使用类来创建对象,或者使用类或接口来声明引用变量时,必须制定具体的类型

  • 实例: 我们修改11.13中的栈类:

    原来的代码:

    修改后的代码:

    1
    2
    GenericStack<E>
    -list: java.util.ArrayList<E>

    就是创建ArrayList的时候<>中不是object而是<E>

  • 可以不使用泛型,而将元素类型设置为Object,也可以容纳任何对象类型,但是使用泛型能够提高软件的可靠性和可读性,因为某些错误能在编译时而不是运行时被检测到.

  • 为了创建一个堆栈,可以使用new GenericStack<String>new GenericStack<>()来创建

    但是不要想当然的认为GenericStack的构造方法是public GenericStack<E>().事实上和非泛型类是一样的定义: public GenericStack()

  • 有时候泛型类可能含有多个参数: 这个时候可以<E1. E2, E3>

  • 可以定义一个类或接口作为泛型类或者泛型接口的子类型,在java API中,java.lang.String类被定义为实现Comparable接口

    public class String implements Comparable<String>

泛型方法

  • 可以利用泛型类型声明一个泛型方法

  • 声明与调用

    1
    2
    3
    4
    5
    6
    7
    8
    9

    public static <E> void print(E[] list)

    //调用一个泛型方法可以声明具体类型
    ClassName.<Integer>print(integers);
    ClassName.<String>print(strings);

    //调用泛型方法也可以和普通方法一样调用,这种情况下编译器自动发现实际类型.
    Class.print(integers);
  • 可以将泛型类型指定为另外一种类型的子类型,这样的泛型称为受限的:

    1
    public static <E extends GeometricObject> boolean equalArea(E o1, E o2){}

    其实也很好理解,也就是E必须是GeometricObject中的一个Object,所以算是受限了

    非受限的<E>等同于<E extends Object>

  • 定义一个泛型类型,泛型类型放在类名之后,比如GenericStack<E>

    定义一个泛型方法,泛型类型放在方法返回类型之前,例如: <E> void max (E o1, E o2)

原始类型和向后兼容

  • 没有指定具体类型的泛型类和泛型接口被称为原始类型,用于和早期的java版本向后兼容.

  • 可以使用泛型类而无需指定具体类型

    GeneticStack stack = new GenericStack()

    它大体等价于:

    GenericStack<Object> stack = new GenericStack<Object>();

    像上面那样不带类型参数的GenericStackArrayList泛型类称为原始类型(raw type),使用原始类型可以向后兼容java的早期版本.

  • 原始类型是不安全的,要尽量避免使用原始类型.

通配泛型

  • 可以使用非受限通配,受限通配或者下限通配来对一个泛型类型指定范围.

  • 在上面的程序运行时会报错,max(intStack)intStack代表的GenericStack<Integer>并不属于GenericStack<Number>(尽管int属于number,但是在这种创建时就是一个类就是一个类),但是这样真的超级不方便,所以引入通配类型来解决这个问题.

  • 通配类型:

    1. ? 称为非受限通配,它和 ? extends Object 是一样的
    2. ? extends T称为受限通配,它表示T或者T的一个子类型
    3. ? super T称为下限通配,表示T或者T的一个父类型
  • 那么上面的程序利用通配类型将max方法改为:

    public static double max(GenericStack<?> stack)即可解决这些问题.

消除泛型

  • 泛型使用类型消除的方法来实现,即在编译的时候使用泛型类型信息来编译代码,但是随后会消除它.那么,泛型信息在运行时是不可用的.这种方法可以使泛型代码兼容原始的遗留代码
  • 泛型存在于编译时,一旦编译器确认泛型类型是安全使用的,就会将它转换为原始类型.
  • 当编译泛型类,接口和方法时,编译器用Object类型代替泛型类型.
  • 如果一个泛型类型是受限的,编译器就会用该受限类型来替换它.

泛型的限制

关键术语

本章小结

测试题