Contents

JavaCoreOne-C08Generic

泛型类和泛型方法具有 类型参数,泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用. 例如, 在Java引入泛型之前, ArrayList 接受 Object 参数并创建数组, 由于继承的特性, 使得一个 ArrayList 对象可以存放任意类型的对象. 其问题在于, 一是取出元素时需要进行强制转换, 二是无法保证容器内实际的对象类型一致, 这可能导致异常. 因此可以使用泛型指明类型参数(type parameter)

ArrayList<String> strArr = new ArrayList<>();

这使得代码具有更好的可读性, 且解决了上述两个问题. 以上, 实际上你已经了解的泛型的概念和使用语法, 但若要创建自己的泛型类, 则需要更多的知识. 本质主要讨论创建自己泛型类的方法和注意事项 – 这同时也为下一章的泛型集合做好了基础

1. 泛型类与泛型方法

定义一个Pair泛型类, 他具有 first 和 second 字段, 两个字段是不同的类型 T. 有两个构造方法 无参和全参, 以及各字段的getter setter.

// Pair.java
package code.gernic;

public class Pair<T>{
    private T first;
    private T second;
    Pair(){
        first = null;
        second = null;
    }
    Pair(T first, T second){
        this.first = first;
        this.second = second;
    }
    public T getFirst(){return first;}
    public T getSecond(){return second;}
    public void setFirst(T first){this.first = first;}
    public void setSecond(T second){this.second = second;}
}

现在做一个使用测试, 从这一角度看, 泛型类就像是普通类的工厂, 可以指定任何具体类型生成不同的 Pair<> 类型:

public static void test1(){
    Pair<String> pair1 = new Pair<>("小米", "冰箱");
    System.out.println(pair1.getFirst() + ", " + pair1.getSecond());
    // 小米, 冰箱
}

再继续考虑泛型方法, 它在具有类似如下的定义和调用方式:

public class AAA{
    public static <T> void f(T... a){
        // ...
    }
}
// other program
AAA.<String>f("a", "b", "c");
// or we can remove the type explannation
AAA.f("a", "b", "c");

注意此处, 通常来说, 调用时其类型指定可以省略, 编译器可以从传入参数推断出具体类型, 例如 String. 然而也可能出现无法推断的情况, 例如传入的各参数类型不同, 且无法找到合理的公共父类.

假设你要设计一个数组算法工具类, 该类包含一个程序 minmax([]), 它接受一个T类型数组, 找出数组中的最小值first和最大值second, 封装为Pair返回. minmax 方法是一个泛型方法, 它的返回值和参数包含类型参数, 其次这样一种类型还必须满足一定的限制条件, 例如: 可比较性. 元素之间是有大小之分的.

public class ArrayAlg {
    public static <T extends Comparable> Pair<T> minmax(T[] a){
        if(a==null || a.length==0) return null;
        T first=a[0];
        T second=a[0];
        for (int i=1; i<a.length; i++) {
            if(first.compareTo(a[i])>0) first = a[i];
            if(second.compareTo(a[i])<0) second = a[i];
        }
        return new Pair<T>(first, second);
    }
}

接下来进行测试

String[] strings = {"Jenny", "Kitty", "Julier", "Mike", "John"};
Pair<String> pair1 = ArrayAlg.minmax(strings);  // 省略了类型指定
System.out.println(pair1.getFirst() + ", " + pair1.getSecond());
// Jenny, Mike

正常编译运行, 不过会有标黄警告: 也即comparable是一个原生接口, 你需要为其指定类型参数, 关于这一点会在后面解释

2. 类型擦除–限制与局限性

2.1 类型擦除

类型擦除的核心在于: JVM层面并不提供泛型的支持, 也即JVM中没有泛型类型对象, 所有对象都是普通类。因此, 任何时候定义一个泛型类型时, 都会有对应的原始类型(raw type), 也即没有类型参数的类名, 例如Pair. 而参数类型会被擦除, 替换为限定类型或者Object(无限定).例如对于 Pair<T>这一实现, 其原始类型变为:

public class Pair{
    private Object first;
    private Object second;
    Pair(){
        first = null;
        second = null;
    }
    Pair(Object first, Object second){
        this.first = first;
        this.second = second;
    }
    public Object getFirst(){return first;}
    public Object getSecond(){return second;}
    public void setFirst(Object first){this.first = first;}
    public void setSecond(Object second){this.second = second;}
}

又比如另一个泛型类 Interval<T extends Comparable & Serializable>. 则其原始类中的参数T就会被替换为 Comparable

请考虑原始Pair类中的getFirst方法, 该方法在擦除后返回的是Object值, 因此实际执行String a = strPair.getFirst()语句时, 应至少包含两步: 1. 对原始类型方法Pair.getFirst的调用 2. 将返回值Object对象强转为String对象

考虑这样的情况: 类Interval继承了Pair<Integer>, 执行语句其含义是表示一个区间, 因此second要保证比first大. 考虑Pair<Integer> a = new Interval(), 以setSecond()为例, Interval类对其进行了重写:

void setSecond(Integer second)

在进行类型擦除后, 从父类继承了方法:

class Interval extends Pair{
    public void setSecond(Object second){ ... }
}

此时, 就有了多态的性质, 我们期望调用a.setSecond()方法时是子类重写后的方法, 编译器会生成一个桥方法public void setSecond(Object second){ setSecond((Integer) second); }以满足期望。具体过程是: 变量a被声明为Pair<Integer>, 该类包含一个setSecond(Object)方法, 虚拟机在引用a的对象上调用该方法, 而该对象是Interval类型, 进而调用Interval.setSecond(Object)方法, 该方法是桥方法, 它会继续调用setSecond(Integer)方法。 然而如果再考虑getSecond()方法呢, 它们仅仅是返回值类型不同, 实际上, 在虚拟机中, 会有参数类型和返回类型共同指定

小结

  1. JVM中没有泛型类和泛型方法,只有普通类和普通方法
  2. 泛型类或者泛型方法的参数类型会被擦除,使用限定类型或者Object替代
  3. 必要时通过类型强制转换保证类型一致
  4. 编译器会生成桥方法以保持多态

2.2 限制与局限性

  1. 不能使用基本类型而是只能使用引用类型作为类型参数, 例如 Pair<Integer>是合法的而Pair<int>非法。
  2. 运行时类型查询只适用于原始类型。也即你无法去判断一个类是否为某一个泛型,例如你可以去判断一个类是否为Pair类型而无法去判断一个类型是否为Pair<String>或者Pair<Integer>.这是由于类型擦除。
  3. 不能在静态字段或方法中引用类型变量

2.2.1 不能创建参数化类型的数组

例如 var table=new Pair<String> [10] 是非法的。数组必须保证各个元素类型的一致性, 不允许插入不同类型的元素, 而泛型类型擦除后失去了类型参数, 只知道table是一个Pair数组, 由于类型参数的可变性, 这违背数组的数据类型一致性, 因此不允许这么做, 也即这从语法上就是错误的. 然而这样的声明或者创建是合乎语法的, 尽管它并不安全: var table = (Pair<String>) new Pair<?> [10]; 声明调配类型数组, 然后进行强制转换, 但他无法保证数组元素一致性. 结论是, 可以简单地使用ArrayList等集合容器存放参数化类型对象.

2.2.2 不能实例化类型变量

例如在构造器中这样做是错误的:

public Pair(){first=new T(); second=new T();}

这是由于T会被擦除为 Object, 一个最好的解决方法是 提供构造器表达式

public static <T> Pair<T> makePair(Supplier<T> constr){
    // 传入参数是一个构造器表达式, 例如 String::new
    return new Pair<>(constr.get(), constr.get());
}

or 使用传统的方法反射 Constructor.newInstance 方法:

public static <T> Pair<T> makePairReflection(Class<T> cl){
    // 传入参数是一个类型, 例如String.class, 通过反射机制创造该类型的对象
    try{
        return new Pair<>(cl.getConstructor().newInstance(), cl.getConstructor().newInstance());
    }catch (Exception e){
        return null;
    }
}

从另一个角度想, 对于使用了类型参数的函数, 要创建该类型的对象, 就必须先明确这个类型具体是什么类型, 而构造器表达式这一形式就十分简洁明确.

2.2.3 不能构造泛型数组

不同于第一小节, 第一小节是说不能创建参数化类型的数组例如Pair<String>[10], 这里是说在泛型方法设计的过程中不能创建泛型数组, 例如minmax(T[])中假如不借助于Pair类存储min max, 而是使用一个数组返回, 例如new T[2]; // Error, 同样由于类型擦除这里非法, 可以通过传入构造器类型解决

// minmax(IntFunction<T[]> constr, T... a)

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

既不能抛出泛型类型异常也不能捕获泛型类型异常, 泛型类型也不被允许扩展Throwable接口

2.2.5 可以取消检查型异常

Java异常处理机制的原则是必须为所有的检查型异常提供一个处理器

3. 泛型类型的继承规则

如果ManagerEmployee的子类, 那么Pair<Manager>Pair<Employee>的子类吗?直觉上讲似乎不太是, 实际上确实后面两者并没有什么关系。 不过另一方面, 泛型类可以扩展或实现其他的泛型类, 例如一个 ArrayList<Manager> 可以转为 List<Manager>.

4. 通配符?

例如: Pair<? extends Employee>表示任何参数类型为Employee的子类型及本身构成的Pair

通配符形式上有点类似于泛型编程中的类型限定, 但在前者的领域, 他是为了保证类型参数满足一定的限制条件以确保数据安全, 而在前者通配符的观念里, 它是为了解除特定类型的限制, 将可用类型拓宽.

在引入通配符之前, 假设一个函数printBuddies接收Pair<Employee>类型, 它打印了对象的字段内容信息, 但由于Pair<Manager>Pair<Employee>并无关系, 所以printBuddies并不接受Pair<Manager>参数, 然而, 在使用通配符之后, 函数printBuddies(Pair<? extends Employee>)就可以接收Pair<Manager>类型的参数了. 这听起来似乎十分完美, 且合乎情理, 然而实现时有一个细节是: ? extends Employee并非一个具体类型, 不可能创造后者的一个引用, 例如? extends Employee emp = xxxx;, 因此我们只能使用Employee这个引用, 该引用可以指向子类型及本身的对象, 形象地说, 可以用父类的指针去接收(读取)子类型的对象, 进而打印它

super超类型限定

例如: Pair<? super Manager>表示任何参数类型为Manager的超类型及本身的Pair

类比extends通配符, 假设一个函数接受Pair<? super Manager> pairA为参数, 那么实际上函数仅能操作 Manager 引用, 并且知道 pairA 的字段应该是 Manager 的父类型, 函数可以在进行一些处理后得到需求的 Manager 对象, 并把它设置到 pairA 的字段, 这同样是由于父类的指针可以去接收(读取)子类型的对象, 也即一个原是一种形状.