理解泛型通配符
近日在学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
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/