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

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

  •  
  •   Mexion · 134 天前 via Android · 2599 次点击
    这是一个创建于 134 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    求各位大哥们解解疑惑

    32 条回复    2021-07-19 23:22:56 +08:00
    namelosw
        1
    namelosw   134 天前
    你的问题描述的不是很清楚,可以写一点代码片段来描述一下问题出在哪。

    ---

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

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

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

    如果又可读又可写,那就是不变,两种类型之间没有子类关系。
    ipwx
        2
    ipwx   134 天前
    我猜你这是 Java 。。。 老哥你该写明一下语言吧。

    Java 不熟,不过你要不试试显式转换成 Animal ?
    ReferenceE
        3
    ReferenceE   134 天前 via Android
    你这是啥语言?没看懂
    Jooooooooo
        5
    Jooooooooo   134 天前   ❤️ 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   134 天前
    @Jooooooooo 这就是我疑惑的地方,既然已经约束了是水果,那么取出来的肯定都是水果,那么我往里存香蕉那也是一个水果,往里存苹果也是一个水果,不是应该保证存进去的都是水果就行了吗,为什么直接就不能存了,存进去保证是水果和水果的子类型,取出来自然也是水果啊
    Mexion
        7
    Mexion   134 天前
    @namelosw 我的疑惑点就是为什么协变是只读的
    Mexion
        8
    Mexion   134 天前
    @ReferenceE 是 Java
    Mexion
        9
    Mexion   134 天前
    @ipwx 对,是 Java,是所有东西都存不了,包括 Animal
    Jooooooooo
        10
    Jooooooooo   134 天前
    @Mexion 因为可以这么写 List<? extends Number> = new ArrayList<Integer>()

    这样往里放一个 double 肯定不合法
    Jooooooooo
        11
    Jooooooooo   134 天前
    @Mexion 要明白的一点是 ? extends 并不是表明一个范围, 而是精准了描述了一个东西. 而 ? 导致无法事先知道是什么东西, 所以也无法往里放.
    Mexion
        12
    Mexion   134 天前
    @Jooooooooo 看到这句 List<? extends Number> = new ArrayList<Integer>(),貌似懂了,谢谢老哥解惑。
    JinTianYi456
        13
    JinTianYi456   134 天前
    @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   134 天前
    @JasonLaw #4 List<? extends Animal> list 可以是 List<Dog>,也可以是 List<Cat>,你不能将一个 Cat 放进 List<Dog>中,但是你可以确定从 list 取出来的东西一定是一个 Animal 。想了解更多,建议你认真看一下 4 楼那个链接。
    Mexion
        15
    Mexion   134 天前
    @JasonLaw 懂了,谢谢老哥
    JasonLaw
        16
    JasonLaw   133 天前
    @Jooooooooo #11 我不太理解你说的这段话。

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

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

    还是那个例子最清晰, 可以这么 List<? extends Number> = new ArrayList<Integer>() 不可以往里面放 Long 是不是很明显.
    JasonLaw
        18
    JasonLaw   133 天前
    @Jooooooooo #17

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

    如果有错误的话,麻烦指出。
    Jooooooooo
        19
    Jooooooooo   133 天前
    @JasonLaw 后面的这个可以搜下 协变. 我理解这是语言带来的特性.

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

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

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

    作为 C++ 程序员的我表示,List<? extends Number> = new ArrayList<Integer>() 是不存在的操作,所以从来没考虑过这种问题。
    34531535
        23
    34531535   133 天前
    虽然我懂泛型,但 18 楼老哥说的真绕
    zpf124
        24
    zpf124   133 天前   ❤️ 3
    使用角度来说,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   133 天前
    <? extends A> 可以 get 不能 put
    <? super A>也可以 get 但是 get 的是 Object,也可以 put A 和 A 的子类
    jinhan13789991
        26
    jinhan13789991   133 天前
    折磨自己干啥,我是哪个能编译通过用哪个 /dog
    kahlkn
        27
    kahlkn   133 天前
    试了一下,明白你的意思了。

    ```
    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   133 天前   ❤️ 1
    考虑两种时态,运行时和编译时,编译时。
    再考虑类中的属性和行为,子类肯定比父类属性和行为要多的,so,如果用子类=父类,即向下转型,这时候需要强转,在强转的过程中呢,会修改指针的 class 类型,这时候会增加多出来的属性和行为。
    GuuJiang
        29
    GuuJiang   133 天前   ❤️ 1
    @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   133 天前
    主要是为了类型安全,如果不限制写入操作,代码中容易出现难以 debug 的错误。假如定义了一个 List<Cat>,你把它传进了 AnimalUtils.process(List<? extends Animal>),然后这个函数里面有 add(new Dog())这样的行为,那么你遍历 List<Cat>就会遇到 classcast 的错误。

    实际上这是个挺复杂的问题,牵扯到的知识点比较广,涉及到多态,subtyping,类型擦除,编译时运行时等概念,三言两语不好描述清楚。
    Mexion
        31
    Mexion   133 天前 via Android
    @GuuJiang 没错,是我刚开始没转过弯来想岔了,以为这个泛型是约束里面的元素的,当看到 List<?extends Number>=new ArrayList<Integer>()时才反应过来其实这个泛型是来约束 List 类型的。
    l8mEQ331
        32
    l8mEQ331   132 天前
    @GuuJiang 学习了,感谢分享,很有帮助。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   3907 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 08:31 · PVG 16:31 · LAX 00:31 · JFK 03:31
    ♥ Do have faith in what you're doing.