乐趣区

[Java菜鸟系列] 「林彪」教你通配符泛型

J007- [Java 菜鸟系列]「林彪」教你 ” 通配符泛型 ”
菜鸟:Java 方法参数中的泛型通配符要怎么理解?老湿:<? extends T> 表示类型的上界,表示该类型的可能是 T 或是 T 的子类;<? super T> 表示类型的下界,表示该参数化类型是 T 或 T 的超类,直至 Object;菜鸟:呃,听不懂 …
优化
林彪
在进行大批量的读写时,为了保证绝对的安全我们采用的是编译期间强检查而且不支持协变的泛型,但是在某些特殊情况下,我们需要既安全又方便,那该怎么办呢?

这里的 ” 某些特殊情况下 ” 指的是什么?这里就不举出示例了,每一本编程书都有,不多废话,不懂去翻翻。
难道是勤劳勇敢的工程师们,直接解决了这个难题吗?
No No No,too yough too simple
攻城狮们并没有强攻,而是针对特定情景进行了优化,从而实现了这一目的。
二战开打时,林彪在苏联养病。(太嘚瑟,一枪被人家打了,233,所以 115 师实际上不是林彪指挥的)召开军事会议的时候,斯大林顺便叫林彪过来,谈到了马奇诺防线的事情。绝大多数的苏联将领都回答说,马奇诺防线德军是不可能摧毁的,防线超过了德军的攻击能力。林彪说到:恕我直言,马奇诺防线就是堆垃圾,攻城狮们为什么非要强攻?从侧翼绕过去不就行了吗?结果是什么我就不说了,嘿嘿嘿 … 可见林彪早早就有了攻城狮思维,牛。考虑到有些同学历史不及格,老湿还是仁慈的,还是放张图吧。

两种优化思路
那么究竟是针对哪些场景进行了优化呢?
主要是两个。
这两种情景我分别称他们为,商场下班模型和捕鼠器模型。
但是其中捕鼠器模型的确是重点,因为这个理解起来比较难,所以老湿的才华才发挥地淋漓尽致。
商场下班模型
还是先重复一点,泛型需要保证类型安全,也就是:只要定好了类型,那么就要保证读写都不出错。
人不可貌相 海水不可斗量
// 示例 1:操作端

//Employee 是雇员,Manager 是经理,Employee 是 Manager 的超类。

public static void main(String[] args) {
ArrayList<Manager> mList = new ArrayList<>();
mList.add(new Manager()); // 对

List<? extends Employee> eList =mList; // 对

Manager manager = new Manager();

eList.add(manager); // 错

eList.add(null); // 对

eList.contains(manager); // 对

Manager a = (Manager) eList.get(0); // 对

}
你可以看到,在第 7 行,使用了泛型通配符,没有这种语法的时候,直觉的操作应该是:
List<Employee> eList =Arrays.asList(new Manager());
也就是,把一个 List< Manager> 赋值给 List<Employee>,在 Object[]中完全可以这么玩,因为在上一次的时候提到过,Object[]为了早期没有泛型时的程序设计方便,是协变的;但是泛型中是不行的,泛型是不可协变的。
所以这种方式是错的,那我们来分析一下新的这种语法。
List<? extends Employee> eList =Arrays.asList(new Manager());
为了方便的使用多态,List<? extends Employee> 也是协变的。
所以看到这种通配符的形式,你就想象它是 Object[],这样就好理解得多,别看他们长得不像,其实他们的体制是相似的。
List<? extends Employee> 的体制更像是 Object[],而不是普通的泛型。看事物不能只看表面,虽然我们中国人长得很像日本人,但是我们的体制差距是很大的;虽然苏联老毛子金发碧眼的,和我们中国人长得不像,但是他们的体制和我们是很类似的。
木桶效应
但是,由于通配符泛型还是泛型,所以他需要保证两个地方的安全。
// 示例 1:类型安全
ArrayList<Manager> mList // 形态 1
List<? extends Employee> eList // 形态 2
在形态 1 中,我们需要保证读写类型均为严格的 Manager;在形态 2 中,我们需要保证读写类型均为严格的? extends Employee。
但是,我们并不需要照顾形态 2,只需要照顾形态 1 就行了,因为形态 1 更严格,只要它被满足了,那么形态 2 一定是被满足的。
维修木桶,只需要补上短板就行。

怎么保证安全? 答案很简单,就是:禁止 形式 2(extends 通配符) 写入。
为什么是这样? 这里建议你先不要往下看,你先想一想,已经说到这一步了,其实大多数人都能自己想出来,这样印象是最深刻的,如果不行的话,呃。。。我只好让商场的老大爷教你了,可别让大爷看不起你。

扫地僧

大爷,大爷,我们公司的小王死活也理解不了 List<? extends Employee> 为啥是安全的,你能教教他吗?

很简单嘛。你看现在我要下班回家了,门也不锁,为啥这么逍遥自在?

你就不怕领导罚你款吗?进来贼人怎么办?

你大爷把门都一锁,只留下一个只能出不能进的单向门,不就行了?
反正进来的时候我都仔细检查过了,现在商场里不会有贼人,所以随便出。

!!!!!!!!!!!
你,你,你 ….. 到底是何方神圣?
只能出不能进
咳咳 … 可怜你们这智商了,让我再细说一下。
老大爷道出了一个真理,要想保证安全,有一个办法是可以的,那就是只能出不能进。
首先,你要知道 List<? extends Employee> 是这么来的,List<? extends Employee> eList =mList;。
所以说,要进来错误的类型,必须在此之后进入,之前是不可能进入的,因为之前管得更严。
所以只要保证 List<? extends Employee> 这个形式下的数据不被写入,那么就保证了绝对的安全。
所以说,这种 <? extends Employee> 的形式,主要适用于多读少写的情况下使用。
通融一下
那万一需要传入参数怎么办?
很简单,还是老办法替 Java 背锅。
你可以看到下面的 List 类中有三个方法,其中第一个是 add,也就是向其中写入数据,这个方法的参数是泛型 E element,泛型只要发现类型不确定的东西,就直接拒绝。所以,不能在 extends 通配符模式下写入任何数据。
但是,方法 remove 和 contains 的参数是 Object,泛型的使命是类型绝对安全,但是 Object 并不会,所以如果方法的参数是 Object,那么就可以传入任何类型。
remove(移除元素)和 contains(查看元素是否包含在内)这两种方法显然是不用明确具体类型的,所以传入 Object 就行了;如果你非要明确类型也可以,要不你就 明确写出来,要不你就从 Object 强制转型。反正只要不用泛型就好,这个锅 Java 不背。
// 示例 1:容器端

public interface List<E> extends Collection<E> {
// ……
void add(E element);
boolean remove(Object o);
boolean contains(Object o);
// ……
}
捕鼠器模型
为什么这么难理解?
通配符泛型的另一种形式是 List<? super Employee>。
为什么这种东西比较难以理解呢? 因为和我们的 直觉 正好相反。
之前的时候,如果你忘记了 List<? extends Employee> 的体制,其实一想 Object[]就全懂了,一切其他的东西都是直接或间接 extends Object。
但是,List<? super Manager> 形式正好和 Object 的性质完全相反,这就不好理解。
这就好比,让你想象一下头朝下脚朝上的星球上发生的事情,没有人想得出来,因为实在是没见过。
图示
这个时候画张图来理解,其实是比较方便的
// 扩展 (继承) 树
Object(一切物体) >> Human(人类) >> Employee(雇员) >> Manager(经理) >> CEO(总裁)
List<? extends Employee> 能接收什么呢?Employee[]能接收什么,它就应该能。

List<? super Employee> 能接收什么呢? 能接受其 超类,也就是说,儿子变成爸爸了。

捕鼠器
如果说,extends 是 ” 只出不进 ”,是 ” 商场下班 ”;那么 super 是相反的,它是 ” 只进不出 ”。
所以,这种形式主要用在只写不读的情境下。

就好像捕鼠器的这个门一样,只能从外面推开进去,但是从里面是没办法推开出来的。
// 示例 2:类型安全
ArrayList<Manager> mList // 形态 1
List<? super Employee> eList // 形态 2
根据木桶原理,这次的薄弱环节变成了 形态 2,因为形态 2 更严格。
所以,只能在形态 2 进行写入,不能读取。
就好像捕鼠器,什么东西都可以钻进去,捕鼠器不管钻进去的是不是老鼠,他只保证已经进去了,老鼠不会出来就行了。
处理老鼠
抓住老鼠之后怎么办?
抓住之后捕鼠器的责任就完了,之后老鼠跑不跑就由你承担了,所以,你就把捕鼠器的门给卸掉了,这样才能拿出老鼠。
这就相当于,返回值变成了 Object,Object 不是泛型,不需要确保类型安全。拿出来之后,如果你非要用,那么就强制转型。
总结
extends + 通配符泛型 主要适用于多读少写的情况下,如果需要传入参数,那么必须保证参数不是泛型。
super + 通配符泛型 主要适用于多写少读的情况下,如果需要接收返回值,那么必须保证返回值不是泛型。
这两种优化模型,分别称为商场下班模型和捕鼠器模型。
学术一点
当然,学术一点的叫法是 PECS(Producer Extends Consumer Super)原则:

频繁往外读取内容的,适合用上界 Extends。
经常往里插入的,适合用下界 Super。

这是《Java Core》这本书里的解释,不知道你们记不记得住,反正老湿是记不住的。
End
心如止水是 Java/AHK 持续学习者,欢迎您来和我探讨 Java/AHK 问题 ^_^
更多文章:

[Java 菜鸟系列] 泛型数组的问题是「体制」问题
[Java 菜鸟系列] 红警思维看 ” 泛型 ”
[Java 菜鸟系列] 协议与回调
[Java 菜鸟系列] 内部类与 lambda 表达式

该文章版权系“心如止水”所有,欢迎分享、转发,但如需转载,请联系 QQ:2531574300,得到许可并标明出处和原链接后方可转载。未经授权,禁止转载。版权所有 ©心如止水 保留一切权利。

鸣谢
最后也感谢一下出场人物吧,感谢诸位的付出:

退出移动版