理解泛型通配符

近日在学kotlin,学到泛型时一脸懵逼,当初学Java时就没有彻底搞懂泛型通配符,趁这个机会好好理解

错误的理解

从前我使用<? extends T>都是这么理解的。例如List<? extends Number>那么就代表这个List可以存放 Number的子类,Integer、Double等。
直到有一次想起来 Integer作为 Number的子类,按理也可以放入List<Number>之中。
那么List<? extends Number>到底有什么不同呢,至于List<? super T>我更是无法理解,不知所然。

先从原因说起

为何要有泛型通配符呢?实际上是为了方便类型转换,请看这个例子

List<String> a = new ArrayList<>();
List<Object> b = a;
b.add(7);

这个例子很好理解,a实际上是一个List<String>,自然无法放入一个 Integer。
所以为了防止这种错误的写法的出现,规定List<String>并不是List<Object>的子类,故无法把List<String>作为List<Object>使用
但这样会出现许多问题,比如 List有个 addAll方法,它是这样的

boolean addAll(Collection<E> c)  {
  //省略内容
}

它的功能是把一个集合中所有元素添加进另一个集合,但如果你试图这么做就会出错

List<Object> a = new ArrayList<>();
List<String> b = new ArrayList<>();
a.addAll(b);

但是由于方法需要一个List<Object>参数,而是b一个List<String>,还记得前面说的结论吗,List<String>不能当做使用List<Object>
然而我们很多时候都需要这种需求,因此通配符就出现了。

extends通配符

为了解决上述问题,出现了extends通配符

例如,List<? extends Object>表示的意思是,这个 List存放着 Object的子类。
这个 List可能是List<String>,也可能是List<Integer>,无论是 Integer,或是 String,都是 Object的子类。
因此,调用List<? extends Object>的get方法,能安全得到一个Object。
但由于我们并不知道这个 List是List<String>还是List<Integer>,因此往里面添加一个元素,可能就会出现问题。
所以声明为List<? extends Object>后,能安全的从中获取,但无法向里面添加内容。
没有了安全问题,这种语法也就允许了。

List<String> a = new ArrayList<>();
List<? extends Object> b = a;
由于b只能读取不能添加,所以避免了开始时向里面添加错误元素的安全问题。

如果我将addAll改成这样

boolean addAll(Collection<? extends E> c)  {
  //省略内容
}

那么这些的代码就可以实现了

List<Object> a = new ArrayList<>();
List<String> b = new ArrayList<>();
a.addAll(b);

extends通配符相当于有更强的安全性,它使一个集合只读,同时也使它可以在一定程度上进行类型转换。

super通配符

与extends通配符相对应,extends是只读,那么super通配符就代表着只写,它能确保一个元素能安全写入某个集合。

static <T> void fill(List<? super T> list, T t, int count) {
    for (int i = 0; i < count; i++) {
        list.add(t);
    }
}

这个方法可以这么调用

fill(new ArrayList<String>(), "this is a text", 10);

也可以这样

fill(new ArrayList<Object>(), "this is a text", 10);

这个例子中,T是String,故List的类型为List<? super String>
它意味着这个List应该是一个List或 String的父类,所以向里面添加一个String是没问题的。
super保证了一个元素一定可以被写入一个集合中,而不用关心这个集合具体是什么类型。

更好的理解

在读kotlin文档时,有提到过生产者、消费者的概念。
生产者就是只能获取元素,不能消费(使用)元素的集合,对应着 extends
消费者就是只能消费元素,不能生产(获取)元素的集合,对应着 super
最初看这两个通配符确实很容易产生误解,因此 kotlin用了更直观的 out代表生产者,in代表消费者。
比如kotlin中不可变的数组就是 Array(out T),只能从这个不可变的数组中获取内容,而不能添加、修改、删除内容。
生产者消费者的概念更容易理解泛型,用这个方式就能十分轻松地理解许多地方使用通配符的原因。

总结

经过一段时间的思考,我对泛型的理解又深入一层,也发现了以前使用泛型时的许多错误。

顺便安利下kotlin,这门语言十分不错,有许多其他语言没有的特性。
Gogo有很长一段时间没有这种”学一门新语言”的感觉了。

本文根据 CC BY-NC-SA 4.0 License 协议授权
本文链接:https://blog.gogo.moe/20170327_%E7%90%86%E8%A7%A3%E6%B3%9B%E5%9E%8B%E9%80%9A%E9%85%8D%E7%AC%A6/