Java编程笔记13:泛型

Java编程笔记13:泛型

Java编程笔记13:泛型_第1张图片

图源:PHP中文网

容器中的泛型

泛型存在的理由之一是解决容器可以持有不同类型的数据的问题。

假设有一个最简单的容器:

package ch13.container;

import util.Fmt;

class SimpleContainer {
    private Object content;

    public SimpleContainer(Object content) {
        this.content = content;
    }

    public Object getContent() {
        return content;
    }

    public void setContent(Object content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return Fmt.sprintf("SimpleContainer(%s)", content);
    }
}

public class Main {
    public static void main(String[] args) {
        SimpleContainer sc = new SimpleContainer(17);
        System.out.println(sc);
        sc.setContent("hello");
        System.out.println(sc);
    }
}
// SimpleContainer(17)
// SimpleContainer(hello)

在这个例子中,SimpleContainer中使用Object类型的对象来持有数据,这样做可以让容器保存所有类型的数据,但问题是我们使用容器时一般都是想持有一种类型的数据,而非任意类型,现在这种做法产生的问题是:

  • 无法在保存数据时进行类型静态检查。
  • 从容器中取出的数据只会是Object,而非我们期待的类型。

通过引入泛型,就可以解决这个问题:

package ch13.container2;

import util.Fmt;

class SimpleContainer<T> {
    private T content;

    public SimpleContainer(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return Fmt.sprintf("SimpleContainer(%s)", content);
    }
}

public class Main {
    public static void main(String[] args) {
        SimpleContainer<Integer> sc = new SimpleContainer<>(17);
        System.out.println(sc);
        sc.setContent(22);
        System.out.println(sc);
        // sc.setContent("hello");
        // System.out.println(sc);
        // The method setContent(Integer) in the type SimpleContainer is not
        // applicable for the arguments (String)
    }
}
// SimpleContainer(17)
// SimpleContainer(22)

示例中注释部分的代码无法运行,因为我们通过泛型让SimpleContainer中保存的数据类型限制为Integer或其子类,想保存一个String类型的数据是无法通过静态检查的。此外,如果使用getContent获取数据,返回结果也将是Integer而非Object,这就避免了可能需要的类型转换,现在就不存在之前所说的问题了。

总的来说,泛型的作用就是让静态类型检查和类型转换的工作由编译器完成。

元组

如果你熟悉Python,就会知道元组(Tuple),事实上这是一种可以包含多个不同类型的元素,且创建后无法修改的容器。

我们可以利用泛型在Java中实现元组:

package ch13.tuple;

import util.Fmt;

class TwoTuple<A, B> {
    public final A a;
    public final B b;

    public TwoTuple(A a, B b) {
        this.a = a;
        this.b = b;
    }
}

public class Main {
    public static void main(String[] args) {
        TwoTuple<String, Integer> student1 = new TwoTuple("Li Lei", 23);
        TwoTuple<String, Integer> student2 = new TwoTuple("Han Meimei", 20);
        printStudent(student1);
        printStudent(student2);
    }

    private static void printStudent(TwoTuple<String, Integer> student) {
        Fmt.printf("Student's name is %s, age is %d.\n", student.a, student.b);
    }
}
// Student's name is Li Lei, age is 23.
// Student's name is Han Meimei, age is 20.

Java和大多数编程语言一样,函数只能有一个返回值(Go的函数支持多返回)。如果想返回多个值,一般需要使用类来包含多个值后返回。如果使用上面创建的元组,就会简单很多:

package ch13.tuple2;

import util.Fmt;

class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static TwoTuple<Student, Boolean> getNewStudent() {
        return new TwoTuple(new Student("none", 0), true);
    }

    @Override
    public String toString() {
        return Fmt.sprintf("Student's name is %s, age is %d.\n", name, age);
    }
}

public class Main {
    public static void main(String[] args) {
        TwoTuple<Student, Boolean> result = Student.getNewStudent();
        if (result.b) {
            System.out.println(result.a);
        }
    }
}
// Student's name is none, age is 0.

如果需要包含两个以上元素的元组,也可以很容易地通过扩展TwoTuple类来实现:

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

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

}

class FourTuple<A, B, C, D> extends ThreeTuple<A, B, C> {
    public final D d;

    public FourTuple(A a, B b, C c, D d) {
        super(a, b, c);
        this.d = d;
    }
}

class FiveTuple<A, B, C, D, E> extends FourTuple<A, B, C, D> {
    public final E e;

    public FiveTuple(A a, B b, C c, D d, E e) {
        super(a, b, c, d);
        this.e = e;
    }
}

在Java编程笔记8:容器(上) - 魔芋红茶’s blog (icexmoon.xyz)中,展示过如何通过LinkedList实现一个栈,利用泛型,我们也可以自己实现一个栈:

package ch13.stack;

import util.Fmt;

class MyStack<T> {
    private static class Node<T> {
        private T content;
        private Node<T> next;

        public Node(T content, Node<T> next) {
            this.content = content;
            this.next = next;
        }
    }

    private Node<T> top = new Node<T>(null, null);

    public boolean empty() {
        if (top.content == null && top.next == null) {
            return true;
        }
        return false;
    }

    public T pop() {
        if (empty()) {
            return null;
        }
        T result = top.content;
        top = top.next;
        return result;
    }

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

public class Main {
    public static void main(String[] args) {
        MyStack<String> stack = new MyStack<String>();
        String[] words = "Hello world, how are you!".split("( |, ?|! ?)");
        for (String word : words) {
            stack.push(word);
        }
        while (!stack.empty()) {
            Fmt.printf("%s ", stack.pop());
        }
    }
}
// you are how world Hello

这里的关键是在MyStack中使用了一个从头部添加和删除元素的链表,并且在链表的尾部使用一个空节点Node(null,null)作为判断栈是否为空的依据。

此外,因为使用了泛型,我们自定义的栈可以像标准组件那样支持多种类型的元素。

泛型接口

接口也可以使用泛型,比如,一般编程语言中都会有“生成器”这个概念,一般来说生成器与迭代器是类似的,但是生成器更倾向于“获取下一个数据”,而不关心遍历的结束条件。

一般来说迭代器会是一个生成器,而反之不一定成立。同样的,一部分实现了生成器功能的程序,并不会一次性读取所有数据源后再迭代,而是边迭代边从数据源获取数据,这样的好处是可以节省内存开销。

我们可以用下面的接口来表示一个生成器:

package ch13.generator;

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

在Java编程笔记12:类型信息 - 魔芋红茶’s blog (icexmoon.xyz)中介绍递归统计的时候,创建过Animal类簇,这里使用这个类簇来编写一个AnimalGenerator类,用来批量创建Ainmal实例:

package ch13.generator;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

class AnimalGenerator implements Generator<Animal>, Iterable<Animal> {
    private int size = 0;
    private List<Class<? extends Animal>> types = new ArrayList<>();
    {
        Collections.addAll(types, SportingDog.class, WorkingDog.class, HerdingDog.class, PersianCat.class,
                BirmanCat.class);
    }
    private static Random rand = new Random();

    public AnimalGenerator(int size) {
        this.size = size;
    }

    @Override
    public Animal next() {
        Class<? extends Animal> type = types.get(rand.nextInt(types.size()));
        Constructor<?> constructor;
        try {
            constructor = type.getDeclaredConstructor();
            return (Animal) constructor.newInstance();
        } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
                | IllegalArgumentException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Iterator<Animal> iterator() {
        return new Iterator<Animal>() {
            private int nowSize = size;

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

            @Override
            public Animal next() {
                if (nowSize <= 0) {
                    return null;
                }
                nowSize--;
                return AnimalGenerator.this.next();
            }

        };
    }

}

public class Main {
    public static void main(String[] args) {
        AnimalGenerator ag = new AnimalGenerator(5);
        for (int i = 0; i < 7; i++) {
            System.out.print(ag.next().toString() + " ");
        }
        System.out.println();
        for (Animal animal : ag) {
            System.out.print(animal.toString() + " ");
        }
    }
}
// 0#WorkingDog 1#HerdingDog 2#PersianCat 3#BirmanCat 4#SportingDog 5#BirmanCat 6#BirmanCat 
// 7#SportingDog 8#WorkingDog 9#PersianCat 10#HerdingDog 11#SportingDog

为了可以让AnimalGenerator直接使用foreach语句,这里让其直在实现Generator接口的同时实现Iterator接口,该接口同样支持泛型。

类似的,还可以编写一个斐波那契数列生成器:

package ch13.generator2;

import ch13.generator.Generator;

class FibonacciGenerator implements Generator<Integer> {
    private int index = 1;

    @Override
    public Integer next() {
        return fibonacci(index++);
    }

    private static int fibonacci(int index) {
        if (index <= 2) {
            return 1;
        }
        return fibonacci(index - 1) + fibonacci(index - 2);
    }

}

public class Main {
    public static void main(String[] args) {
        FibonacciGenerator fg = new FibonacciGenerator();
        for (int i = 0; i < 10; i++) {
            System.out.print(fg.next() + " ");
        }
        System.out.println();
    }
}
// 1 1 2 3 5 8 13 21 34 55 

要让斐波那契数列生成器也可以使用foreach语句,同样可以选择让其实现Iterator接口,如果不方便那样做,也可以选择使用适配器模式:

package ch13.generator3;

import java.util.Iterator;

class FibonacciAdapter implements Iterable<Integer> {
    private FibonacciGenerator fg;
    private int size;

    public FibonacciAdapter(FibonacciGenerator fg, int size) {
        this.fg = fg;
        this.size = size;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            private int nowSize = size;

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

            @Override
            public Integer next() {
                if (nowSize <= 0) {
                    return -1;
                }
                int result = fg.next();
                nowSize--;
                return result;
            }

        };
    }

}

public class Main {
    public static void main(String[] args) {
        FibonacciGenerator fg = new FibonacciGenerator();
        for (int number : new FibonacciAdapter(fg, 10)) {
            System.out.print(number + " ");
        }
        System.out.println();
    }
}
// 1 1 2 3 5 8 13 21 34 55

泛型方法

前面我们看到,泛型类中的方法可能是泛型的,事实上泛型方法也可以存在于非泛型类中,比如:

package ch13.gen_method;

class Test {
    public static <T> void genericMthod(T param) {
        System.out.println("param type is " + param.getClass().getSimpleName());
    }
}

public class Main {
    public static void main(String[] args) {
        Test.genericMthod(1);
        Test.genericMthod("hello");
    }
}
// param type is Integer
// param type is String

要创建泛型方法,需要在方法返回值之前添加这样的“类型标签”,就像上边的genericMthod方法。

Test并非一个泛型类,但其静态方法genericMthod是一个泛型方法。

因为静态方法是不依赖于类实例的,所以即使Test是一个泛型类,genericMthod也无法从其实例中获取泛型的类型信息。

此外,虽然使用泛型类的时候需要指明泛型的类型信息,但使用泛型方法的时候一般不需要,因为编译器会根据参数或返回值的具体类型推断出类型信息,这叫做类型参数推断(type argument inference)。

再看一个从返回值推断类型的示例:

package ch13.gen_method2;

import java.util.List;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = getList();
        for (int i = 0; i < 10; i++) {
            numbers.add(i);
        }
        System.out.println(numbers);
    }

    private static <T> List<T> getList() {
        return new ArrayList<T>();
    }
}
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

将泛型方法的返回值作为参数进行传递同样可以完成类型推断:

package ch13.gen_method3;

import java.util.List;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = fillList(getList(), 10);
        System.out.println(numbers);
    }

    private static List<Integer> fillList(List<Integer> numbers, int size) {
        for (int i = 0; i < size; i++) {
            numbers.add(i);
        }
        return numbers;
    }

    private static <T> List<T> getList() {
        return new ArrayList<T>();
    }
}
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

在JavaSE 5之前这样做是不可行的。

在大多数情况下类型推断都可以很好的工作,但某些时候我们需要手动指定泛型方法应当使用的类型,此时就需要显式类型说明

使用起来也很简单,只要在调用泛型方法时,在方法名前加上“类型说明”即可:

...
public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = fillList(Main.<Integer>getList(), 10);
        System.out.println(numbers);
    }
    ...
}

需要注意的是,并不能直接使用getList()这样的写法,使用显式类型说明时必须在方法前添加上所属的类或实例,如果在类内部,应当使用this.methodName()这样的写法。

可变参数与泛型

可变参数是可以与泛型共存的,比如:

package ch13.multi_param;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List list = getList(1, 2, 3, 4, 5);
        System.out.println(list);
    }

    private static <T> List<T> getList(T... items) {
        List<T> list = new ArrayList<>();
        for (T t : items) {
            list.add(t);
        }
        return list;
    }
}
// [1, 2, 3, 4, 5]

事实上标准库的Arrays.asList方法就是这么做的。

简化元组

利用泛型方法的类型推断,我们可以简化之前的元组类型:

package ch13.easy_tuple;

import util.Fmt;

class Tuple {
    public static <A, B> TwoTuple<A, B> tuple(A a, B b) {
        return new TwoTuple<A, B>(a, b);
    }

    public static <A, B, C> ThreeTuple<A, B, C> tuple(A a, B b, C c) {
        return new ThreeTuple(a, b, c);
    }

    public static <A, B, C, D> FourTuple<A, B, C, D> tuple(A a, B b, C c, D d) {
        return new FourTuple(a, b, c, d);
    }

    public static <A, B, C, D, E> FiveTuple<A, B, C, D, E> tuple(A a, B b, C c, D d, E e) {
        return new FiveTuple(a, b, c, d, e);
    }
}

public class Main {
    public static void main(String[] args) {
        printStudent(Tuple.tuple("Li Lei", 20));
        printStudent(Tuple.tuple("Han Meimei", 15));
    }

    private static void printStudent(TwoTuple<String, Integer> student) {
        Fmt.printf("Student's name is %s, age is %d\n", student.a, student.b);
    }
}
// Student's name is Li Lei, age is 20
// Student's name is Han Meimei, age is 15

如果你需要频繁使用元组,使用Tuple中重载的tuple方法来创建元组会省事许多。

填充容器

再前边介绍泛型接口的时候,我们用生成器来举例,使用泛型方法,可以轻松实现一种通用的用生成器来填充容器的代码:

package ch13.fill_container;

import java.util.List;
import java.util.ArrayList;
import java.util.Collection;

import ch13.generator.Generator;
import ch13.generator3.FibonacciGenerator;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        Generator<Integer> fg = new FibonacciGenerator();
        fillContainerByGenerator(numbers, fg, 10);
        System.out.println(numbers);
    }

    /**
     * 用生成器给容器填充指定数目的元素
     * 
     * @param        元素类型
     * @param container 容器
     * @param generator 生成器
     * @param num       指定数目
     */
    private static <T> void fillContainerByGenerator(Collection<T> container, Generator<T> generator, int num) {
        if (num <= 0) {
            return;
        }
        for (int i = 0; i < num; i++) {
            container.add(generator.next());
        }
    }
}
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

泛型方法fillContainerByGenerator可以接受任何一种类型的容器和生成器,并完成填充工作,具有相当的灵活性,这就是泛型方法的好处。

集合运算

标准库的Set并不直接支持集合运算,但利用泛型方法,我们可以轻松实现:

package ch13.sets;

import java.util.HashSet;
import java.util.Set;

/**
 * 集合运算
 */
public class Sets {
    /**
     * 并运算
     * 
     * @param 
     * @param set1 集合1
     * @param set2 集合2
     * @return 集合1和集合2的并集
     */
    public static <T> Set<T> union(Set<T> set1, Set<T> set2) {
        Set<T> result = new HashSet<>(set1);
        result.addAll(set2);
        return result;
    }

    /**
     * 交运算
     * 
     * @param 
     * @param set1 集合1
     * @param set2 集合2
     * @return 集合1与集合2的交集
     */
    public static <T> Set<T> intersection(Set<T> set1, Set<T> set2) {
        Set<T> result = new HashSet<>(set1);
        result.retainAll(set2);
        return result;
    }

    /**
     * 差集(补集)
     * 
     * @param 
     * @param set1 集合1
     * @param set2 集合2
     * @return 集合1对集合2的差集
     */
    public static <T> Set<T> difference(Set<T> set1, Set<T> set2) {
        Set<T> result = new HashSet<>(set1);
        result.removeAll(set2);
        return result;
    }

    /**
     * 互补
     * 
     * @param 
     * @param set1 集合1
     * @param set2 集合2
     * @return 集合1和集合2的互补
     */
    public static <T> Set<T> complement(Set<T> set1, Set<T> set2) {
        return difference(union(set1, set2), intersection(set1, set2));
    }
}

可以用枚举来进行验证:

package ch13.sets;

import java.util.EnumSet;
import java.util.Set;

enum Color {
    BLUE, LIGHT_BLUE, DEEP_BLUE, RED, LIGHT_RED, DEEP_RED, YELLOW, LIGHT_YELLOW, DEEP_YELLOW, BLACK, PINK, LIGHT_PINK,
    DEEP_PINK
}

public class Main {
    public static void main(String[] args) {
        Set<Color> colors1 = EnumSet.range(Color.BLUE, Color.DEEP_RED);
        Set<Color> colors2 = EnumSet.range(Color.RED, Color.DEEP_YELLOW);
        System.out.println("colors1: " + colors1);
        System.out.println("colors2: " + colors2);
        System.out.println("colors1 union colors2: " + Sets.union(colors1, colors2));
        System.out.println("colors1 intersection colors2: " + Sets.intersection(colors1, colors2));
        System.out.println("colors1 difference colors2: " + Sets.difference(colors1, colors2));
        System.out.println("colors1 complement colors2: " + Sets.complement(colors1, colors2));
    }
}
// colors1: [BLUE, LIGHT_BLUE, DEEP_BLUE, RED, LIGHT_RED, DEEP_RED]
// colors2: [RED, LIGHT_RED, DEEP_RED, YELLOW, LIGHT_YELLOW, DEEP_YELLOW]
// colors1 union colors2: [BLUE, DEEP_YELLOW, YELLOW, LIGHT_BLUE, LIGHT_RED, DEEP_BLUE, DEEP_RED, LIGHT_YELLOW, RED]        
// colors1 intersection colors2: [LIGHT_RED, DEEP_RED, RED]
// colors1 difference colors2: [BLUE, LIGHT_BLUE, DEEP_BLUE]
// colors1 complement colors2: [BLUE, DEEP_YELLOW, YELLOW, LIGHT_BLUE, DEEP_BLUE, LIGHT_YELLOW]

示例中EnumSet.range的用途是通过指定开始和结束的枚举值来快速创建一个该范围内枚举值组成的集合。

我们还可以利用这种集合运算来查看Collection及其子类之间的方法差异:

package ch13.sets;

import java.util.List;
import java.util.ArrayList;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

public class MethodsDiff {
    private static final Set<String> objectMethodNames = getObjectMethodNames();

    private static Set<String> getObjectMethodNames() {
        Set<String> methodNames = getMethodNames(Object.class);
        methodNames.add("clone");
        return methodNames;
    }

    private static Set<String> getMethodNames(Class<?> type) {
        Set<String> methodNames = new HashSet<>();
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            methodNames.add(method.getName());
        }
        return methodNames;
    }

    public static void printMethodsDiff(Class<?> type1, Class<?> type2) {
        Set<String> methodNames1 = getMethodNames(type1);
        Set<String> methodNames2 = getMethodNames(type2);
        Set<String> diffMethodNames = Sets.difference(methodNames1, methodNames2);
        diffMethodNames = Sets.difference(diffMethodNames, objectMethodNames);
        System.out.println(diffMethodNames);
    }

    public static void main(String[] args) {
        printMethodsDiff(ArrayList.class, List.class);
        printMethodsDiff(LinkedList.class, List.class);
    }
}
// [trimToSize, ensureCapacity]
// [descendingIterator, offerFirst, poll, getLast, pollLast, removeLast, offer,
// pop, addLast, getFirst, removeFirst, element, removeLastOccurrence,
// peekFirst, peekLast, push, peek, offerLast, pollFirst, removeFirstOccurrence,
// addFirst]

获取给定Class对象的方法名,并保存到集合中求差集,就很容易可以观察到不同类之间多出了哪些方法。一般我们不需要关心Object中的方法,所以结果还需要排除Object中的方法。

类型擦除

Java的泛型并非完美,早期的Java是不支持泛型的,而为了能够在JavaSE 5中加入泛型且不会影响到之前版本的代码,不得不采取了一些折中措施,其中最重要和影响最深远的是类型擦除

我们看一个例子:

package ch13.type_param;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();
        System.out.println(list1.getClass() == list2.getClass());
        System.out.println(list1.getClass() == ArrayList.class);
    }
}
// true
// true

这个简单的示例说明了ArrayListArrayListClass对象相同,都是ArrayListClass对象。

再看一个例子:

package ch13.type_param2;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
    }
}
// [E]
// [K, V]

这里通过Class对象的getTypeParameters方法可以打印泛型类的“类型参数”,即定义泛型类时使用的各种类型标记字母。当然这些符号意义并不大,我们可能希望在程序运行时能够获取到泛型类实例的“真正的类型参数”,比如上边示例中list[String],以及map[String, Integer]

但这实际上是做不到的,因为类型擦除的存在。

在Java程序运行时,泛型类实例的内部实际上是不知道真实的类型的:

package ch13.type_param3;

class GenericTest<T> {
    private T obj;

    public GenericTest(T obj) {
        this.obj = obj;
    }

    public void f() {
        obj.f();
    }
}

class FCaller {
    public void f() {

    }
}

public class Main {
    public static void main(String[] args) {
        GenericTest<FCaller> gt = new GenericTest<>(new FCaller());
        gt.f();
    }
}
// The method f() is undefined for the type T

上边的代码无法通过编译,会提示类型T的f()方法没有被定义,但实际上我们使用具有f()方法的FCaller类作为GenericTest的类型参数,理论上以上的代码在运行时是合理的。

事实上类似的代码在C++中的确可以正常运行,因为C++具有完备的泛型机制,可以在运行时在泛型类内部确定类型参数的实际类型,但Java不行,在Java中的泛型类实例内部,会将真实的类型参数“擦除”成一个“边界类型”,对于上边的T来说,会被擦除成Object,也就是说在代码实际运行时,GenericTest实际上会按以下定义来创建:

class GenericTest<T> {
    private Object obj;

    public GenericTest(Object obj) {
        this.obj = obj;
    }

    public void f() {
        obj.f();
    }
}

也就是说属性obj实际上是一个Object类型的变量——无论泛型的参数类型是什么,而Object类型显然是没有f()方法的。

如果要让类似的代码可以在Java中正常运行,我们需要给泛型添加一个边界:

package ch13.type_param4;

class GenericTest<T extends FCaller> {
    private T obj;

    public GenericTest(T obj) {
        this.obj = obj;
    }

    public void f() {
        obj.f();
    }
}

class FCaller {
    public void f() {
        System.out.println("FCaller's f function is called.");
    }
}

public class Main {
    public static void main(String[] args) {
        GenericTest<FCaller> gt = new GenericTest<>(new FCaller());
        gt.f();
    }
}
// FCaller's f function is called.

通过T extends FCaller我们给类型参数T指定了一个“上界”FCaller,也就是说,GenericTest的类型参数T只能是某个继承自FCaller的子类型(也包括FCaller自己)。

此时,在运行时真实的类型参数不会再被擦除为Object,而是会被擦除为FCaller。根据李氏替换原则,FCaller的子类型都可以被当做FCaller对待,而FCaller拥有f()方法,所以obj现在可以正常调用f方法。

迁移兼容性

擦除并非是某种特性,而是为了照顾既有代码的一种不得已的折中方案。

考虑到在JavaSE 5之前已经有大量代码使用了将会被替换为泛型版本的容器类和其它标准类库,如果要实现完备的泛型机制,就可能要求使用相关类的客户端代码必须传入一个参数类型,以便泛型类在运行中使用,这就可能导致大量已有代码都需要修改。

因此,Java的维护团队采用了这种“类型擦除”的机制,巧妙的仅仅添加使用泛型类时传入数据时进行静态类型检查,获取数据时进行转型来实现泛型。这样做的好处是并不要求客户端代码必须指定一个类型参数,如果没有指定,就会按照JavaSE 5之前的无泛型版本对待,反正泛型类内部实际上会被擦除到Object(JavaSE5 之前不存在泛型边界的问题),也就是说泛型类内部实现是没有任何变化的,已有的客户端代码自然也无需任何改变。

擦除的问题

因为类型擦除的存在,在泛型类内部,类似T element这样的定义,实际运行中其实会是以Object element这样的形式创建,不清楚这点的开发人员很容易产生误解。

此外,因为前边所说的关系,无论是使用泛型类创建实例,还是对已有泛型类的继承,指定类型参数都并非必须:

package ch13.type_param5;

class GenericBase<T> {
    private T element;

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

    public T get() {
        return element;
    }
}

class GenericSub1<T> extends GenericBase<T> {
}

class GenericSub2 extends GenericBase {
}

public class Main {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        GenericSub2 gs = new GenericSub2();
        Object obj = gs.get();
        gs.set(obj);
    }
}

在这个例子中,GenericSub2继承GenericBase时使用了原始的非泛型版本,虽然这样做不影响代码的编译和执行,但是会产生一个warning。此外,在调用gs.set时同样会产生“应当使用泛型版本的GenericBase类”这样的warning,不过可以通过添加标签@suppressWarning来屏蔽。

边界处的动作

先看一个利用泛型创建数组的示例:

package ch13.edge;

import java.lang.reflect.Array;
import java.util.Arrays;

class ArrayMaker<T> {
    private Class<T> type;

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

    public T[] create(int length) {
        return (T[]) Array.newInstance(type, length);
    }
}

public class Main {
    public static void main(String[] args) {
        ArrayMaker<String> am = new ArrayMaker(String.class);
        String[] arr = am.create(5);
        System.out.println(Arrays.toString(arr));
        arr[0] = "hello";
        System.out.println(Arrays.toString(arr));
    }
}
// [null, null, null, null, null]
// [hello, null, null, null, null]

这个示例中,ArrayMaker泛型类接受一个Class类型的参数,并创建相应类型的数组。

利用泛型类的类型参数创建数组时,不能使用类似new T[]这样的方式,因为之前我们说过,在泛型类内部类型参数会被擦除到边界,类似的new T[]在代码实际运行时就会是new Object[],显然这不是我们希望看到的,所以必须利用反射包中的Array.newInstance方法创建数组。

虽然上边的代码可以正常执行,但实际上编译器会产生warning,在create方法中的转换语句,因为Array.newInstance方法返回的是一个Object,所以编译器会认为转换到T[]类型是不可靠的。

这里的深层次原因是外部传入的参数Class在实际运行时会被擦除为Class,虽然这并不影响Class对象依然是一个正确的String类型的Class对象,但对于Array.newInstance方法而言,可以利用反射正确地创建相应的数组,但是无法让创建好的数组直接变成String[]类型,因为对于Java这门语言,类型信息是和编译期密切相关的,而因为类型擦除的存在,编译期又无法确定具体类型。

再看一个类似的利用泛型创建List的例子:

package ch13.edge2;

import java.util.ArrayList;
import java.util.List;

class ListMaker<T> {

    public ListMaker() {
    }

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

}

public class Main {
    public static void main(String[] args) {
        ListMaker<String> lm = new ListMaker<>();
        List<String> list = lm.create();
        list.add("Hello");
        list.add("World");
        System.out.println(list);
    }
}
// [Hello, World]

利用泛型类创建List并不需要持有Class对象,且也不会产生任何warning

之所以会有这样的差异,是因为Java的编译器会在泛型类的“边界”上执行静态类型检查和类型转换的工作,以便让Java的泛型机制看上去和其它语言的泛型具有相似的功能。

这点可以通过反编译代码来确认。

下面展示两个持有对象的简单例子,一个是普通的持有Object对象的版本,另一个是泛型版本:

package ch13.edge3;

class NormalHolder{
    private Object content;

    public Object getContent() {
        return content;
    }

    public void setContent(Object content) {
        this.content = content;
    }

}

public class Main {
    public static void main(String[] args) {
        NormalHolder nh = new NormalHolder();
        nh.setContent("Hello World!");
        String msg = (String)nh.getContent();
        System.out.println(msg);
    }
}

对上边代码的字节码利用javap -c工具反编译,就能看到:

❯ javap -c .\Main.class
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Compiled from "Main.java"
public class ch13.edge3.Main {
  public ch13.edge3.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class ch13/edge3/NormalHolder
       3: dup
       4: invokespecial #9                  // Method ch13/edge3/NormalHolder."":()V
       7: astore_1
       8: aload_1
       9: ldc           #10                 // String Hello World!
      11: invokevirtual #12                 // Method ch13/edge3/NormalHolder.setContent:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #16                 // Method ch13/edge3/NormalHolder.getContent:()Ljava/lang/Object;
      18: checkcast     #20                 // class java/lang/String
      21: astore_2
      22: getstatic     #22                 // Field java/lang/System.out:Ljava/io/PrintStream;
      25: aload_2
      26: invokevirtual #28                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: return
}

其中18: checkcast #20这行字节码对应的就是源码中的String msg = (String)nh.getContent();强制类型转换语句。

package ch13.edge4;

class GenericHolder<T> {
    private T content;

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

}

public class Main {
    public static void main(String[] args) {
        GenericHolder<String> gh = new GenericHolder<>();
        gh.setContent("Hello World!");
        String msg = gh.getContent();
        System.out.println(msg);
    }
}

类似的,对泛型版本代码反编译:

❯ javap -c .\Main.class
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Compiled from "Main.java"
public class ch13.edge4.Main {
  public ch13.edge4.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class ch13/edge4/GenericHolder
       3: dup
       4: invokespecial #9                  // Method ch13/edge4/GenericHolder."":()V
       7: astore_1
       8: aload_1
       9: ldc           #10                 // String Hello World!
      11: invokevirtual #12                 // Method ch13/edge4/GenericHolder.setContent:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #16                 // Method ch13/edge4/GenericHolder.getContent:()Ljava/lang/Object;
      18: checkcast     #20                 // class java/lang/String
      21: astore_2
      22: getstatic     #22                 // Field java/lang/System.out:Ljava/io/PrintStream;
      25: aload_2
      26: invokevirtual #28                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: return
}

同样出现了18: checkcast #20这样的转型字节码命令。虽然泛型版本的源码中并没有显式的转型语句,但实际上在调用getContent获取返回值的时候,编译器会自动添加相应的转型字节码命令,以将擦除后的Object类型的content转型为实际类型,这个过程和之前的非泛型版本是没有区别的。

此外,泛型还会在调用setContent向泛型类传入参数时进行静态类型检查,这个好处是非泛型版本的代码不具备的。

擦除补偿

就像前边说的,因为存在类型擦除,所以在泛型类内部无法使用类型参数执行一些确切类型才能执行的操作,比如:

class GenericClass<T>{
    private final int SIZE = 10;
    public void f(T obj){
        // T instance = new T();
        // if(obj instanceof T){}
        // T[] arr = new T[SIZE];
        T[] arr2 = (T[])new Object[SIZE];
    }
}

上边的示例中被注释的代码均不能通过编译,这说明不能将类型参数用于:

  • 创建对象。
  • instanceof进行判断。
  • 创建数组。

虽然最后一行代码可以通过编译,但这样做依然只是实际上创建了Object类型的数组,就像之前展示的那样,在这种情况下应当传入一个Class对象,并使用反射包的Array.newInstance来创建数组。

如果你一定要在泛型类中实现类似的功能,可以通过Class对象来实现:

package ch13.compensation2;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;

class GenericClass<T> {
    private final int SIZE = 10;
    private Class<T> type;

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

    public void f(T obj) {
        T instance = null;
        try {
            instance = type.getDeclaredConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(e);
        }
        System.out.println(instance);
        if (type.isInstance(obj)) {
            System.out.println("true");
        }
        T[] arr = (T[]) Array.newInstance(type, SIZE);
        System.out.println(Arrays.toString(arr));
    }
}

public class Main {
    public static void main(String[] args) {
        GenericClass<String> gc = new GenericClass<>(String.class);
        gc.f("hello");
    }
}

// true
// [null, null, null, null, null, null, null, null, null, null]

这可以看做是对于类型擦除的某种“补偿”。

创建类型实例

虽然我们可以像上面介绍的那样利用Class对象创建类型参数的实例,但这样做就无法使用Java的静态类型检查(在编译期编译器无法知道将要使用的类型是否有正确的构造器)。

如果你想要在创建类型实例时依然能进行静态类型检查,可以使用工厂模式:

package ch13.create;

interface Factory<T> {
    public T create();
}

class GenericClass<T> {
    private T instance;

    public <F extends Factory<T>> GenericClass(F factory) {
        instance = factory.create();
    }

    public T getInstance() {
        return instance;
    }
}

class Student {
    public static class StudentFactory implements Factory<Student> {

        @Override
        public Student create() {
            return new Student();
        }

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

public class Main {
    public static void main(String[] args) {
        GenericClass<String> gc = new GenericClass<>(new Factory<String>() {

            @Override
            public String create() {
                return "hello";
            }

        });
        GenericClass<Student> gc2 = new GenericClass<>(new Student.StudentFactory());
        System.out.println(gc.getInstance());
        System.out.println(gc2.getInstance());
    }
}
// hello
// Student

还可以使用模版方法模式:

package ch13.create2;

abstract class GenericClass<T> {
    private T instance;

    public GenericClass() {
        instance = create();
    }

    abstract protected T create();

    public T getInstance() {
        return instance;
    }
}

class SubClass extends GenericClass<String> {

    @Override
    protected String create() {
        return "hello";
    }

}

public class Main {
    public static void main(String[] args) {
        SubClass sc = new SubClass();
        System.out.println(sc.getInstance());
    }
}
// hello

泛型数组

事实上,Java中的数组没有泛型版本,无法创建泛型类的数组:

package ch13.array;

class GenericClass<T>{}

public class Main {
    public static void main(String[] args) {
        // GenericClass[] arr = new GenericClass<>[10];
    }
}

被注释的代码无法通过编译。

解决方式也很简单:在所有需要使用泛型数组的地方用容器类进行替代:

package ch13.array2;

import java.util.ArrayList;
import java.util.List;

class GenericClass<T>{}

public class Main {
    public static void main(String[] args) {
        List<GenericClass<String>> arr = new ArrayList<>();
    }
}

但某些时候,你可能希望在泛型类内部使用类型参数定义的数组,比如标准库ArrayList中那样。这样做可能会产生一些问题:

package ch13.array3;

import java.util.Arrays;

class SimpleList<T> {
    private T[] array;

    public SimpleList(int size) {
        array = (T[])new Object[size];
    }

    public void set(int index, T value){
        array[index] = value;
    }

    public T get(int index){
        return array[index];
    }

    public T[] getArr(){
        return array;
    }
}

public class Main {
    public static void main(String[] args) {
        SimpleList<String> sl = new SimpleList(5);
        sl.set(1, "hello");
        System.out.println(sl.get(1));
        String[] strings = sl.getArr();
        System.out.println(Arrays.toString(strings));
    }
}
// hello
// Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')
//         at ch13.array3.Main.main(Main.java:30)

虽然SimpleList中的数组声明为T[]类型,但因为类型擦除的关系,实际上我们只能用Object[]类型的数组来实现,这也是为什么String[] strings = sl.getArr()会报错,因为并不能将Object[]类型的数组转化为String[]

报错本身没问题,但关键在于这种错误无法在编译时被检测到,而只能在运行时暴露,这无疑增加了排查问题的成本。

除了直接将泛型类内的数组声明为T[],还可以直接声明为Object[],并在获取元素时再进行转型:

package ch13.array4;

import java.util.Arrays;

class SimpleList<T> {
    private Object[] array;

    public SimpleList(int size) {
        array = new Object[size];
    }

    public void set(int index, T value){
        array[index] = value;
    }

    public T get(int index){
        return (T)array[index];
    }

    public T[] getArr(){
        return (T[])array;
    }
}
...

当然运行结果是相同的,都会在运行时报错,但好处在于这里很明确,在泛型类内部array的真实类型就是Object[],而不会被误以为是T[]

就像前边说的,如果要创建某个真实类型的数组,而非Object[],就需要使用Class对象:

package ch13.array5;

import java.lang.reflect.Array;
import java.util.Arrays;

class SimpleList<T> {
    private T[] array;

    public SimpleList(Class<T> type, int size) {
        array = (T[]) Array.newInstance(type, size);
    }

    public void set(int index, T value) {
        array[index] = value;
    }

    public T get(int index) {
        return array[index];
    }

    public T[] getArr() {
        return array;
    }
}

public class Main {
    public static void main(String[] args) {
        SimpleList<String> sl = new SimpleList(String.class, 5);
        sl.set(1, "hello");
        System.out.println(sl.get(1));
        String[] strings = sl.getArr();
        System.out.println(Arrays.toString(strings));
    }
}
// hello
// [null, hello, null, null, null]

类型边界

我们可以通过extends关键字和super关键字给泛型的类型参数指定一个边界。

package ch13.type_edge;

class Fruit {
}

class Apple extends Fruit {
}

class Orange extends Fruit {
}

class GenericClass<T extends Fruit> {
    private T obj;

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

    public T get() {
        return obj;
    }
}

public class Main {
    public static void main(String[] args) {
        GenericClass<Apple> gc = new GenericClass<>();
        gc.set(new Apple());
        System.out.println(gc.get());
    }
}

需要注意的是,这里的T extends Fruit与OOP中的继承含义是截然不同的,虽然看起来很相似,但这里的含义是类型参数T是一个Fruit的导出类型,换言之,我们通过T extends Fruit语句给类型参数T加了一个“上界”。

在这种情况下,对于泛型类GenericClass而言,类型擦除将会停止到Fruit而非Object

这是有意义的,就像前面所说,普通情况下因为类型擦除的存在,只能在泛型类中对于类型参数定义的对象调用Object拥有的方法,但是如果给类型参数添加了上界,就可以调用相应边界的方法:

package ch13.type_edge2;

abstract class Fruit {
    abstract public void eat();
}

class Apple extends Fruit {
    @Override
    public void eat() {
        System.out.println("eating apple...");
    }
}

class Orange extends Fruit {
    @Override
    public void eat() {
        System.out.println("eating orange...");
    }
}

class GenericClass<T extends Fruit> {
    private T obj;

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

    public T get() {
        return obj;
    }

    public void eat() {
        obj.eat();
    }
}

public class Main {
    public static void main(String[] args) {
        GenericClass<Apple> gc = new GenericClass<>();
        gc.set(new Apple());
        System.out.println(gc.get());
        gc.eat();
    }
}
// ch13.type_edge2.Apple@372f7a8d
// eating apple...

但同样的,因为我们给GenericClass的类型参数添加了这种限制,其也就只能应用于Fruit及其子类型,将无法用于其他类型,这同样缩小了泛型的适用范围。

给类型参数添加的上界并不一定是一个类,接口也是可以的:

package ch13.type_edge3;

interface EatAble {
    public void eat();
}

class GenericClass<T extends EatAble> {
    private T obj;

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

    public T get() {
        return obj;
    }

    public void eat() {
        obj.eat();
    }
}

public class Main {
    public static void main(String[] args) {
        GenericClass<EatAble> gc = new GenericClass<>();
        gc.set(new EatAble() {
            @Override
            public void eat() {
                System.out.println("eating someting...");
            }
        });
        System.out.println(gc.get());
        gc.eat();
    }
}
// ch13.type_edge3.Main$1@372f7a8d
// eating someting...

这里指定的边界是一个接口EatAble,其余代码几乎没有 改变,在main方法中调用gc.set时传入的是一个用实现了EatAble接口的局部匿名类创建的对象(这点从输出的对象的toString内容也可以看出)。

更为特殊的是,还可以同时用类和接口来指定边界:

package ch13.type_edge4;

interface EatAble {
    public void eat();
}

class Fruit {
}

class Apple extends Fruit {
}

class Orange extends Fruit implements EatAble {
    @Override
    public void eat() {
        System.out.println("eating orange...");
    }
}

class GenericClass<T extends Fruit & EatAble> {
    private T obj;

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

    public T get() {
        return obj;
    }

    public void eat() {
        obj.eat();
    }
}

public class Main {
    public static void main(String[] args) {
        GenericClass<Orange> gc = new GenericClass<>();
        // GenericClass gc2 = new GenericClass<>();
        // Bound mismatch: The type Apple is not a valid substitute for the bounded
        // parameter  of the type GenericClass
    }
}

在这个例子中,通过T extends Fruit & EatAble,限定了T类型是一个Fruit的子类,且实现了EatAble接口。main方法中的测试结果也说明了这点,GenericClass gc2 = new GenericClass<>();无法通过编译,因为Apple类虽然是Fruit的子类,但是并没有实现EatAble接口,但是GenericClass gc就可以。

需要注意的是Fruit & EatAble的顺序是有意义的,必须是类在前接口在后,且只能有一个类(原因是Java不支持多继承),否则无法通过编译。

当然也可以指定多个接口,比如:

package ch13.type_edge5;

interface EatAble {
    public void eat();
}

interface JuicingAble {
    public void juicing();
}

class Fruit {
}

class Apple extends Fruit {
}

class Orange extends Fruit implements EatAble, JuicingAble {
    @Override
    public void eat() {
        System.out.println("eating orange...");
    }

    public void juicing() {
        System.out.println("juicing orange...");
    }
}

class GenericClass<T extends Fruit & EatAble & JuicingAble> {

    private T obj;

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

    public T get() {
        return obj;
    }

    public void eat() {
        obj.eat();
    }

    public void juicing() {
        obj.juicing();
    }
}

public class Main {
    public static void main(String[] args) {
        GenericClass<Orange> gc = new GenericClass<>();
        gc.set(new Orange());
        gc.eat();
        gc.juicing();
    }
}
// eating orange...
// juicing orange...

还可以更复杂一点,在继承层次中使用类型边界:

package ch13.type_edge6;

interface EatAble {
    public void eat();
}

interface JuicingAble {
    public void juicing();
}

class Fruit {
}

class Apple extends Fruit {
}

class Orange extends Fruit implements EatAble, JuicingAble {
    @Override
    public void eat() {
        System.out.println("eating orange...");
    }

    public void juicing() {
        System.out.println("juicing orange...");
    }
}

class GenericClass<T> {

    private T obj;

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

    public T get() {
        return obj;
    }
}

class EatAbleFruit<T extends Fruit & EatAble> extends GenericClass<T> {
    public void eat() {
        get().eat();
    }
}

class JuicingEatAbleFruit<T extends Fruit & EatAble & JuicingAble> extends EatAbleFruit<T> {
    public void juicing(){
        get().juicing();
    }
}

public class Main {
    public static void main(String[] args) {
        JuicingEatAbleFruit<Orange> jef = new JuicingEatAbleFruit<>();
        jef.set(new Orange());
        jef.eat();
        jef.juicing();
    }
}
// eating orange...
// juicing orange...

在上边这个示例中,在GenericClass这个比较通用的泛型类基础上,通过继承并且限定泛型参数边界,分别构建了JuicingEatAbleFruitEatAbleFruit这两个特定用途的泛型类。

这样做的好处在于可以充分利用已有的通用泛型类的代码,避免了代码重用。

通配符

在详细说明泛型中的通配符?的作用前,先看一个比较奇怪的例子:

package ch13.wildcard;

class Fruit {
}

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Fruit();
        // Exception in thread "main" java.lang.ArrayStoreException: ch13.wildcard.Fruit
        // at ch13.wildcard.Main.main(Main.java:9)
    }
}

类似Fruit f = new Apple();这样的“向上转型”在Java中是非常常见的,但如果类比到数组上,就会出现一些奇怪的问题。比如上边的Fruit[] fruits = new Apple[10];,看似是可行的,但如果仔细思考,即使AppleFruit的子类,但Apple[]并不能完全当做Fruit[]来使用,下面fruits[0] = new Fruit();会在运行时报错也说明了这一点。

事实上,Fruit[] fruits = new Apple[10]这种转型是违反李氏替换原则的,但遗憾的是这种错误的写法可以通过编译期的静态检查。

当然,你也不用太过担心,至少这种错误会在运行时通过异常的形式报告。考虑到泛型的用途之一就是将某些类型问题提前到编译期发现,所以很容易想到用泛型解决类似的问题是不是更好一些:

package ch13.wildcard2;

import java.util.ArrayList;
import java.util.List;

class Fruit {
}

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        // List fruits = new ArrayList();
        // Type mismatch: cannot convert from ArrayList to List
    }
}

事情的确如我们期望的那样,类似的List fruits = new ArrayList()写法根本无法通过编译检查。

但是某些时候我们可能希望用一个“更宽泛”的类型参数来取代一个具体的类型参数,这时候就需要使用到通配符:

package ch13.wildcard3;

import java.util.ArrayList;
import java.util.List;

class Fruit {
}

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        List<? extends Fruit> fruits = new ArrayList<Apple>();
        // fruits.add(new Fruit());
        // The method add(capture#1-of ? extends Fruit) in the type List
        // extends Fruit> is not applicable for the arguments (Fruit)
        // fruits.add(new Apple());
        // The method add(capture#2-of ? extends Fruit) in the type List
        // extends Fruit> is not applicable for the arguments (Apple)
        fruits.add(null);
        Fruit f = fruits.get(0);
    }
}

可能相当一部分人刚看到List这样的写法时会和我一样想当然地认为这是一个以Fruit子类组成的List,但经过系统性的学习泛型后你应当明白,其真实含义是List的类型参数是某个继承自Fruit的子类,尤其需要注意,是某个,而不是某些,在运行时一个泛型类实例只能具备一个类型的类型参数。也就是说,List实例的真实类型参数可能是Fruit,也可能是Apple,或者是其它的任何继承自Fruit的类型,但同一时刻只能是其中之一,理解这点非常重要。

现在回头来看示例中运行会报错的语句:

fruits.add(new Fruit());
fruits.add(new Apple());

第一行报错很容易理解,因为实际上fruits依然是一个ArrayList实例,显然是没法添加Fruit类型的实例的。但第二行就很费解了,理论上ArrayList当然可以添加Apple实例。

但是在Java编译器看来,List fruits这样的声明,将fruits限定为了一个“具备某个Fruit子类作为类型参数的List”,具体是哪个子类,当然在编译期是不知情的。而Java编译器的做法是,将其当做“某个子类型”来看待,不考虑具体类型。

这就导致一个后果,即如果真实类型是Apple,那么fruits.add(new Apple())是合法的,但如果真实类型是Orange,必然是非法的。而编译器无法在编译期知道真实类型,最终的效果就是——不允许任何类型的实例进行添加,除了null

上面这段非常难以理解,建议多思考几次。

当然,Fruit f = fruits.get(0);是合法的,因为虽然我们不知道类型参数具体是哪个类型,但我们知道其必然是Fruit的子类型,当然可以将其实例用Fruit句柄承接。

但是,并非所有需要传入参数的泛型类的方法都会在此类情况下拒绝接收null以外的参数,比如:

package ch13.wildcard4;

class GenericClass<T> {
    private T obj;

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

    public T get() {
        return obj;
    }

    @Override
    public boolean equals(Object other) {
        if (obj == null) {
            if (other == null) {
                return true;
            } else {
                return false;
            }
        }
        return obj.equals(other);
    }
}

class Fruit {
};

class Apple extends Fruit {
};

public class Main {
    public static void main(String[] args) {
        GenericClass<? extends Fruit> gc = new GenericClass<Apple>();
        // gc.set(new Apple());
        System.out.println(gc.equals(new Apple()));
    }
}

同样是属于泛型类并接收参数的方法equals,就可以用使用通配符的句柄进行调用,这是因为其参数类型并非类型参数T,而是Object,自然就不会陷入之前所说的编译期静态检查问题。

事实上标准库的泛型类也同时使用这两种方式定义方法,比如:

public interface List<E> extends Collection<E> {
	...
	boolean add(E e);
	boolean contains(Object o);
	boolean remove(Object o);
	...
}

很明显,对于List类型的句柄来说,可以调用其containsremove等使用Object类型作为参数的方法,但是无法调用add这类使用类型参数E作为方法参数的方法。

这样定义泛型类的方法是有意义的,比如通过add向容器中添加元素的时候,我们必须确保其类型正确,因此需要类型检查,就要使用类型参数E。但是判断容器中是否有某个元素,或者要从容器中移除某个元素,其实并不需要严格限制传入的参数类型,因为Objectequals方法就可以用来实现不同实例是否相等的检测。

这样的实现方式放宽了泛型类的使用场景,让使用了通配符的泛型类句柄依然可以调用某些方法。

逆变

如果将List fruits = new ArrayList();这样的转型语句看做是某种程度的“协变”,那么List objs = new ArrayList();这样的转型就可以看做是“逆变”(或者反协变),其效果也与协变正好相反。

package ch13.wildcard5;

class GenericClass<T>{
    private T obj;
    public GenericClass(T obj){
        this.obj = obj;
    }
    public void set(T obj){
        this.obj = obj;
    }
    public T get(){
        return this.obj;
    }
}

class Fruit{}
class Apple extends Fruit{}
class RedApple extends Apple{}

public class Main {
    public static void main(String[] args) {
        GenericClass<? super Apple> gc = new GenericClass<Fruit>(new Fruit());
        gc.set(new Apple());
        gc.set(new RedApple());
        // gc.set(new Fruit());
        // Apple apple = gc.get();
        Object obj = gc.get();
    }
}

在前边类型参数“协变”的示例中,我们不能轻易向方法中传入参数,但可以很容易获取类型参数返回值。但逆变的效果刚好相反。

GenericClass gc意味着gc是一个用某个Apple的父类作为类型参数的GenericClass实例,这就意味着其set方法可以接收Apple及其子类的对象,而不能接收其它类型的对象。就像上面示例中的那样,gc真实类型为new GenericClass,但同样不能调用gc.set(new Fruit())

同样的,如果我们调用某个以类型参数作为返回值的方法,比如上边的get。因为此时我们只能确定当前类型参数是某个Apple的父类,有可能是AppleFruit,甚至是Object,我们不能确定。所以唯一能用来承接的句柄类型就是Object,而不能是其它类型。

利用这种特性我们可以编写更灵活的泛型代码,比如:

package ch13.wildcard6;

import java.util.ArrayList;
import java.util.List;

class Fruit {
}

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<>();
        List<Apple> apples = new ArrayList<>();
        addItem2List(fruits, new Fruit());
        addItem2List(apples, new Apple());
        addItem2List(fruits, new Apple());
        System.out.println(fruits);
    }

    private static <T> void addItem2List(List<T> list, T item) {
        list.add(item);
    }
}

比较令人惊奇的是addItem2List(fruits, new Apple())这行代码可以正常执行,理论上泛型方法addItem2List此时的两个参数类型不一致,应当无法通过编译才对(Java编程思想的类似示例的确显示JavaSE 5版本是会报错的),但实际上在当前的Java版本中是没有问题的。

我查阅了相关官方文档,遗憾的是并没有找到我想要的内容,不过我猜测是泛型方法的类型推断机制得到优化的结果,这点可以通过为泛型方法显式指定类型参数来证实:

package ch13.wildcard7;

import java.util.ArrayList;
import java.util.List;

class Fruit {
}

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<>();
        List<Apple> apples = new ArrayList<>();
        Main.<Fruit>addItem2List(fruits, new Fruit());
        Main.<Apple>addItem2List(apples, new Apple());
        // Main.addItem2List(fruits, new Apple());
        // The parameterized method addItem2List(List, Apple) of type Main
        // is not applicable for the arguments (List, Apple)
        Main.<Fruit>addItem2List(fruits, new Apple());
        System.out.println(fruits);
    }

    private static <T> void addItem2List(List<T> list, T item) {
        list.add(item);
    }
}

addItem2List(fruits, new Apple())被调用时,如果使用Fruit作为类型参数,就没有问题(Apple实例可以作为Fruit类型的参数传递),但如果使用Apple作为类型参数就会出错。

同时我也尝试了将两个参数调换位置,结果没有任何影响,这说明目前的泛型方法的类型推断并不是按照参数顺序来确定真实类型。

虽然因为上边的缘故,逆变已经在类似的情况下已经变得很没有必要,但是至少可以说明一些运行机制:

package ch13.wildcard8;

import java.util.ArrayList;
import java.util.List;

class Fruit {
}

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        List<Fruit> fruits = new ArrayList<>();
        List<Apple> apples = new ArrayList<>();
        Main.<Fruit>addItem2List(fruits, new Fruit());
        Main.<Apple>addItem2List(apples, new Apple());
        Main.<Apple>addItem2List(fruits, new Apple());
        Main.<Fruit>addItem2List(fruits, new Apple());
        System.out.println(fruits);
    }

    private static <T> void addItem2List(List<? super T> list, T item) {
        list.add(item);
    }
}

类似的,利用协变也可以增加代码的灵活性:

package ch13.wildcard9;

import java.util.Arrays;
import java.util.List;

class FirstItemGetter<T> {
    public T get(List<T> list) {
        return list.get(0);
    }
}

class Fruit {
};

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        List<Fruit> fruits = Arrays.asList(new Fruit());
        List<Apple> apples = Arrays.asList(new Apple());
        FirstItemGetter<Fruit> fig = new FirstItemGetter<>();
        Fruit f = fig.get(fruits);
        System.out.println(f);
        // Fruit f2 = fig.get(apples);
        // The method get(List) in the type FirstItemGetter is not
        // applicable for the arguments (List)
        // System.out.println(f2);
    }
}

这个示例中,无法进行Fruit f2 = fig.get(apples)这样的调用,因为get只能接收List类型的参数,如果使用协变改写:

package ch13.wildcard10;

import java.util.Arrays;
import java.util.List;

class FirstItemGetter<T> {
    public T get(List<? extends T> list) {
        return list.get(0);
    }
}

class Fruit {
};

class Apple extends Fruit {
}

public class Main {
    public static void main(String[] args) {
        List<Fruit> fruits = Arrays.asList(new Fruit());
        List<Apple> apples = Arrays.asList(new Apple());
        FirstItemGetter<Fruit> fig = new FirstItemGetter<>();
        Fruit f = fig.get(fruits);
        System.out.println(f);
        Fruit f2 = fig.get(apples);
        System.out.println(f2);
    }
}

这里的关键在于协变后的泛型实例可以返回具体类型的返回值。

无界通配符

使用了无界通配符的泛型类List,看起了和原始版本的类List没有区别,事实上因为类型擦除的存在,在运行时它们的确没有区别,内部实际上的类型使用的都是Object(这也是类型擦除存在的价值和意义)。但是,在编译期和含义上它们还是有一些区别的:

  • List表示这是一个使用了某个类型作为元素类型的List
  • List表示这是一个使用了Object作为元素类型的List,相当于List

    示例:

    package ch13.wildcard12;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
            List list = numbers;
            list.add(Integer.valueOf(6));
            System.out.println(list.get(0));
            List<?> list2 = numbers;
            // list2.add(Integer.valueOf("6"));
            // The method add(capture#13-of ?) in the type List is not
            // applicable for the arguments (Integer)
            System.out.println(list.get(0));
        }
    }
    

    原生的List句柄是可以用于传入参数和获取返回值的(虽然这样会产生一些warming),但List不可以传入参数,其原因与之前讨论协变和反协变类似,都是因为编译器无法在编译期确定具体类型,所以只能是拒绝类似的语句。

    捕获转换

    关于捕获转换,《Java编程思想》举了一个类似这样的例子:

    package ch13.wildcard13;
    
    public class Main {
        public static void main(String[] args) {
            SimpleHolder holder = new SimpleHolder<String>("hello");
            // printContentType(holder);
            printContentType2(holder);
        }
    
        private static <T> void printContentType(SimpleHolder<T> holder) {
            T content = holder.get();
            System.out.println(content.getClass().getSimpleName());
        }
    
        private static void printContentType2(SimpleHolder<?> holder) {
            printContentType(holder);
        }
    }
    

    这个示例中,因为holder对象被声明为了一个原始类型SimpleHolder,也就是说丢失了具体的类型参数,如果使用printContentType方法调用,就会产生一个warming。但如果调用printContentType2方法,因为其参数是SimpleHolder,是一个无界通配符,所以不会产生warming,且因为无界通配符内含“包含了某种类型”这层意思,可以调用printContentType而不产生warming,这样就达成了某种形式上的“曲线救国”。

    《Java编程思想》将上面这种无界通配符的用途称作“捕获转换”,但我个人觉得这种特性难以理解,且用处不大,并不值得花费额外时间和精力去学习。

    使用“捕获转换”仅仅会减少相应的warming,对程序运行结果没有任何影响。

    《Java编程思想》关于泛型的这个章节内容多且庞杂,剩余的部分就在下一篇笔记中总结吧。

    最近个人有一些其它的事情,所以这篇笔记断断续续写了几个星期才发出来,谢谢阅读。

    你可能感兴趣的:(JAVA,java,容器,开发语言,泛型,类型参数)