V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
Mexion
V2EX  ›  问与答

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

  •  
  •   Mexion · 2021-07-18 15:03:44 +08:00 via Android · 4064 次点击
    这是一个创建于 984 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    求各位大哥们解解疑惑

    34 条回复    2023-03-29 18:04:58 +08:00
    namelosw
        1
    namelosw  
       2021-07-18 15:10:56 +08:00
    你的问题描述的不是很清楚,可以写一点代码片段来描述一下问题出在哪。

    ---

    不过直觉上听起来是型变问题,也就是 List<Dog> 并不一定是 List<Animal> 的子类。

    如果是协变,也就是 Animal 和 Dog 只读的,那么 List<Dog> 就是 List<Animal> 的子类。

    如果反过来逆变,也就是只写的,那么 List<Animal> 是 List<Dog> 的子类。

    如果又可读又可写,那就是不变,两种类型之间没有子类关系。
    ipwx
        2
    ipwx  
       2021-07-18 15:11:59 +08:00
    我猜你这是 Java 。。。 老哥你该写明一下语言吧。

    Java 不熟,不过你要不试试显式转换成 Animal ?
    ReferenceE
        3
    ReferenceE  
       2021-07-18 15:28:10 +08:00 via Android
    你这是啥语言?没看懂
    Jooooooooo
        5
    Jooooooooo  
       2021-07-18 15:31:23 +08:00   ❤️ 2
    这个确实是初学让人非常迷惑的地方

    可以了解 PECS 原则, extends 只能是生产者, 往外提供东西, 只能 get, 无法往里 put

    同理 super 只能是消费者, 往里放东西, 只能 put, 无法往 get

    至于为什么, 可以考虑 Collection<? extends Fruit> 这样一个集合你知道肯定都是水果, 但不知道具体是哪一种, 所以往里面放会破坏原有的结构(原来如果是苹果你往里放一个香蕉肯定不行), 唯一知道的这里面都是水果, 所以往外 get 是可以的

    super 是类似的.


    想要知道的更多可以仔细读下这个:

    泛型: www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html

    协变: dzone.com/articles/covariance-and-contravariance
    Mexion
        6
    Mexion  
    OP
       2021-07-18 15:41:21 +08:00
    @Jooooooooo 这就是我疑惑的地方,既然已经约束了是水果,那么取出来的肯定都是水果,那么我往里存香蕉那也是一个水果,往里存苹果也是一个水果,不是应该保证存进去的都是水果就行了吗,为什么直接就不能存了,存进去保证是水果和水果的子类型,取出来自然也是水果啊
    Mexion
        7
    Mexion  
    OP
       2021-07-18 15:42:41 +08:00
    @namelosw 我的疑惑点就是为什么协变是只读的
    Mexion
        8
    Mexion  
    OP
       2021-07-18 15:43:06 +08:00
    @ReferenceE 是 Java
    Mexion
        9
    Mexion  
    OP
       2021-07-18 15:44:07 +08:00
    @ipwx 对,是 Java,是所有东西都存不了,包括 Animal
    Jooooooooo
        10
    Jooooooooo  
       2021-07-18 15:51:10 +08:00
    @Mexion 因为可以这么写 List<? extends Number> = new ArrayList<Integer>()

    这样往里放一个 double 肯定不合法
    Jooooooooo
        11
    Jooooooooo  
       2021-07-18 15:52:38 +08:00
    @Mexion 要明白的一点是 ? extends 并不是表明一个范围, 而是精准了描述了一个东西. 而 ? 导致无法事先知道是什么东西, 所以也无法往里放.
    Mexion
        12
    Mexion  
    OP
       2021-07-18 15:58:07 +08:00
    @Jooooooooo 看到这句 List<? extends Number> = new ArrayList<Integer>(),貌似懂了,谢谢老哥解惑。
    JinTianYi456
        13
    JinTianYi456  
       2021-07-18 15:58:16 +08:00
    @Mexion #6 是不是这样?(我没验证)
    1. 有一个方法 void do(Collection<? extends Fruit> f)
    2. 然后变量 a 是 Collection<苹果>, 调用 do 方法,do 方法内可以执行 f.add(香蕉)吗?
    3. 同理变量 b 是 Collection<香蕉>, 调用 do 方法,do 方法内可以执行 f.add(苹果)吗?
    JasonLaw
        14
    JasonLaw  
       2021-07-18 15:58:23 +08:00
    @JasonLaw #4 List<? extends Animal> list 可以是 List<Dog>,也可以是 List<Cat>,你不能将一个 Cat 放进 List<Dog>中,但是你可以确定从 list 取出来的东西一定是一个 Animal 。想了解更多,建议你认真看一下 4 楼那个链接。
    Mexion
        15
    Mexion  
    OP
       2021-07-18 16:13:29 +08:00
    @JasonLaw 懂了,谢谢老哥
    JasonLaw
        16
    JasonLaw  
       2021-07-18 17:00:49 +08:00
    @Jooooooooo #11 我不太理解你说的这段话。

    1. 怎么理解“? extends 并不是表明一个范围,而是精准了描述了一个东西”?
    2. 怎么理解“? 导致无法事先知道是什么东西, 所以也无法往里放”?
    Jooooooooo
        17
    Jooooooooo  
       2021-07-18 17:31:36 +08:00
    @JasonLaw List<> 里面装的东西是个明确的东西, 无论是 Integer 还是 Long, 无法往一个 List<Integer> 里面放 Long, 反过来也不行. ? extends Number 使得 List 里面的东西变得不可知, 只知道里面肯定都是 Number.

    因为不知道里面是不是装着 Integer, 所以无法放 Integer, 其它类型同理.

    还是那个例子最清晰, 可以这么 List<? extends Number> = new ArrayList<Integer>() 不可以往里面放 Long 是不是很明显.
    JasonLaw
        18
    JasonLaw  
       2021-07-18 18:06:21 +08:00
    @Jooooooooo #17

    你说“因为不知道里面是不是装着 Integer, 所以无法放 Integer”,其实是因为不知道是 List<Integer>还是 List<Long>,所以才不能放 Integer 。如果是“因为不知道里面是不是装着 Integer, 所以无法放 Integer”,那么如果有个 List<Number> list 的话,我们也不知道里面是不是装着 Integer,但是我们是可以放 Integer 的。

    如果有错误的话,麻烦指出。
    Jooooooooo
        19
    Jooooooooo  
       2021-07-18 18:26:05 +08:00
    @JasonLaw 后面的这个可以搜下 协变. 我理解这是语言带来的特性.

    举个例子, 假设有一个父类 Person, 有一个子类 Child extends Person.

    有一个方法, 入参和返回值都是 Person. public Person test(Person p)

    你会发现只有入参可以传 Child, 返回值不能用 Child 去接. 这个应该是语言的取舍, 我记得在 Java 5 之前入参也不能是 Child, 后来改的.
    Jooooooooo
        20
    Jooooooooo  
       2021-07-18 18:30:44 +08:00
    @JasonLaw 稍微再想了一下, 为什么 List<Number> 可以装 Integer 而 ? extends Number 不行. 原因就应该是类似你说的更准确, 因为 ? extends Number 可以是 Integer 也可以是 Long, 但并不明确是哪个, 所以哪个都不能装.
    zxCoder
        21
    zxCoder  
       2021-07-18 18:35:05 +08:00
    @Jooooooooo 越看越迷糊了
    ipwx
        22
    ipwx  
       2021-07-18 19:09:07 +08:00
    @Jooooooooo 老哥,稳。这个例子解释一切。(虽然我确实不太懂 Java )

    作为 C++ 程序员的我表示,List<? extends Number> = new ArrayList<Integer>() 是不存在的操作,所以从来没考虑过这种问题。
    34531535
        23
    34531535  
       2021-07-18 20:17:40 +08:00
    虽然我懂泛型,但 18 楼老哥说的真绕
    zpf124
        24
    zpf124  
       2021-07-18 20:18:53 +08:00   ❤️ 4
    使用角度来说,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
        25
    fly2mars  
       2021-07-18 21:15:48 +08:00
    <? extends A> 可以 get 不能 put
    <? super A>也可以 get 但是 get 的是 Object,也可以 put A 和 A 的子类
    jinhan13789991
        26
    jinhan13789991  
       2021-07-19 09:47:25 +08:00
    折磨自己干啥,我是哪个能编译通过用哪个 /dog
    kahlkn
        27
    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
        28
    zhaorunze  
       2021-07-19 10:11:58 +08:00   ❤️ 1
    考虑两种时态,运行时和编译时,编译时。
    再考虑类中的属性和行为,子类肯定比父类属性和行为要多的,so,如果用子类=父类,即向下转型,这时候需要强转,在强转的过程中呢,会修改指针的 class 类型,这时候会增加多出来的属性和行为。
    GuuJiang
        29
    GuuJiang  
       2021-07-19 13:04:26 +08:00   ❤️ 2
    @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
        30
    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
        31
    Mexion  
    OP
       2021-07-19 15:50:10 +08:00 via Android
    @GuuJiang 没错,是我刚开始没转过弯来想岔了,以为这个泛型是约束里面的元素的,当看到 List<?extends Number>=new ArrayList<Integer>()时才反应过来其实这个泛型是来约束 List 类型的。
    l8mEQ331
        32
    l8mEQ331  
       2021-07-19 23:22:56 +08:00
    @GuuJiang 学习了,感谢分享,很有帮助。
    AnnaIsGod
        33
    AnnaIsGod  
       2023-03-29 10:28:15 +08:00
    因为你把大肠放在了你的脑袋里,别搞错了
    Mexion
        34
    Mexion  
    OP
       2023-03-29 18:04:58 +08:00
    @AnnaIsGod 你真是把我逗笑了,我回复你不是在批评你,只是在说很多人的实际情况,包括我自己也是,比上不足,比下没必要。你倒好,到我的提问帖子里挖坟攻击我,我只能说你这种人自己理解有问题,攻击性还这么强,自视清高,听不得别人说实话,怪不得找不到工作,难道不应该自己找找原因吗?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   950 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 21:27 · PVG 05:27 · LAX 14:27 · JFK 17:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.