您的位置:

Java泛型指南

泛型是Java的一个重要特性,它使得我们可以在编译期间发现类型错误,避免了在运行期间出现的类型转换错误。除此之外,泛型还可以提高代码的重用性和可读性。在这篇文章中,我们将会详细的讲解Java中的泛型,希望能帮助读者更好的理解泛型的概念和使用。

一、表示器和类型擦除

Java中的泛型实际上是基于类型擦除(type erasure)实现的。在编译期间,泛型实例会被转换成无泛型形式,这就是所谓的类型擦除。

例如,下面的代码演示了泛型的使用:

public class Box<T> {

    private T t;

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

    public T get() {
        return t;
    }
}

上述代码中,Box类使用了一个泛型类型参数T,因此我们可以创建一个Box对象,并指定它的类型参数为String:

Box<String> box = new Box<>();

当我们在编译期间编译Box类时,类型擦除会将上述代码转换为以下代码:

public class Box {

    private Object t;

    public void set(Object t) {
        this.t = t;
    }

    public Object get() {
        return t;
    }
}

可以看到,泛型类型参数T被转换成了Object类型,这就是Java中泛型类型的类型擦除机制。

二、通配符和边界

Java中的通配符(wildcard)与边界(bounded)可以用来限定泛型类型参数的范围。通配符有两种形式:

  • ? extends T:表示类型是T的子类或T本身
  • ? super T:表示类型是T的超类或T本身

对于上述两种通配符,我们可以通过以下示例代码来理解:

public class Container<T> {

    private List<T> list = new ArrayList<>();

    public void addAll(Collection<? extends T> c) {
        list.addAll(c);
    }

    public void add(T t) {
        list.add(t);
    }

    public void remove(T t) {
        list.remove(t);
    }

    public List<T> getList() {
        return list;
    }

    public static void main(String[] args) {
        Container<Number> container = new Container<>();
        List<Integer> integers = Arrays.asList(1, 2, 3);
        container.addAll(integers);
        System.out.println(container.getList()); // [1, 2, 3]
    }
}

上述Container类中,addAll方法使用了一个通配符参数c,并限定它是T的子类或T本身(也就是Number的子类或Number本身)。这样我们就可以使用addAll方法添加Integer类型的元素到Container对象中。如果我们将addAll方法的参数定义为Collection<T>,那么就不能够添加Integer类型的元素到Container对象中。

另外,我们还可以使用边界限定泛型类型参数的范围,例如:

public class MinMax<T extends Comparable<T>> {

    private T min;
    private T max;

    public MinMax(T[] array) {
        if (array.length == 0) {
            throw new IllegalArgumentException("array is empty");
        }
        min = max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i].compareTo(min) < 0) {
                min = array[i];
            }
            if (array[i].compareTo(max) > 0) {
                max = array[i];
            }
        }
    }

    public T getMin() {
        return min;
    }

    public T getMax() {
        return max;
    }

    public static void main(String[] args) {
        Integer[] integers = new Integer[]{1, 2, 3, 4};
        MinMax<Integer> minMax = new MinMax<>(integers);
        System.out.println(minMax.getMin()); // 1
        System.out.println(minMax.getMax()); // 4
    }
}

上述例子中,MinMax类使用了一个泛型类型参数T,并限定它是Comparable<T>的子类或T本身。这样我们就可以确保MinMax类的实例只会被创建用于该类型具有比较能力的类的实例。

三、类型转换和擦除

Java的泛型机制在类型擦除的过程中丢失了很多类型信息,这会导致类型转换与擦除相关的问题。在探讨这些问题之前,我们需要先了解虚拟机的类型擦除方式。

在虚拟机中,泛型类型参数T会被转换为Object类型,同时,在代码中隐含加入一些类型转换操作,使得类型转换过程不会抛出ClassCastException异常。

例如,在Java中,我们可以使用反射实现泛型数组的创建,例如:

public static <T> T[] newArray(Class<T> clazz, int length) {
    return (T[]) Array.newInstance(clazz, length);
}

public static void main(String[] args) {
    Integer[] integers = newArray(Integer.class, 10);
    System.out.println(integers.length); // 10
}

上述newArray方法使用了一个Class类型参数来表示数组的类型,同时在返回值中强制类型转换为T[]类型。

然而,如果我们使用了通配符或者边界,就需要注意类型擦除对类型转换的影响。例如:

public static <T> void copy(List<? extends T> src, List<? super T> dest) {
    for (T t : src) {
        dest.add(t);
    }
}

public static void main(String[] args) {
    List<Number> numbers = new ArrayList<>();
    List<Integer> integers = Arrays.asList(1, 2, 3);
    copy(integers, numbers);
    System.out.println(numbers); // [1, 2, 3]
}

上述copy方法使用了两个通配符参数,在方法内部将src中的元素添加到dest中。在这个过程中,我们使用了被定义为? super T的dest参数,这样可以确保dest中的元素是T类型的超类。如果我们使用了? extends T的dest参数,那么就不能正确的执行类型转换操作,因为我们不能保证dest的元素类型是T的子类。

四、泛型和遗留代码

在Java中,我们有时需要与一些遗留的代码进行交互,这些代码并没有使用泛型类型参数。在这种情况下,我们可以使用原始类型(raw type)来代替泛型类型,例如:

public class LegacyLibrary {

    public void add(List list, Object o) {
        list.add(o);
    }
}

public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    LegacyLibrary legacyLibrary = new LegacyLibrary();
    legacyLibrary.add(strings, "hello");
    String s = strings.get(0);
    System.out.println(s); // hello
}

上述例子中,我们无法修改LegacyLibrary类的源代码,因此只能使用原始类型来与该类进行交互,这样就可以将字符串添加到一个泛型List对象中。不过我们需要注意到,这样做是有潜在风险的,因为我们无法在编译期间发现类型错误,只能在运行期间发现该问题,这可能导致一些Bug的产生。

如果我们使用了SuppressWarnings("unchecked")注解,就可以压制编译器发出的警告信息。例如:

@SuppressWarnings("unchecked")
public static void add(List list, Object o) {
    list.add(o);
}

public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    add(strings, "hello");
    String s = strings.get(0);
    System.out.println(s); // hello
}

上述add方法使用了SuppressWarnings("unchecked")注解,这样就可以避免编译器发出的"unchecked"警告信息。但是在使用该注解之前,我们需要确保代码不会导致类型错误或者运行期间出现ClassCastException异常。

五、总结

在Java中使用泛型可以提高代码的可读性,安全性和重用性,同时可以加速编程的速度。在本文中,我们对Java中的泛型机制进行了详细的探讨,包括通配符和边界、类型转换和擦除以及泛型和遗留代码等内容。

希望读者能够通过本文,更好的理解Java中的泛型机制,避免出现类型转换错误和其他一些常见的Bug。