[笔记] Java 泛型

By | 6月 15, 2018

简单泛型

public class TwoTuple<A, B> {
    public final A first;
    public final B second;
    public TwoTuple(A a, B b) {
        this.first = a;
        this.second = b;
    }
    public String toString() {
        return "(" + first + ". " + second + ")";
    }
}

泛型接口

泛型除了应用于类之外,也可以用于接口,比如生成器(Generator)。Generator 无需任何额外信息就知道如何创建一个新的对象出来。
一般情况下,生成器只需要定义一个方法来产生一个新的对象:

public interface Generator {
    T next();
}

比如下面用来生成一个 fibonacci 队列的生成器:

public class Fibonacci implements Generator {
    private int count = 0;
    @Override
    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() + " ");
        }
    }
}

更进一步,还可以通过实现 Iterable<> 接口来实现一个生成 fibonacci 队列的迭代器。不过我们并不想重写刚才的类,这个时候可以通过创建一个原来类的适配器(Adapter)来达成目的:

public class IterableFibonacci extends Fibonacci implements Iterable {
    private int n;
    public IterableFibonacci(int count) {
        n = count;
    }
    class FibonacciIterator implements Iterator {
        @Override
        public boolean hasNext() {
            return n > 0;
        }
        @Override
        public Integer next() {
            n--;
            return IterableFibonacci.this.next();
        }
    }
    @Override
    public Iterator iterator() {
        return new FibonacciIterator();
    }
    public static void main(String[] args) {
        for (int i : new IterableFibonacci(18)) {
            System.out.print(i + " ");
        }
    }
}

泛型方法

泛型除了可以应用于类上,还可以应用在方法之上。
一个基本的原则是:任何时候,只要能使用泛型方法来替代整个类的泛型化,那么就应该使用泛型方法,因为这样可以使逻辑更加清晰。
定义泛型方法示例:

public class GenericMethods {
    public  void f(T x) {
        System.out.println(x.getClass().getName());
    }
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f("t");
        gm.f("t");  // 显示声明
    }
}

利用 Generator,可以很方便的填充一个 Collection,而一个泛型方法 fill 来实现这个是再简单不过的事情了,下面是示例代码:

public class Generators {
    public static  Collection fill(Collection coll, Generator gen, int n) {
        for (int i = 0; i < n; i++) {
            coll.add(gen.next());
        }
        return coll;
    }
    public static void main(String[] args) {
        Collection coffee = fill(new ArrayList<>(), new CoffeeGenerator(), 4);
        for (Coffee c : coffee) {
            System.out.println(c);
        }
    }
}

另外一个非常有用的场景是为任何类构造一个通用的 Generator 来构造对象:

public class BasicGenerator implements Generator {
    private Class type;
    public BasicGenerator(Class type) {
        this.type = type;
    }
    @Override
    public T next() {
        try {
            return type.newInstance();
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
    public static  Generator create(Class type) {
        return new BasicGenerator<>(type);
    }
}

示例对象:

public class CountedObject {
    private static long counter = 0;
    private final long id = counter++;
    public long id() {
        return id;
    }
    public String toString() {
        return "CountedObject " + id;
    }
}

示例调用代码:

public class BasicGeneratorDemo {
    public static void main(String[] args) {
        Generator gen = BasicGenerator.create(CountedObject.class);
        for (int i = 0; i < 5; i++) {
            System.out.println(gen.next());
        }
    }
}

应用于匿名内部类

class Customer {
    private static long counter = 1;
    private final long id = counter++;
    private Customer() {}
    public String toString() {
        return "Customer " + id;
    }
    public static Generator generator() {
        return new Generator<>() {
            public Customer next() {
                return new Customer();
            }
        };
    }
}
class Teller {
    private static long counter = 1;
    private final long id = counter++;
    private Teller() {}
    public String toString() {
        return "Teller " + id;
    }
    public static Generator generator = new Generator<>() {
        public Teller next() {
            return new Teller();
        }
    };
}
public class BankTeller {
    public static void serve(Teller t, Customer c) {
        System.out.println(t + " serves " + c);
    }
    public static void main(String[] args) {
        Random rand = new Random(47);
        Queue line = new LinkedList<>();
        Generators.fill(line, Customer.generator(), 15);
        List tellers = new ArrayList<>();
        Generators.fill(tellers, Teller.generator, 4);
        for (Customer c : line) {
            serve(tellers.get(rand.nextInt(tellers.size())), c);
        }
    }
}

擦除带来的影响

Java 的泛型和 C++ 中的泛型有很多不同,因为历史包袱的原因,Java 的泛型使用擦除来实现,这也会带来很多额外的问题,比如下面的代码:

Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1 == c2);

c1 和 c2 虽然看起来是不同的类型,一个是存放 String 的 ArrayList,另一个是存放 Integer 的 ArrayList,但是输出结果是 true,也就是说,在编译器看来,c1 和 c2 的类型是等价的,都是 ArrayList
一个残酷的现实是:在泛型代码的内部,无法获得任何有关泛型参数类型的信息。
在使用泛型的时候,任何具体的类型信息都被擦除了,你唯一知道的就是,这是个 Object。
所以,任何在运行时需要知道确切类型信息的操作都将无法工作,比如下面的代码:

public class Erased {
    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
    }
}

下面看如何通过补偿措施来达到上述代码的目的。

擦除的补偿措施

判定传入的对象是否为对应的类型

上面的代码 if (arg instanceof T) 出错的原因是内部不知道对应的类型信息,那我们可以通过传入类型标签,调用动态的 isInstance() 方法的来绕过,比如:

public class ClassTypeCapture {
    Class kind;
    public ClassTypeCapture(Class kind) {
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
    public static void main(String[] args) {
        ClassTypeCapture ctt1 = new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture ctt2 = new ClassTypeCapture<>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
}

输出结果为:

true
true
false
true

创建类型实例

T var = new T() 这种表达式在 Java 中会触发 Error,绕过方式是传递一个工厂对象,并用它来创建新的实例,示例代码:

interface FactoryI {
    T create();
}
class Foo2 {
    private T x;
    public > Foo2(F factory) {
        x = factory.create();
    }
}
class IntegerFactory implements FactoryI {
    public Integer create() {
        return 0;
    }
}
public class FactoryConstraint {
    public static void main(String[] args) {
        new Foo2(new IntegerFactory());
    }
}

除此之外,可以使用模板方法的设计模式,下面的示例中,get() 是模板方法,create() 是在子类中定义的、用来产生子类类型的对象:

abstract class GenericWithCreate {
    final T element;
    GenericWithCreate() {
        element = create();
    }
    abstract T create();
}
class X {}
class Creator extends GenericWithCreate {
    X create() {
        return new X();
    }
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}
public class CreatorGeneric {
    public static void main(String[] args) {
        Creator c = new Creator();
        c.f();
    }
}

泛型数组

一般的解决方案是再任何想要创建泛型数组的地方使用 ArrayList,这样既可以获得数组的行为,也可以获得由泛型提供的编译期的类型安全。示例:

public class ListOfGenerics {
    private List array = new ArrayList<>();
    public void add(T item) {
        array.add(item);
    }
    public T get(int index) {
        return array.get(index);
    }
}

但如果想直接创建泛型类型的数组,像下面这样:

public class GenericArray {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArray(int sz) {
        array = (T[]) new Object[sz];
    }
    public T[] rep() {
        return array;
    }
    public static void main(String[] args) {
        GenericArray gai = new GenericArray<>(10);
        Integer[] ia = gai.rep(); // Error
        Object[] oa = gai.rep(); // OK
    }
}

可以看到,用 Integer[] 方式取回数组给出了一个无法转换的异常,因为数组实际上是一个 Object[] 的类型。这里同样可以使用传递类型标签的方式来解决:

public class GenericArrayWithTypeToken {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class type, int sz) {
        array = (T[]) Array.newInstance(type, sz);
    }
    public T[] rep() {
        return array;
    }
    public static void main(String[] args) {
        GenericArrayWithTypeToken gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
        Integer[] ia = gai.rep();
    }
}

边界

当泛型没有边界限制的时候,可以认为是一个 Object,但是我们可以在用于泛型的参数类型上设置限制条件。这样有两个好处:

  • 可以强制规定泛型可以应用的类型
  • 可以按照自己定义的边界类型来调用对应的方法

Java 重用了 extends 关键字来完成这个操作:

class HoldItem {
    T item;
    HoldItem(T item) {
        this.item = item;
    }
    T getItem() {
        return item;
    }
}
class Colored2 extends HoldItem {
    Colored2(T item) {
        super(item);
    }
    Color color() {
        return item.getColor();
    }
}
// 注意 class 在前,interface 在后,class 仅允许 0 或 1 个,interface 允许任意多个
class ColoredDimension2 extends Colored2 {
    ColoredDimension2(T item) {
        super(item);
    }
    int getX() {
        return item.x;
    }
    int getY() {
        return item.y;
    }
    int getZ() {
        return item.z;
    }
}
class Solid2 extends ColoredDimension2 {
    Solid2(T item) {
        super(item);
    }
    int weight() {
        return item.weight();
    }
}
public class InheritBounds {
    public static void main(String[] args) {
        Solid2 solid2 = new Solid2<>(new Bounded());
        solid2.color();
        solid2.getY();
        solid2.weight();
    }
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注