java 泛型
java 

在java中使用泛型,可以避免使用Object或者强制类型转换。泛型最适用于集合类,比如List。 使用泛型编写代码增加了其可复用性,可以被许多不同类型的对象使用。举例来说,不想对StringFile的集合分别编程,可以使用ArrayList来处理各种类型的集合。

使用泛型编程有三种技术层次:

  1. 只知道如何使用泛型,而不知道它们为什么可以这么使用;
  2. 当在使用泛型的过程中,遇到一些不能解决的问题,就需要了解泛型的具体使用法则;
  3. 可以自己实现泛型和其方法。

只有那些涉及到很多类型的通用代码,才适合用泛型来处理

定义泛型类

泛型类是含有一个或多个类型变量(如下例中T)的类。

public class Pair<T> {
    private T first;
    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst () {return this.first;}
    public T getSecond () {return this.second;}
    public void setFirst(T newValue) { first = newValue; }
    public void setSecond(T newValue) { second = newValue; }
}

当然了,我们可以定义多个类型变量,像 Pair<T,U>

定义泛型方法

可以在方法上中使用类型参数,该方法即可以在泛型类中定义,也可以在普通类中定义。其中T是在修饰符(public static)后,在返回类型前。

class ArrayAlg {
    public static <T> T getMiddle(T... a) {
        return a[a.length/2];
    }
}

下面的方式避免了返回结果是 Object

public static <T> T convertXmlStrToObject(Class<T> clazz, String xmlStr) {
    // do something
}

该方法可以用如下方式调用:

String middle = ArrayAlg.<String>getMiddle("1", "2", "4");
// 因为编译器可以根据参数类型推断出 T 的类型,所以类型参数可以省略
String middle2 = ArrayAlg.getMiddle("1", "2", "4");

像上述调用,依靠编译器推断类型时,尽量使用同一类型。 像Number middle = GenericMethod.getMiddle(3.14, 1729, 0); 这种调用,编译器会将参数自动装箱成(Double, Integer, Integer), 然后找到这些类型所共有的父类——NumberComparable。 所以可以将结果赋给 Number 或者 Comparable. 否则会报错:

Error:(10, 56) java: 不兼容的类型: 推断类型不符合上限 推断: java.lang.Number&java.lang.Comparable> 上限: java.lang.Double,java.lang.Object


## 类型变量的界限
当我们想要寻找数组中最小值时,就需要泛型变量是继承了`Comparable`的,这时候就可以使用如下方法对类型变量进行类型限制。
```java
public static <T extends Comparable> T min (T[] a) {
  if (a == null || a.length == 0) return null;
  T smallest = a[0];
  for (int i=1; i<a.length; i++) {
      if (smallest.compareTo(a[i]) > 0) smallest = a[i];
  }
  return smallest;
}

虽然extends一般用在类上,而不是接口。但是类型变量界限使用的是关键字extends,只是因为它更适合表达子类型概念,这样就不用新增一个关键字来表示类型变量界限。 当类型变量有多个界定,用&分割: T extends Comparable & Serializable。 类似java中的继承,可以有任意多个接口限定,但是只能有一个类限定。如果有类限定,那么这个类限定的位置必须是界限列表的第一个。

泛型代码和java虚拟机

在java虚拟机中并没有泛型类型,所有的对象都是普通的类。编译器会对泛型类或方法进行类型擦除。

类型擦除

当定义一个泛型后,对应的原始类型会被虚拟机自动创建。原始类型的类名同其对应的泛型的类型是一致的,只是将类型参数移除,并且用它们的限定类型代替类型参数。

public class Interval<T extends Comparable & Serializable, U> implements Serializable {
    private T lower;
    private T upper;
    private U other;
    public Interval(T first, U second) {
        if (first.compareTo(second) <= 0) { lower = first; upper = second; }
        else { lower = second; upper = first; }
    }
}

上列中,类型擦除后,T 使用其第一个限定界限Comparable代替;U因为没有限定,所以用Object代替。类型擦除之后的代码如下:

public class Interval implements Serializable {
    private Comparable lower;
    private Comparable upper;
    private Object other;
    public Interval(Comparable first, Obejct second) {
        // do something
    }
}

泛型表达式的擦除TY

当调用一个泛型方法时,编译器会在返回类型擦除之后,插入类型转换的代码。

Pair<Employee> pairs = new Pair<>();
Employee pair = pairs.getFirst();

getFirst()方法返回的类型经过类型擦除之后是 Object,编译器自动插入了Employee的类型转换: Employee pair = (Employee)pairs.getFirst(). 当然了,对于直接访问泛型属性,编译器的处理也是相似的。

泛型方法的擦除

泛型方法的擦除方式同泛型类类似。 public static <T extends Comparable> T min (T[] a)方法擦除后是 public static Comparable T min(Comparable[] a). 但是,当我们在继承泛型类时,只是擦除类型,泛型方法会有问题。例如下面的例子

class DateInterval extends Pair<LocalDate> {
    public void setSecond(LocalDate second) {
        // do something
    }
}

上述类执行类型擦除之后,如下:

class DateInterval extends Pair { // after erasure 
    public void setSecond(LocalDate second) {
        // do something
    }
}

如果泛型类Pair<LocalDate>中,恰好有一个方法,类型擦除后是 public void setSecond(Object second)。显然,这两个方法因为参数签名不一致,是两个不同的方法。但是,这两个方式不应该不是一个方法。

Pair<LocalDate> pair = new DateInterval(. . .);; // OK--assignment to superclass
pair.setSecond(aDate);

上述方法中,我们想要调用的是DateInterval中的setSecond(LocalDate second)。但是,类型擦除后,调用的应该会是setSecond(Object second)。为了修正这个问题,编译器会为DateInterval生成一个桥接(bridge method)方法: public void setSecond(Object second) { setSecond((Date) second); }

概括来说,Java中泛型的类型擦除会遵循以下原则:

  • 虚拟机中并没有泛型类,只有普通类
  • 所有的类型参数都会被其界限替代
  • 合成桥接方法以保持多态
  • 需要的时候,会插入类型转换

使用泛型的限制

大多数的限制是类型擦除导致的。

类型参数不能是原始类型

不能将原始类型作为参数类型,只能有Pair<Double>,而不能用Pair<double>。因为类型擦除之后,Pair类是Object类型的属性,不能用来存储double的值。

运行时类型查询仅适用于原始类型

类型查询仅适用于原始类型,如a instanceof Pair<String>是错误的,只能判断a是否是Pair的实例,而不能判断a是否是Pair<String>类型的。

Pair<String> stringPair = ...;
Pair<Empolyee> employeePair = ...;
stringPair.getClass() == employeePair.getClass() // true, 他们是相等的,getClass都会返回 Pair.class

不能创建泛型类型的数组

不能实例化泛型类型的数组,Pair<String>[] table = new Pair<String>[10]; 是错误的。 当上述代码执行类型擦除之后,table的类型是Pair[],我们可以将它转换成Object[]。 但是,数组会记住其元素的类型,如果存储一个错误类型的元素,会抛出一个ArraySotreException异常。 如objectArr[0]="hello"; 只有数组的创建是不合法的,我们还是可以声明Pair<String>[] 类型的数组,但是不能用new Pair<String>[10]来将其初始化。

可以声明通配符类型数组,然后将其转换成对应的类型

Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];

但是这种方式是不安全的,如果想调用Pair中的方法,会抛出ClassCastException. 如果想用泛型的数组,可以使用ArrayList,它是安全并且有效的。

可使用可变参方法

Java不支持泛型数组的初始化,但是可以使用可变参数作为方法的参数。代码可以正常运行,但是会有警告。

@SafeVarargs
public static <T> void addAll(T... ts) {
    for (t: ts) {
        this.add(t);
    }
}

不能实例化泛型变量

不能用new T()方法创建实例。这是因为类型擦除会将T变成Object。显然new Object()并不是我们想要的结果。 我们可以通过其他的方式来创建泛型实例:

public static <T> Pair<T> makePair(Class<T> cl){
    try { return new Pair<>(cl.newInstance(), cl.newInstance()); }
    catch (Exception ex) { return null; }
}
// 调用方式如下:
Pair<String> p = Pair.makePair(String.class);

泛型类的静态变量或方法中不能使用类型变量

我们不能在泛型类中的静态变量或者方法使用类型变量,下面的方式是错误的:

public class Singleton<T> {
    private static T singleInstance; // Error
    public static T getSingleInstance() { // Error
  
    if (singleInstance == null) construct new instance of T
    return singleInstance;
    }
}

如果这样的方式是可以的,那么我们可以定义一个Singleton<Random>来共享一个随机数生成器,Singleton<File>来共享一个文件处理器。但这肯定是不可能的,类型擦除之后,只有一个Singleton类和一个singleInstance属性。

不能抛出或者捕获泛型类的实例

定义一个继承Throwable的泛型类是不合法的:

public class GenericException<T> extends Exception {} // Error

但是我们可以用在异常处理中使用泛型:

public static <T extends Throwable> void doWork(T t) throws T  {// OK
    try {
        // do work
    } catch (Throwable realCause) {
        t.initCause(realCause);
        throw t;
    }
}

注意类型擦除后方法相同签名

泛型类型被擦除之后,可能会导致方法签名一直。如果Pair类中添加eqauls方法:

public class Pair<T> {
    public boolean equals(T value) {
        return first.equals(value) && second.equals(value);
    }
    ...
}

Pair<T>类型擦除之后,其equals方法变为 boolean equals(Object o)Object中的equals方法一直。 解决方法是命名成其他名字。 为了防止擦除带来的方法冲突,我们强加了一个限制。即类或类型变量不能同时是同一个接口的不同参数化的两种子类型。

class Employee implements Comparable<Employee> { . . . }
class Manager extends Employee implements Comparable<Manager>{ . . . } // Error

上例子中,Manager 需要实现Comparable<Manager>Comparable<Employee>,这两个接口就是同一接口的两个不同参数化的接口。 虽然类型擦除之后,看起来是合法的:

class Employee implements Comparable { . . . }
class Manager extends Employee implements Comparable { . . . }

但是桥接方法是冲突的,实现Comparable<X>接口的类,会有一个桥接方法:

public int comparTo(Object other) {return compareTo((X) other);}

泛型类中的继承规则

在使用泛型类型的时候,需要了解一些继承的规则。如果有两个类: EmployeeManagerManagerEmployee子类。Pair<Manager>也是Pair<Employee>的子类么? 实际上,在Pair<S>Pair<T>之间没有任何的关系,不管ST是什么关系。

通配符类型

通配符类型是指可以有不同的类型参数。通配符类型使用方式为Pair<? extends Employee>,它表示类型参数是Employee的子类的任何通用Pair类型,比如: Pair<Manager>。 我们可以把Pair<Manager>看做是Pair<? extends Employee> 的子类。因此,我们可以把Pair<Manager>的实例传入下面的方法中:

public static void print(Pair<? extends Employee> p) {
    // do something
}

通配符类型的上界限

通配符的界限同类型变量的界限相似,不过通配符界限可以指定一个下限: ? super Manager。该通配符表示所有Manager的父类。 含有父类界限的通配符与上述的通配符类型正好相反。我们可以为方法提供参数,但是不能使用其返回值。比如Pair<? super Manager>的方法可以表述如下:

void setFirst(? super Manager);
? super Manager getFirst();

编译器不能知道setFirst参数准确的类型,因此不能接受具有EmployeeObject类型的参数调用。该方法只能传递Manager或者其子类型的对象。对于调用getFirst将会返回一个Object对象,因为我们不知道它的返回类型是什么。

public static void minmaxBonus(Manager [] a, Pair<? super Manager> result) {
    if (a.length == 0) return ;
    Manager min = a[0];
    Manager max = a[0];
    for (int i=1; i<a.length; i++) {
        if (min.getBonus() > a[i].getBonus()) min = a[i];
        if (max.getBonus() < a[i].getBonus()) max = a[i];
    }
    result.setFirst(min);
    result.setSecond(max);
}

其类图如下所示:

一般来说,父类界定的通配符类型可以set对象,而子类型界定的通配符类型可以让你get对象

Comparable接口本身就是一个泛型:

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

使用T extends Comparable方式,经过类型擦除后是:

public interface Comparable {
    public int compareTo(Object other);
}

所以,对于ArrayAlg类中的min方法,我们可以将其定义为:

public static <T extends Comparable<T>> T min(T[] a)

这样比T extends Comparable更为全面,而且对于大多数类来说都能很好的工作。但是对于LocalDate对象来说,并不适用。LocalDate实现了ChronoLocalDate接口,ChronoLocalDate接口继承了Comparable<ChronoLocalDate>,因此LocalDate实现了Comparable<ChronoLocalDate>而不是Comparable<LocalDate>。 在这种情况下,使用下面的方法更适用:

public static <T extends Comparable<? super T>> T min(T[] a) {}

这样经过类型擦除之后:

public int compareTo(LocalDate other);

虽然这样看起来很繁琐,但是这样声明能够消除调用方法时对参数的不必要限制。

无界限通配符

无界限通配符,如Pair<?>。无界限通配符与原始类型Pair一样。但是,这两个类型是不同的,Pair<?>的方法如下:

? getFirst();
void setFirst(?);

getFirst方法的返回值只能是Object,而setFirst方法不能被调用,即使是Object作为参数也不行,当然null是可以的。这是Pair<?>与原始的Pair类型的不同。

无界限通配符一般会用来做非常简单的操作,例如,判断值是不是null:

public static boolean hasNulls(Pair<?> p) {
    return p.getFirst() == null || p.getSecond() == null;
}

通配符捕获

我们有一个交换Pair元素的方法:

public static void swap(Pair<?> p)

通配符不是一个类型变量,我们不能用?作为一个类型,也就是下面的代码是不合法的:

? t = p.getFirst(); // Error
p.setFirst(p.getSecond());
p.setSecond(t);

我们在交换元素时,必须要将其中一个元素保存成临时变量。为了解决这个问题,我们可以编写一个辅助方法swapHelper:

public static <T> void swapHelper (Pair<T> p) {
    T t = p.getFirst();
    p.setFirst(p.getSecond());
    p.setSecond(t);
}

swapHelper是一个泛型方法,而swap不是,现在我们可以在swap中调用swapHelper:

public static void swap(Pair<?> p) { swapHelper(p);}

在上述方法中,swapHelper中的T捕获了swap中的通配符类型。编译器不知道通配符是什么类型,但它是一种确定的类型,当T表示该类型时, swapHelper的定义是完美的。 当然,在这个例子中,我们没有必要使用Pair<?>,可以直接定义<T> void swapHelper。但是,如果某个方法中像如下定义:

public static void maxmin(Manager[] a, Pair<? super Manager> result) {
    PairAlg.swap(result); // swapHepler 捕获了通配符类型
} 

下边的例子给了前边所描述的内容:


public class GenericMethod {
    public static void main(String[] args) {
        Manager ceo = new Manager("Gus Greedy", 800000, 2003, 12, 15);
        Manager cfo = new Manager("Sid Sneaky", 600000, 2003, 12, 15);
        Pair<Manager> buddies = new Pair<>(ceo, cfo);
        printBuddies(buddies);

        ceo.setBonus(1000000);
        cfo.setBonus(500000);
        Manager[] managers = {ceo, cfo};

        Pair<Employee> result = new Pair<>();
        minmaxBonus(managers, result);
        System.out.println("first: " + result.getFirst().getName()
            + ", second: " + result.getSecond().getName());
        maxminBonus(managers, result);
        System.out.println("first: " + result.getFirst().getName()
            + ", second: " + result.getSecond().getName());
    }

    public static void printBuddies(Pair<? extends Employee> p) {
        Employee first = p.getFirst();
        Employee second = p.getSecond();
        System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
    }

    public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
        if (a.length == 0) return;
        Manager min = a[0];
        Manager max = a[0];
        (Continues)
            .8 Wildcard Types 449
        Listing 8.3 (Continued)
        for (int i = 1; i < a.length; i++) {
            if (min.getBonus() > a[i].getBonus()) min = a[i];
            if (max.getBonus() < a[i].getBonus()) max = a[i];
        }
        result.setFirst(min);
        result.setSecond(max);
    }

    public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
        minmaxBonus(a, result);
        PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type
    }
}

class PairAlg {
    public static boolean hasNulls(Pair<?> p) {
        return p.getFirst() == null || p.getSecond() == null;
    }

    public static void swap(Pair<?> p) {
        swapHelper(p);
    }

    public static <T> void swapHelper(Pair<T> p) {
        T t = p.getFirst();
        p.setFirst(p.getSecond());
        p.setSecond(t);
    }
}