JavaSE笔记——泛型


前言

多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。在类内部也是如此,在任何使用特定类型的地方,基类意味着更大的灵活性。除了 final 类(或只提供私有构造函数的类)任何类型都可被扩展,所以大部分时候这种灵活性是自带的。

拘泥于单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中。如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。这给予调用方一种选项,通过调整现有的类来实现接口,满足方法参数要求。接口可以突破继承体系的限制。

即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用 “非特定的类型”,而不是一个具体的接口或类。

这就是泛型的概念,是 Java 5 的重大变化之一。泛型实现了参数化类型,这样你编写的组件(通常是集合)可以适用于多种类型。“泛型” 这个术语的含义是 “适用于很多类型”。编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。


一、简单泛型

促成泛型出现的最主要的动机之一是为了创建集合类,集合用于存放要使用到的对象。数组也是如此,不过集合比数组更加灵活,功能更丰富。几乎所有程序在运行过程中都会涉及到一组对象,因此集合是可复用性最高的类库之一。

我们希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数。在下面的例子中,T 就是类型参数:

public class GenericHolder<T> {
    private T a;

    public GenericHolder() {
    }

    public T getA() {
        return a;
    }

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

    public static void main(String[] args) {
        GenericHolder<String> genericHolder = new GenericHolder<>();
        genericHolder.setA("aaaa");
        System.out.println(genericHolder.getA());
    }
}

创建 GenericHolder 对象时,必须指明要持有的对象的类型,将其置于尖括号内,就像 main() 中那样使用。然后,你就只能在 GenericHolder 中存储该类型(或其子类,因为多态与泛型不冲突)的对象了。当你调用 get() 取值时,直接就是正确的类型。这就是 Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。

1.一个元组类库

有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。

这个概念称为元组,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象或 信使)。

通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面是一个可以存储两个对象的元组:

public class Tuple2<A, B> {

    public final A a1;
    public final B a2;

    public Tuple2(A a, B b) {
        a1 = a;
        a2 = b;
    }

    public String rep() {
        return a1 + ", " + a2;
    }

    @Override
    public String toString() {
        return "(" + rep() + ")";
    }
    
}

public class TupleTest {
    static Tuple2<String, Integer> f() {
        return new Tuple2<>("hi", 47);
    }

    public static void main(String[] args) {
        Tuple2<String, Integer> ttsi = f();
        System.out.println(ttsi);
    }
}

构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。

初次阅读上面的代码时,你可能认为这违反了 Java 编程的封装原则。a1 和 a2 应该声明为 private,然后提供 getFirst() 和 getSecond() 取值方法才对呀?考虑下这样做能提供的 “安全性” 是什么:元组的使用程序可以读取 a1 和 a2 然后对它们执行任何操作,但无法对 a1 和 a2 重新赋值。例子中的 final 可以实现同样的效果,并且更为简洁明了。

2.一个堆栈类

用链式结构实现的堆栈:

public class LinkedStack<T> {
    private static class Node<U> {
        U item;
        Node<U> next;

        Node() {
            item = null;
            next = null;
        }

        Node(U item, Node<U> next) {
            this.item = item;
            this.next = next;
        }

        boolean end() {
            return item == null && next == null;
        }
    }

    // 栈顶
    private Node<T> top = new Node<>();

    public void push(T item) {
        top = new Node<>(item, top);
    }

    public T pop() {
        T result = top.item;
        if (!top.end()) {
            top = top.next;
        }
        return result;
    }

    public static void main(String[] args) {
        LinkedStack<String> lss = new LinkedStack<>();
        for (String s : "Phasers on stun!".split(" ")) {
            lss.push(s);
        }
        String s;
        while ((s = lss.pop()) != null) {
            System.out.println(s);
        }
    }
}

在这里插入图片描述
内部类 Node 也是一个泛型,它拥有自己的类型参数。这个例子使用了一个 末端标识 (end sentinel) 来判断栈何时为空。这个末端标识是在构造 LinkedStack 时创建的。然后,每次调用 push() 就会创建一个 Node 对象,并将其链接到前一个 Node 对象。当你调用 pop() 方法时,总是返回 top.item,然后丢弃当前 top 所指向的 Node,并将 top 指向下一个 Node,除非到达末端标识,这时就不能再移动 top 了。如果已经到达末端,程序还继续调用 pop() 方法,它只能得到 null,说明栈已经空了。

二、泛型接口

泛型也可以应用于接口。例如 生成器,这是一种专门负责创建对象的类。实际上,这是 工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象。

一般而言,一个生成器只定义一个方法,用于创建对象。例如 java.util.function 类库中的 Supplier 就是一个生成器,调用其 get() 获取对象。get() 是泛型方法,返回值为类型参数 T。

public class CoffeeSupplier implements Supplier<Coffee>, Iterable<Coffee> {

    private Class<?>[] types = {Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class};
    private static Random rand = new Random(47);

    public CoffeeSupplier() {
    }

    private int size = 0;

    public CoffeeSupplier(int sz) {
        size = sz;
    }

    @Override
    public Coffee get() {
        try {
            return (Coffee) types[rand.nextInt(types.length)].newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Iterator<Coffee> iterator() {
        return new CoffeeIterator();
    }

    class CoffeeIterator implements Iterator<Coffee> {
        int count = size;

        @Override
        public boolean hasNext() {
            return count > 0;
        }

        @Override
        public Coffee next() {
            count--;
            return CoffeeSupplier.this.get();
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

    }

    public static void main(String[] args) {
        Stream.generate(new CoffeeSupplier()).limit(5).forEach(System.out::println);
        for (Coffee c : new CoffeeSupplier(5)) {
            System.out.println(c);
        }

    }
}

在这里插入图片描述
参数化的 Supplier 接口确保 get() 返回值是参数的类型。CoffeeSupplier 同时还实现了 Iterable 接口,所以能用于 for-in 语句。不过,它还需要知道何时终止循环,这正是第二个构造函数的作用。

三、泛型方法

到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。

泛型方法独立于类而改变方法。作为准则,请 “尽可能” 使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。

如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。

要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:

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);
    }
}

在这里插入图片描述
尽管可以同时对类及其方法进行参数化,但这里未将 GenericMethods 类参数化。只有方法 f() 具有类型参数,该参数由方法返回类型之前的参数列表指示。

对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。这称为 类型参数推断。因此,对 f() 的调用看起来像普通的方法调用,并且 f() 看起来像被重载了无数次一样。它甚至会接受GenericMethods 类型的参数。

1.变长参数和泛型方法

泛型方法和变长参数列表可以很好地共存:

public class GenericVarargs {

    @SafeVarargs
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<>();
        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);
    }

}

在这里插入图片描述
此处显示的 makeList() 方法产生的功能与标准库的 java.util.Arrays.asList() 方法相同。

@SafeVarargs 注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。

2.一个泛型的 Supplier

这是一个为任意具有无参构造方法的类生成 Supplier 的类。为了减少键入,它还包括一个用于生成 BasicSupplier 的泛型方法:

public class BasicSupplier<T> implements Supplier<T> {
    private Class<T> type;

    public BasicSupplier(Class<T> type) {
        this.type = type;
    }

    @Override
    public T get() {
        try {
            return type.newInstance();
        } catch (InstantiationException |
                IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> Supplier<T> create(Class<T> type) {
        return new BasicSupplier<>(type);
    }
}

此类提供了产生以下对象的基本实现:

  1. 是 public 的。因为 BasicSupplier 在单独的包中,所以相关的类必须具有 public权限,而不仅仅是包级访问权限。
  2. 具有无参构造方法。要创建一个这样的 BasicSupplier 对象,请调用 create() 方法,并将要生成类型的类型令牌传递给它。通用的 create() 方法提供了 BasicSupplier.create(MyType.class) 这种较简洁的语法来代替较笨拙的 new BasicSupplier <MyType> (MyType.class)。

3.简化元组的使用

    static Tuple2<String, Integer> f() {
        return new Tuple2<>("hi", 47);
    }

4.一个 Set 工具

对于泛型方法的另一个示例,请考虑由 Set 表示的数学关系。这些被方便地定义为可用于所有不同类型的泛型方法:

public class Sets {
    public static <T> Set<T> union(Set<T> a, Set<T> b) {
        Set<T> result = new HashSet<>(a);
        result.addAll(b);
        return result;
    }

    public static <T>
    Set<T> intersection(Set<T> a, Set<T> b) {
        Set<T> result = new HashSet<>(a);
        result.retainAll(b);
        return result;
    }
    
    public static <T> Set<T>
    difference(Set<T> superset, Set<T> subset) {
        Set<T> result = new HashSet<>(superset);
        result.removeAll(subset);
        return result;
    }
    
    public static <T> Set<T> complement(Set<T> a, Set<T> b) {
        return difference(union(a, b), intersection(a, b));
    }
}

前三个方法通过将第一个参数的引用复制到新的 HashSet 对象中来复制第一个参数,因此不会直接修改参数集合。因此,返回值是一个新的 Set 对象。

这四种方法代表数学集合操作:union() 返回一个包含两个参数并集的 Set ,intersection() 返回一个包含两个参数集合交集的 Set ,difference() 从 superset中减去 subset 的元素,而 complement() 返回所有不在交集中的元素的 Set。

四、构建复杂模型

泛型的一个重要好处是能够简单安全地创建复杂模型。例如,我们可以轻松地创建一个元组列表:

public class TupleList <A, B> extends ArrayList<Tuple2<A, B>> {

    public static void main(String[] args) {
        TupleList<String, Integer> tl = new TupleList<>();
        tl.add(TupleTest.f());
        tl.add(TupleTest.f());
        tl.forEach(System.out::println);
    }

}

在这里插入图片描述
这将产生一个功能强大的数据结构,而无需太多代码。

五、泛型擦除

public class LostInformation {
    public static void main(String[] args) {
        List<Frob> list = new ArrayList<>();
        Map<Frob, Fnorkle> map = new HashMap<>();
        Quark<Fnorkle> quark = new Quark<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
    }

    static class Frob {}
    static class Fnorkle {}
    static class Quark<Q> {}
}

在这里插入图片描述
根据 JDK 文档,Class.getTypeParameters() “返回一个 TypeVariable 对象数组,表示泛型声明中声明的类型参数…” 这暗示你可以发现这些参数类型。但是正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。

残酷的现实是:在泛型代码内部,无法获取任何有关泛型参数类型的信息。因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。在使用 Java 泛型工作时,它是必须处理的最基本的问题。

Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List<String> 和 List<Integer> 在运行时实际上是相同的类型。它们都被擦除成原生类型 List。

1.迁移兼容性

如果 Java 1.0 就含有泛型的话,那么这个特性就不会使用擦除来实现——它会使用具体化,保持参数类型为第一类实体,因此你就能在类型参数上执行基于类型的语言操作和反射操作。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。

在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,List 这样的类型注解会被擦除为 List,普通的类型变量在未指定边界的情况下会被擦除为 Object。

擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为 “迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。

因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。

2.擦除的问题

因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。

擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。考虑如下的代码段:

class Foo<T> {
    T var;
}

Foo<Cat> f = new Foo<>();

class Foo 中的代码应该知道现在工作于 Cat 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,当你在编写这个类的代码时,必须提醒自己:“不,这只是一个 Object“。

另外,擦除和迁移兼容性意味着,使用泛型并不是强制的:

public class GenericBase<T> {
    private T element;
    public void set(T arg) {
        element = arg;
    }
    public T get() {
        return element;
    }
}
class Derived1<T> extends GenericBase<T> {}

class Derived2 extends GenericBase {}

3.边界处的动作

因为擦除,发现泛型最令人困惑的方面是可以表示没有任何意义的事物。例如:

public class ArrayMaker<T> {
    private Class<T> kind;
    public ArrayMaker(Class<T> kind) {
        this.kind = kind;
    }
    @SuppressWarnings("unchecked")
    T[] create(int size) {
        return (T[]) Array.newInstance(kind, size);
    }
    public static void main(String[] args) {
        ArrayMaker<String> stringMaker = new ArrayMaker<>(String.class);
        String[] stringArray = stringMaker.create(9);
        System.out.println(Arrays.toString(stringArray));
    }

}

在这里插入图片描述
即使 kind 被存储为 Class<T>,擦除也意味着它实际被存储为没有任何参数的Class。因此,当你在使用它时,例如创建数组,Array.newInstance() 实际上并未拥有 kind 所蕴含的类型信息。所以它不会产生具体的结果,因而必须转型,这会产生一条令你无法满意的警告。

如果我们创建一个集合而不是数组,情况就不同了:

public class ListMaker<T> {
    List<T> create() {
        return new ArrayList<>();
    }

    public static void main(String[] args) {
        ListMaker<String> stringMaker = new ListMaker<>();
        List<String> stringList = stringMaker.create();
    }
}

编译器不会给出任何警告,尽管我们知道(从擦除中)在 create() 内部的 newArrayList<>() 中的 被移除了——在运行时,类内部没有任何 <T>,因此这看起来毫无意义。但是如果你遵从这种思路,并将这个表达式改为 new ArrayList(),编译器就会发出警告。

本例中这么做真的毫无意义吗?如果在创建 List 的同时向其中放入一些对象呢,像这样:

public class FilledList<T> extends ArrayList<T> {
    
    public FilledList(T t, int size) {
        for (int i = 0; i < size; i++) {
            this.add(t);
        }
    }

    public static void main(String[] args) {
        List<String> list = new FilledList<>("Hello", 4);
        System.out.println(list);
    }
}

在这里插入图片描述

即使编译器无法得知 add() 中的 T 的任何信息,但它仍可以在编译期确保你放入FilledList 中的对象是 T 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。

因为擦除移除了方法体中的类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。

六、补偿擦除

因为擦除,我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型:

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

    public void f(Object arg) {
        // error: illegal generic type for instanceof
        if (arg instanceof T) {
        }
        // error: unexpected type
        T var = new T();
        // error: generic array creation
        T[] array = new T[SIZE];
        // warning: [unchecked] unchecked cast
        T[] array = (T[]) new Object[SIZE];
    }
}

有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 Class 对象,以在类型表达式中使用它。

public class ClassTypeCapture<T> {
    Class<T> kind;

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }

    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
}

在这里插入图片描述
编译器来保证类型标签与泛型参数相匹配。

1.创建类型的实例

试图在类中 new T() 是行不通的,部分原因是由于擦除,部分原因是编译器无法验证 T 是否具有默认(无参)构造函数。 Java 中的解决方案是传入一个工厂对象,并使用该对象创建新实例。方便的工厂对象只是 Class 对象,因此,如果使用类型标记,则可以使用 newInstance() 创建该类型的新对象:

public class ClassAsFactory<T> implements Supplier<T> {
    Class<T> kind;

    ClassAsFactory(Class<T> kind) {
        this.kind = kind;
    }

    @Override
    public T get() {
        try {
            return kind.newInstance();
        } catch (InstantiationException |
                IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        ClassAsFactory<Building> fii = new ClassAsFactory<>(Building.class);
        System.out.println(fii.get());

//        ClassAsFactory<Integer> fi = new ClassAsFactory<>(Integer.class);
//        System.out.println(fi.get());
    }
}

在这里插入图片描述

对于 ClassAsFactory<Integer> 会失败,这是因为 Integer 没有无参构造函数。由于错误不是在编译时捕获的,因此语言创建者不赞成这种方法。他们建议使用显式工厂(Supplier)并约束类型,以便只有实现该工厂的类可以这样创建对象。这是创建工厂的两种不同方法:

public class IntegerFactory implements Supplier<Integer> {
    private int i = 0;

    @Override
    public Integer get() {
        return ++i;
    }
}

public class Widget {
    private int id;

    Widget(int n) {
        id = n;
    }

    @Override
    public String toString() {
        return "Widget " + id;
    }

    public static class Factory implements Supplier<Widget> {
        private int i = 0;

        @Override
        public Widget get() {
            return new Widget(++i);
        }
    }
}

public class Fudge {
    private static int count = 1;
    private int n = count++;

    @Override
    public String toString() {
        return "Fudge " + n;
    }
}

public class Foo2<T> {
    private List<T> x = new ArrayList<>();

    Foo2(Supplier<T> factory) {
        Suppliers.fill(x, factory, 5);
    }

    @Override
    public String toString() {
        return x.toString();
    }
}

public class Suppliers {
    // Create a collection and fill it:
    public static <T, C extends Collection<T>> C
    create(Supplier<C> factory, Supplier<T> gen, int n) {
        return Stream.generate(gen)
                .limit(n)
                .collect(factory, C::add, C::addAll);
    }

    // Fill an existing collection:
    public static <T, C extends Collection<T>>
    C fill(C coll, Supplier<T> gen, int n) {
        Stream.generate(gen)
                .limit(n)
                .forEach(coll::add);
        return coll;
    }

    // Use an unbound method reference to
    // produce a more general method:

    public static <H, A> H fill(H holder, BiConsumer<H, A> adder, Supplier<A> gen, int n) {
        Stream.generate(gen)
                .limit(n)
                .forEach(a -> adder.accept(holder, a));
        return holder;
    }

}

public class FactoryConstraint {
    public static void main(String[] args) {
        System.out.println(new Foo2<>(new IntegerFactory()));
        System.out.println(new Foo2<>(new Widget.Factory()));
        System.out.println(new Foo2<>(Fudge::new));
    }
}

IntegerFactory 本身就是通过实现 Supplier<Integer> 的工厂。Widget 包含一个内部类,它是一个工厂。还要注意,Fudge 并没有做任何类似于工厂的操作,并且传递 Fudge::new 仍然会产生工厂行为,因为编译器将对函数方法 Fudge::new 的调用转换为对 get() 的调用。

另一种方法是模板方法设计模式。在以下示例中,create() 是模板方法,在子类中被重写以生成该类型的对象:

public abstract class GenericWithCreate<T> {
    final T element;

    GenericWithCreate() {
        element = create();
    }

    abstract T create();
}

public class X {
}

public class XCreator extends GenericWithCreate<X>{

    @Override
    X create() {
        return new X();
    }

    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

public class CreatorGeneric {
    public static void main(String[] args) {
        XCreator xc = new XCreator();
        xc.f();
    }
}

在这里插入图片描述
GenericWithCreate 包含 element 字段,并通过无参构造函数强制其初始化,该构造函数又调用抽象的 create() 方法。这种创建方式可以在子类中定义,同时建立 T 的类型。

七、边界

边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法

由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends 关键字。

重要的是要理解,当用于限定泛型类型时,extends 的含义与通常的意义截然不同。此示例展示边界的基础应用:

public class Coord {
    public int x, y, z;

    public Coord(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

public class Bounded extends Coord{

    public Bounded(int x, int y, int z) {
        super(x, y, z);
    }
}

public class Solid<T extends Coord> {
    T item;

    public Solid(T t) {
        item = t;
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }

    public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<>(new Bounded(1, 2, 3));
        System.out.println(solid.getX());
    }
}

八、通配符

你想在两个类型间建立某种向上转型关系,通配符可以产生这种关系。

public class GenericsAndCovariance {
    public static void main(String[] args) {
        List<? extends Fruit> flist = new ArrayList<>();
//        flist.add(new Apple());
//        flist.add(new Fruit());
//        flist.add(new Object());
        flist.add(null);
        Fruit f = flist.get(0);
    }
}

flist 的类型现在是 List<? extends Fruit>,你可以读作 “一个具有任何从 Fruit继承的类型的列表”。然而,这实际上并不意味着这个 List 将持有任何类型的 Fruit。通配符引用的是明确的类型,因此它意味着 “某种 flist 引用没有指定的具体类型”。因此这个被赋值的 List 必须持有诸如 Fruit 或 Apple 这样的指定类型,但是为了向上转型为 Fruit,这个类型是什么没人在意。

List 必须持有一种具体的 Fruit 或 Fruit 的子类型,但是如果你不关心具体的类型是什么,那么你能对这样的 List 做什么呢?如果不知道 List 中持有的对象是什么类型,你怎能保证安全地向其中添加对象呢?就像在 CovariantArrays.java 中向上转型一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这个问题。

你可能认为事情开始变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有Apple 对象的 List 中放入一个 Apple 对象。是的,但编译器并不知道这一点。List<? extends Fruit> 可能合法地指向一个 List<Orange>。一旦执行这种类型的向上转型,你就丢失了向其中传递任何对象的能力,甚至传递 Object 也不行。

1.超类型通配符

这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass> ,或者甚至使用类型参数:<?super T>(尽管你不能对泛型参数给出一个超类型边界;即不能声明 <T super
MyClass> )。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 Collection 写入了:

public class SuperTypeWildcards {
    static void writeTo(List<? super Fruit> apples) {
        apples.add(new Apple());
        apples.add(new Orange());
    }
}

2.无界通配符

无界通配符 <?> 看起来意味着 “任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的:

public class UnboundedWildcards1 {
    static List list1;
    static List<?> list2;
    static void assign1(List list) {
        list1 = list;
        list2 = list;
    }

    public static void main(String[] args) {
        assign1(new ArrayList());
    }
}

总结

普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大,泛型使我们能编写更通用的代码。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值