泛型#

背景知识#

泛型(generics)是 JDK 5 中引入的一个新特性,允许在定义类和接口的时候使用类型参数。

正常情况下,我们经常会给方法传递参数,那么能不能给类或接口传递参数呢?因此,诞生了泛型。

泛型的诞生,很大一部分应用都是用在容器上了。

我们知道,一个 List 容器可以放入不同类型的元素,如果没有泛型,那么当我们想要存放 String 元素时, 就需要定义一个可以接受 String 的 List 接口,后面想放 Interger 时,又要定义另一个接口。

而泛型的出现,最大的好处就是可以提高代码的复用性。 我们使用 JDK 为我们提供的泛型接口,可以方便地存放各种不同类型的元素到容器中,而不用自己实现。

使用泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。

通配符#

通配符是文本值中代替未知字符的特殊字符,可方便使用类似但不相同的数据查找多个项目。

在计算机软件技术中,通配符可用于代替单个或多个字符。

通常地,星号 * 匹配 0 个或以上的字符,问号 ? 匹配 1 个字符。

在 Java 泛型中,常用的通配符和含义如下(没有星号):

  • E :Element,在集合中使用,因为集合中存放的是元素

  • T :Type,Java 类

  • K :Key,键

  • V :Value,值

  • N :Number,数值类型

  • ? :表示不确定的 Java 类型

限定通配符 对类型进⾏限制, 泛型中有两种限定通配符:(尖括号部分只是代表一个具体类型)

  • 上界通配符,格式为: <? extends T> 是指泛型中的类必须为当前类的子类或当前类;

  • 下界通配符,格式为: <? super T> 是指泛型中的类必须为当前类或者其父类。

泛型类型必须⽤限定内的类型来进⾏初始化,否则会导致编译错误。

在使用泛型时,存取元素时用 super,获取元素时,用 extends

频繁往外读取内容的,适合用上界 Extends。经常往里插入的,适合用下界 Super

⾮限定通配符 表⽰可以⽤任意泛型类型来替代,类型为 <T>

简单泛型#

在这个案例中,我们用 Holder3 持有 Automobile 对象。代码实现如下:

//: generics/Holder3.java

class Automobile {}

public class Holder3<T> {
    private T a;

    public Holder3(T a) {
        this.a = a;
    }

    public void set(T a) {
        this.a = a;
    }

    public T get() {
        return a;
    }

    public static void main(String[] args) {
        Holder3<Automobile> h3 = new Holder3<Automobile>(new Automobile());
        Automobile a = h3.get(); // No cast needed
        // h3.set("Not an Automobile"); // Error
        // h3.set(1); // Error
    }
} ///:~

使用了泛型之后,我们得到了一个类型安全的容器,在取出容器的内容时,编译器自动为我们完成了转型。 因为我们给容器说,只能接收 Automobile 类型的对象,因此,在传入 StringInterger 对象时都报错了。

使用泛型的容器除了能够接收单一类型的对象外,也可以设计为能够接收多个类型的对象,比如下面代码所示:

//: net/mindview/util/TwoTuple.java
package net.mindview.util;

public class TwoTuple<A,B> {
    public final A first;
    public final B second;

    public TwoTuple(A a, B b) {
        first = a;
        second = b;
    }

    public String toString() {
        return "(" + first + ", " + second + ")";
    }
} ///:~

这种成对的对象,我们可以称之为 元组。往容器中塞对象时,需要一次一个元组。

上面的代码段是一次塞两个对象,而且我们可以使用继承机制实现更长的元组。

//: net/mindview/util/ThreeTuple.java
package net.mindview.util;

public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
    public final C third;

    public ThreeTuple(A a, B b, C c) {
        super(a, b);
        third = c;
    }

    public String toString() {
        return "(" + first + ", " + second + ", " + third +")";
    }
} ///:~

为了 使用元组,你只需要定义一个长度合适的元组,将其作为方法的返回值,然后再 return 语句中创建该元组,并返回即可。

//: generics/TupleTest.java
import net.mindview.util.*;

public class TupleTest {
    static TwoTuple<String,Integer> f() {
        // Autoboxing converts the int to Integer:
        return new TwoTuple<String,Integer>("hi", 47);
    }

    public static void main(String[] args) {
        TwoTuple<String,Integer> ttsi = f();
        System.out.println(ttsi);
        // ttsi.first = "there"; // Compile error: final
    }
} /* Output:
(hi, 47)
*///:~

泛型接口#

将泛型应用于接口,我们希望实现该接口的类,能够返回给我们满足一些符合我们预期的类型信息。

作为案例,我们希望创建一个生成器,它可以给我们生成(返回)Fibonacci 数列中的下一个值。

一般而言,一个生成器只定义一个方法,该方法用以产生新对象。在这里,就是用 next() 方法。

//: net/mindview/util/Generator.java
// A generic interface.
package net.mindview.util;

public interface Generator<T> {
    T next();
} ///:~

比如我们可以实现 Generator 类,用以生成 Fibonacci 数列。

//: generics/Fibonacci.java
// Generate a Fibonacci sequence.
import net.mindview.util.*;

public class Fibonacci implements Generator<Integer> {
    private int count = 0;

    public Integer next() {
        return fib(count++);
    }

    private int fib(int n) {
        if(n < 2)
            return 1;
        return fib(n-2) + fib(n-1);
    }

    public static void main(String[] args) {
        Fibonacci gen = new Fibonacci();
        for(int i = 0; i < 18; i++)
            System.out.print(gen.next() + " ");
    }
} /* Output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
*///:~

虽然,在 Fibonacci 类使用的都是 int 类型,但是其类型参数却是 Integer

这个例子引出了 Java 泛型的一个局限性:基本类型无法作为类型参数

不过,Java SE5 具备自动打包和拆包的功能,可以很方便地在基本类型和其相应的包装器类型之间进行转换。

泛型方法#

提示

如果使用泛型方法可以取代整个类泛型化,那么就应该只使用泛型方法。

定义泛型方法,只需将泛型参数列表置于返回值之前。

//: generics/GenericMethods.java

public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
    }
} /* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*///:~

可变参数与泛型方法:

//: generics/GenericVarargs.java
import java.util.*;

public class GenericVarargs {
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<T>();
        for(T item : args)
            result.add(item);
        return result;
    }
    public static void main(String[] args) {
        List<String> ls = makeList("A");
        System.out.println(ls);
        ls = makeList("A", "B", "C");
        System.out.println(ls);
        ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
        System.out.println(ls);
    }
} /* Output:
[A]
[A, B, C]
[, A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
*///:~

当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型。 因为编译器会为我们找出具体的类型。这称为 类型参数推断(type argument inference)

因此, 我们可以像调用普通方法一样调用 f(),就好像 f() 被无限次地重载过。 甚至,它可以接受 GenericMethods 作为其参数类型。

类型推断只对赋值操作有效,其他时候并不起作用。 如果你将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器不会执行推断。 在这种情况下,编译器认为,调用泛型方法后,其返回值被赋给一个 Object 类型的变量。 比较下面两个程序段:

程序段一:等号赋值,可以推断

//: generics/SimplerPets.java
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;

public class SimplerPets {
    public static void main(String[] args) {
        Map<Person, List<? extends Pet>> petPeople = New.map(); // 等号赋值给一个泛型容器
        // Rest of the code is the same...
    }
} ///:~

程序段二:参数引用传递,不能推断

//: generics/LimitsOfInference.java
import typeinfo.pets.*;
import java.util.*;

public class LimitsOfInference {
    static void f(Map<Person, List<? extends Pet>> petPeople) {}
    public static void main(String[] args) {
        // f(New.map()); // Does not compile  // 函数返回值作为参数赋值给泛型容器
    }
} ///:~

类型擦除#

类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。 编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。 具象化一些就是,我们可以声明 ArrayList.class 但是不能声明 ArrayList<Integer>.class 就是因为擦除。擦除会移除参数类型信息。 List<String>List<Integer> 在运行时事实上是相同的类型,即 List

类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。

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

List<T extends HasF> 为例,类型擦除的主要过程如下:

  1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。这是说,泛型参数 <T extends HasF> 经过擦除后,变成了 HasF。若 <T> 未指定边界,将被擦除为 Object

  2. 移除所有的类型参数。经过这一步变换, List<T extends HasF> 就变成了额 List

擦除带来的问题#

丢失类型信息#

擦除最直接的影响就是丢失了一些类型信息。

擦除直观上的理解就是发生了向上转型,它丢失了泛型代码中执行某些操作的能力。 任何在运行时需要知道确切类型信息的操作都将无法工作。比如,下面的代码段无法进行编译:

//: generics/Erased.java
// {CompileTimeError} (Won't compile)

public class Erased<T> {
    private final int SIZE = 100;

    public static void f(Object arg) {
        if(arg instanceof T) {}           // Error
        T var = new T();                  // Error
        T[] array = new T[SIZE];          // Error
        T[] array = (T)new Object[SIZE];  // Unchecked warning
    }
} ///:~

泛型与重载#

当类型擦除遇到重载时,也会遇到一些问题,如下代码,它将无法通过编译:

import java.util.List;

public class TypeErasue {
    public static void method(List<String> list) {
        System.out.println("invoke method(List<String> list)");
    }
    public static void method(List<Integer> list) {
        System.out.println("invoke method(List<Integer> list)");
    }
}

因为前面讲过, List<Integer>List<String> 编译后都被擦除了,变成了一样的原生类型 List。 擦除动作导致这两个方法的特征签名变得一模一样。

泛型与 catch#

如果我们自定义了一个泛型异常类 GenericException,那么不要尝试用多个 catch 取匹配不同的异常类型。 例如你想要分别捕获 GenericExceptionGenericException,这也是有问题的。

泛型内包含静态变量#

先阅读一下下面的代码段,你认为结果是多少?

class MyStatic<T> {
    public static int var = 0; // 泛型内的静态变量
}

public class TypeErasue {
    public static void main(String[] args) {
        MyStatic<Integer> myStatic1 = new MyStatic<Integer>();
        myStatic1.var = 1;
        MyStatic<String> myStatic2 = new MyStatic<String>();
        myStatic2.var = 2;
        System.out.println(myStatic1.var);
    }
}

答案是 2。经过类型擦除,所有的泛型类实例都关联到同一份字节码上,因此,泛型类的所有静态变量是共享的

List 和 List<Object>#

区别一:

原始类型 List 和带参数类型 List<Object> 之间的主要区别是: 在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。

通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如 StringInteger

区别二:

你可以把任何带参数的类型传递给原始类型 List,但却不能把 List<String> 传递给接受 List<Object> 的方法,因为会产生编译错误。

List<?> 和 List<Object>#

List<?> 是一个未知类型的 List,而 List<Object> 其实是任意类型的 List。你可以把 List<String>List<Integer> 赋值给 List<?>,却不能把 List<String> 赋值给 List<Object>