Java泛型通配符总结

今天看了两篇博客,感觉很有收获,是关于java通配符的,感觉写的很不错

本文参考了 Java泛型详解 和 Java 之泛型通配符 ? extends T 与 ? super T 解惑

泛型基础

java泛型擦除的概念,感觉大家都熟悉,首先, 说到 Java 的泛型, 我们必须要提到的是Java 泛型的类型擦除机制: Java中的泛型基本上都是在编译器这个层次来实现的. 在生成的 Java 字节代码中是不包含泛型中的类型信息的。所以感觉这个<>符号更多是个表面,底层系统压根不知道这个<>符号存在的意义。使用泛型的时候加上的类型参数, 会被编译器在编译的时候去掉, 这个过程就称为类型擦除。

如在代码中定义的List和List等类型, 在编译之后都会变成List, JVM看到的只是List, 而由泛型附加的类型信息对JVM来说是不可见的。

至于为什么java需要这样实现java 泛型,这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。对于这一点,如果阅读Java集合框架的源码,可以发现有些类其实并不支持泛型

是什么

在使用泛型类时, 我们可以使用一个具体的类型, 例如可以定义一个 List 的对象, 我们的泛型参数就是 Integer; 我们也可以使用通配符 ? 来表示一个未知类型, 例如 List 就表示了泛型参数是某个类型, 只不过我们并不知道它的具体类型时什么。

那么如何使用这个List呢?

List所声明的就是所有类型都是可以的, 但需要注意的是, List并不等同于List. 对于 List 来说, 它实际上确定了 List 中包含的是 Object 及其子类, 我们可以使用 Object 类型来接收它的元素. 相对地, List 则表示其中所包含的元素类型是不确定, 其中可能包含的是 String, 也可能是 Integer. 如果它包含了 String 的话, 往里面添加 Integer 类型的元素就是错误的 作为对比, 我们可以给一个 List 添加 String 元素, 也可以添加 Integer 类型的元素, 因为它们都是 Object 的子类

我们可以做的再绝一点

    	List<?> list = new ArrayList<E>();
		list.add(new Object()); //报错,哦吼,List连 Object都不让放!

正因为类型未知, 我们就不能通过 new ArrayList() 的方法来创建一个新的ArrayList 对象, 因为编译器无法知道具体的类型是什么. 但是对于 List 中的元素, 我们却都可以使用 Object 来接收, 因为虽然类型未知, 但肯定是Object及其子类。这么看来,还可以取出操作,只能执行Object类型元素!!

边界符

现在我们要实现这样一个功能,查找一个泛型数组中大于某个特定元素的个数,我们可以这样实现:

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

但是这样很明显是错误的,因为除了short, int, double, long, float, byte, char等原始类型,其他的类并不一定能使用操作符>,所以编译器报错,那怎么解决这个问题呢?答案是使用边界符。

public interface Comparable {
public int compareTo(T o);
}
做一个类似于下面这样的声明,这样就等于告诉编译器类型参数T代表的都是实现了Comparable接口的类,这样等于告诉编译器它们都至少实现了compareTo方法。

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

然后进行擦除就会出现类似下面的代码,也就是说类型T会被擦除为Comparable

public static  Comparable int countGreaterThan(Comparable[] anArray, Comparable elem) {
    int count = 0;
    for (Comparable e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

PECS原则

? extends T

? extends T 描述了通配符上界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的子类, 例如:

List<? extends Number> numberArray = new ArrayList<Number>();  // Number 是 Number 类型的
List<? extends Number> numberArray = new ArrayList<Integer>(); // Integer 是 Number 的子类
List<? extends Number> numberArray = new ArrayList<Double>();  // Double 是 Number 的子类

上面三个操作都是合法的, 因为 ? extends Number 规定了泛型通配符的上界, 即我们实际上的泛型必须要是 Number 类型或者是它的子类, 而 Number, Integer, Double 显然都是 Number 的子类(类型相同的也可以, 即这里我们可以认为 Number 是 Number 的子类).

关于读取

根据上面的例子, 对于 List numberArray 对象:

我们能够从 numberArray 中读取到 Number 对象, 因为 numberArray 中包含的元素是 Number 类型或 Number 的子类型.

我们不能从 numberArray 中读取到 Integer 类型, 因为 numberArray 中可能保存的是 Double 类型.

同理, 我们也不能从 numberArray 中读取到 Double 类型.

关于写入

根据上面的例子, 对于 List numberArray 对象:

我们不能添加 Number 到 numberArray 中, 因为 numberArray 有可能是List 类型

我们不能添加 Integer 到 numberArray 中, 因为 numberArray 有可能是 List 类型

我们不能添加 Double 到 numberArray 中, 因为 numberArray 有可能是 List 类型

即, 我们不能添加任何对象到 List 中, 因为我们不能确定一个 List 对象实际的类型是什么, 因此就不能确定插入的元素的类型是否和这个 List 匹配. List 唯一能保证的是我们从这个 list 中读取的元素一定是一个 T 类型的

? super T

? super T 描述了通配符下界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的父类, 例如:

// 在这里, Integer 可以认为是 Integer 的 “父类”
List array = new ArrayList();
// Number 是 Integer 的 父类
List array = new ArrayList();
// Object 是 Integer 的 父类
List array = new ArrayList();

关于读取

对于上面的例子中的 List array 对象:

我们不能保证可以从 array 对象中读取到 Integer 类型的数据, 因为 array 可能是 List 类型的.

我们不能保证可以从 array 对象中读取到 Number 类型的数据, 因为 array 可能是 List 类型的.


 
  

唯一能够保证的是, 我们可以从 array 中获取到一个 Object 对象的实例

关于写

对于上面的例子中的 List array 对象:

我们可以添加 Integer 对象到 array 中, 也可以添加 Integer 的子类对象到 array 中.

我们不能添加 Double/Number/Object 等不是 Integer 的子类的对象到 array 中.

容易混淆的地方

List<? super Integer> l1 = ...
List<? extends Integer> l2 = ..

? super Integer 和 ? extends Integer 限制的其实是 泛型参数, 即 List l1 表示 l1 的泛型参数 T 必须要满足 T 是 Integer 的父类, 因此诸如 List, List

PECE 原则: Producer Extends, Consumer Super

Producer extends: 如果我们需要一个 List 提供类型为 T 的数据(即希望从 List 中读取 T 类型的数据), 那么我们需要使用 ? extends T, 例如 List. 但是我们不能向这个 List 添加数据.

 类型一个[Number,?)的集合范围,泛型必须落在这个范围内,如果泛型实为Integer,
那么Double的数据肯定无法插入到 List,因为Double也不是Integer子类,无法被看作为Intger对象,所以插入受限!

反之,集合中的数据必然是Number类型或其子类,自然我们可以把集合中数据都当成Number类型去取出数据。

Consumer Super: 如果我们需要一个 List 来消费 T 类型的数据(即希望将 T 类型的数据写入 List 中), 那么我们需要使用 ? super T, 例如 List. 但是这个 List 不能保证从它读取的数据的类型.

 类型一个(?,Integer]的集合范围,泛型必须落在这个范围内,那么Integer及其子类都可以被看作为
泛型的子类型,插入到集合中

但是取出的时候遭罪了 ,不清楚泛型具体是什么类型,可能是List也可能List,
不知道用什么类型来接取出的数据,所以只能取出object类型

 
  

如果我们既希望读取, 也希望写入, 那么我们就必须明确地声明泛型参数的类型, 例如 List.

泛型的问题总结

下面这段基本都是摘抄自 Java泛型详解,我觉得写得非常好,所以粘贴过来

public class Node<T> {

    private T data;
    private Node<T> next;

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

    public T getData() { return data; }
    // ...
}

编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

这意味着不管我们声明Node还是Node,到了运行期间,JVM统统视为Node。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:

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

    private T data;
    private Node<T> next;

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

    public T getData() { return data; }
    // ...
}

这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题,有些问题在C++的泛型中可能不会遇见,但是在Java中却需要格外小心。

问题一

在Java中不允许创建泛型数组,类似下面这样的做法编译器会报错:


List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error

为什么编译器不支持上面这样的做法呢?继续使用逆向思维,我们站在编译器的角度来考虑这个问题。

我们先来看一下下面这个例子:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

对于上面这段代码还是很好理解,字符串数组不能存放整型元素,而且这样的错误往往要等到代码运行的时候才能发现,编译器是无法识别的。接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:

Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
// An ArrayStoreException should be thrown, but the runtime can't detect it.
stringLists[1] = new ArrayList<Integer>();

假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM实际上根本就不知道new ArrayList()和new ArrayList()的区别。类似这样的错误假如出现才实际的应用场景中,将非常难以察觉。

如果你对上面这一点还抱有怀疑的话,可以尝试运行下面这段代码:

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2); // true
    }
}

问题二

继续复用我们上面的Node的类,对于泛型代码,Java编译器实际上还会偷偷帮我们实现一个Bridge method。

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

看完上面的分析之后,你可能会认为在类型擦除后,编译器会将Node和MyNode变成下面这样:

public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

实际上不是这样的,我们先来看一下下面这段代码,这段代码运行的时候会抛出ClassCastException异常,提示String无法转换成Integer:


MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Causes a ClassCastException to be thrown.
// Integer x = mn.data;

如果按照我们上面生成的代码,运行到第3行的时候不应该报错(注意我注释掉了第4行),因为MyNode中不存在setData(String data)方法,所以只能调用父类Node的setData(Object data) 方法,既然这样上面的第3行代码不应该报错,因为String当然可以转换成Object了,那ClassCastException到底是怎么抛出的?

实际上Java编译器对上面代码自动还做了一个处理:

class MyNode extends Node {

    // Bridge method generated by the compiler
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

这也就是为什么上面会报错的原因了,setData((Integer) data);的时候String无法转换成Integer。所以上面第2行编译器提示unchecked warning的时候,我们不能选择忽略,不然要等到运行期间才能发现异常。如果我们一开始加上Node n = mn就好了,这样编译器就可以提前帮我们发现错误。

问题三

正如我们上面提到的,Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}
但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}
我们可以像下面这样调用:

List<String> ls = new ArrayList<>();
append(ls, String.class);

实际上对于上面这个问题,还可以采用Factory和Template两种设计模式解决,感兴趣的朋友不妨去看一下Thinking in Java中第15章中关于Creating instance of types(英文版第664页)的讲解,这里我们就不深入了。

问题四

我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息,正如我们上面验证过的JVM在运行时期无法识别出ArrayList和ArrayList的之间的区别:

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

=> { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }

和上面一样,我们可以使用通配符重新设置bounds来解决这个问题:

public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}
我还多试了一种
List<Integer> list = new ArrayList<>();
		 
if(list instanceof ArrayList<? extends Number>) { //也会报错,根据提示只能用ArrayList才不会报错
    			
}
		


你可能感兴趣的:(java基础)