为什么泛型使用了 extends 就不能存东西了?

2021-07-18 15:03:44 +08:00
 Mexion

小白学习泛型产生了个问题。为什么泛型中使用了 extends 来约束类型就不能存东西了?

比如<? extends Animal>这表名 Animal 和 Animal 的子类型,子类型比如 Dog,Cat 按理来说不都是应该可以存入的吗,都按照 Animal 来存,取出来也都认为是 Animal 类型不行吗?

求各位大哥们解解疑惑

4131 次点击
所在节点    问与答
34 条回复
zxCoder
2021-07-18 18:35:05 +08:00
@Jooooooooo 越看越迷糊了
ipwx
2021-07-18 19:09:07 +08:00
@Jooooooooo 老哥,稳。这个例子解释一切。(虽然我确实不太懂 Java )

作为 C++ 程序员的我表示,List<? extends Number> = new ArrayList<Integer>() 是不存在的操作,所以从来没考虑过这种问题。
34531535
2021-07-18 20:17:40 +08:00
虽然我懂泛型,但 18 楼老哥说的真绕
zpf124
2021-07-18 20:18:53 +08:00
使用角度来说,java 中你可以直接死记硬背就好,简单的将 List<? extends Animal> 这种 ?号表示的泛型记成只读的集合。

从思考逻辑层面来理解如下三个方法:
1 、public List<Animal> getZoomAnimals () —— 获取动物园所有的动物
返回的结果指的是这个集合存放的就是 Animal 类型,Cat,Dog,Tiger 都算,都可以混着装在这里面。

2 、public List<? extends Animal> getSomeAreaAnimals(int AreaId) —— 获取动物园某个区域的动物
而这里就不是指的存放的是 Animal 类型的元素了,*他的类型就是确定的<?>类型*。
这个类型是什么? 不知道,但可以肯定他,是 Animal 下面的*某一种具体的子类*,<?>类型如果是 Cat,那这个集合就不能存放 Dog 。这个<?>和匿名内部类一样,指的是某个具体但不知道名字子类类型,而不是父类的。

我在老虎洞里找到的一定全是老虎,而不会有犀牛;海底水族馆里也只会找到海洋动物,而不会把鹦鹉扔进去。


3 、public <T extends Animal> List<T> getSomeAreaAnimals(int AreaId,Class<T> cls)
与<?>相对的还有一个写法,如果我想让人操作返回的结果怎么办?答:你用的时候直接告诉我,“你知道我会返回什么具体的类型”。
这里实际与 <? extend Aniaml> 唯一的区别就是,你调用我的时候就已经知道 我会给你返回什么结果了, 当然也可能出现你以为的只是你以为,我会直接报错告诉你你给的类型不对。

你递给我一个鱼缸,让我去鱼类区给你抓鱼,我抓回来的一定是鱼,不会把猴子也塞里面; 你给我老虎笼子让我给你昆虫区抓蚊子,我只会告诉你你给错家伙事了。
fly2mars
2021-07-18 21:15:48 +08:00
<? extends A> 可以 get 不能 put
<? super A>也可以 get 但是 get 的是 Object,也可以 put A 和 A 的子类
jinhan13789991
2021-07-19 09:47:25 +08:00
折磨自己干啥,我是哪个能编译通过用哪个 /dog
kahlkn
2021-07-19 10:07:47 +08:00
试了一下,明白你的意思了。

```
List<? extends Animal> list = new ArrayList<Animal>();
list.add(new Dog());

Map<String, ? extends Animal> map = new HashMap<String, Animal>();
map.put("str", new Dog());
```

会出现错误:“
add (capture<? extends test.bean.Animal>) in List cannot be applied
to (test.bean.Dog)”


感觉无力解释,只能说 ? extends Animal 不应该用在这个位置。 一般来说可以这样用,表示可以传入一个 list,可以是 List<Animal>,可以是 List<Dog>,可以是 List<Cat> 。
```
public void takeThing(List<? extends Animal> list);
```

如果需要 List 中可以同时存入 dog 、cat,直接这样就行了。
```
List<Animal> list = new ArrayList<Animal>();
list.add(new Dog());
list.add(new Cat());
```
zhaorunze
2021-07-19 10:11:58 +08:00
考虑两种时态,运行时和编译时,编译时。
再考虑类中的属性和行为,子类肯定比父类属性和行为要多的,so,如果用子类=父类,即向下转型,这时候需要强转,在强转的过程中呢,会修改指针的 class 类型,这时候会增加多出来的属性和行为。
GuuJiang
2021-07-19 13:04:26 +08:00
@Mexion 这个其实是个很有价值的问题,说句冒犯的话,我相信现在越来越多的 Java 程序员当问到他这个话题时,都能说出协变、逆变、PECS 原则等名词,但是继续深入追问下去就会发现开始难以自圆其说了,在这里我根据自己的理解,争取能够一次性把这个问题说清楚
1. 首先回答你主题里的疑问,首先你的疑问来自于一个误解,List<? extends Animal>并不是用来约束里面元素类型的,而是用来约束 List 本身的,只要这个弯转过来那主题里的疑问以及其他类型的疑问都一并迎刃而解了,为什么说这是个误解,有个很简单的证据,你直接写个 List<Animal>,里面一样是可以存 Animal 及其子类的,所以 List<? extends Animal>绝对不是表示这个 list 可以存 Animal 及其子类的意思,到底表示什么下面展开讲
2. 首先简单回顾一点预备知识,在绝大多数的 OO 语言里,类的继承关系都是表示 is-a 的关系,简单地说就是如果 B is-a A,那么在所有期望一个 A 的地方(包括方法参数,变量等),都可以提供一个 B,并且不需要任何的显示类型转换,这就是为什么可以写 List l = new ArrayList();的原因,对于简单类型,判断 is-a 很简单,只要二者在一条继承关系的链上,就能定义 is-a 关系,但是引入泛型后,问题就开始变得有点复杂了,List<Animal>和 List<Dog>之间的 is-a 关系是怎么样的呢?结论是不具有 is-a 关系,List<Animal> is-a List<Dog>不成立看起来是显然的,但是反过来似乎就有点反直觉了,这就涉及到下面要讲的几个概念
3. 关于不变(invariant)、协变(covariant)、逆变(contravariant),首先声明,虽然这里使用 Java 语言举例,但是这几个概念在几乎所有支持泛型的 OO 语言里都存在,事实上这几个概念在存在计算机语言之前就已经存在了,是范畴论里的几个概念,下面以 Java 为例说下分别是什么意思,前面说了,List<Animal>和 List<Dog>之间不存在 is-a 关系,这就叫做不变(invariant),但是有的时候,我们需要让它们之间存在 is-a 关系,这就要通过一些关键字来人为指定下面两种关系,假设规定 List<Dog> is-a List<Animal>,这种关系就叫做协变(covariant),因为 List 之间的 is-a 关系和 Animal 及 Dog 之间的 is-a 关系方向是一致的,反之,如果规定 List<Animal> is-a List<Dog>,这种关系就叫做逆变(contravariant),因为 List 之间的 is-a 关系和 Animal 及 Dog 之间的 is-a 关系方向是相反的,各种语言里都会有一些标识来定义协变及逆变,例如 c#用 in/out,scala 用+/-,而 Java 就是用的 extends 和 super,简单说 List<? extends Animal>实际表示的是,对于这个变量,可以接受 List<Animal>,也可以接受 List<Dog>、List<Cat>等,反之,如果定义 List<? super Animal>,那么表示这个变量可以接受 List<Animal>、List<Object>等,这就是我在开头说的,当定义一个变量 List<? extends Animal> l 时,这里的 extends 并不是约束“l 里可以存什么”,而是约束“什么样的 List 能够赋值给 l”
4. 相信到这里题主应该已经能够自己想通问题出在哪了,但是我还是顺便展开说一说 PECS 原则,PECS 原则确实是个帮助记忆的好东西,但是真正合格的程序员应该主动多思考一下,PECS 里是用 List 举例的,但是泛型类并不一定都是个容器,对于非容器类型的泛型类,到底什么算 produce 什么算 consume,其实,所谓的 produce 和 consume 是人逻辑上的概念,编译器肯定不认识啊,所以 PECS 的本质其实指的是这个类型边界出现在方法参数上还是出现在返回值里,下面举个例子,假如我们写了一个方法,作用是将一个 list 里的元素取出来进行某种操作然后放到另一个 list 里,为什么方法签名必须定义成这个样子
void map(List<? extends Animal> source, List<? super Animal> target)
首先,对于 source,我们可能会进行这样的操作
Animal a = source.get(0);
这就要求传给 source 的 list 必须满足“能够从中取出 Animal”这个条件(当然,从编译器的角度,应该是“可以调用返回类型为 Animal 的方法”),因此当在调用 map 时,可以给 source 参数传 List<Animal>、List<Dog>、List<Cat>都是合法的,但是不能传 List<Object>
而对于参数 target,我们可能进行这样的操作
target.add(a);
这就要求传给 target 的 list 必须满足“能够往里添加 Animal”这个条件(当然,从编译器的角度,应该是“可以调用参数为 Animal 的方法”),因此当在调用 map 时,可以给 target 参数传 List<Animal>,List<Object>都是合法的,假设 target 能够接受 List<Dog>,而在 map 方法体内部往里放了个 Animal,这显然是不合理的

一不小心写了这么多,最后放个太长不看版吧,一句话,对于 List<? extends Animal> l 和 List<? super Animal>,泛型边界并不是表示 l 里能够存什么,而是表示 l 能够接受什么样的 list
bigbyto
2021-07-19 13:55:48 +08:00
主要是为了类型安全,如果不限制写入操作,代码中容易出现难以 debug 的错误。假如定义了一个 List<Cat>,你把它传进了 AnimalUtils.process(List<? extends Animal>),然后这个函数里面有 add(new Dog())这样的行为,那么你遍历 List<Cat>就会遇到 classcast 的错误。

实际上这是个挺复杂的问题,牵扯到的知识点比较广,涉及到多态,subtyping,类型擦除,编译时运行时等概念,三言两语不好描述清楚。
Mexion
2021-07-19 15:50:10 +08:00
@GuuJiang 没错,是我刚开始没转过弯来想岔了,以为这个泛型是约束里面的元素的,当看到 List<?extends Number>=new ArrayList<Integer>()时才反应过来其实这个泛型是来约束 List 类型的。
l8mEQ331
2021-07-19 23:22:56 +08:00
@GuuJiang 学习了,感谢分享,很有帮助。
AnnaIsGod
2023-03-29 10:28:15 +08:00
因为你把大肠放在了你的脑袋里,别搞错了
Mexion
2023-03-29 18:04:58 +08:00
@AnnaIsGod 你真是把我逗笑了,我回复你不是在批评你,只是在说很多人的实际情况,包括我自己也是,比上不足,比下没必要。你倒好,到我的提问帖子里挖坟攻击我,我只能说你这种人自己理解有问题,攻击性还这么强,自视清高,听不得别人说实话,怪不得找不到工作,难道不应该自己找找原因吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/790199

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX