V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lhx2008
V2EX  ›  Java

Java 的 <T> T get(List<? extends T> i) 怎么理解?和 <T> T get(List<T> i) 有啥不同?

  •  2
     
  •   lhx2008 ·
    xenv · 2018-12-05 23:38:37 +08:00 · 3703 次点击
    这是一个创建于 2181 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Collections 里面有一段代码

        private static <T> T get(ListIterator<? extends T> i, int index) {
            T obj = null;
            int pos = i.nextIndex();
            if (pos <= index) {
                do {
                    obj = i.next();
                } while (pos++ < index);
            } else {
                do {
                    obj = i.previous();
                } while (--pos > index);
            }
            return obj;
        }
    

    他这里用了 <T> T get(ListIterator<? extends T> i) 我有点不理解,因为 T 就是传入的 ListIterator 的泛型定的,根本没有再向下转型的可能了。

    所以应该和 <T> T get(ListIterator<T> i) 是一样的,那为什么要多此一举呢?

    第 1 条附言  ·  2018-12-06 08:02:19 +08:00
    有人可以写一段代码调用这两种参数的写法,一个可以传进去,一个传不进去的吗?这样就证明这两种参数确实不一样了。
    28 条回复    2018-12-06 13:54:38 +08:00
    lhx2008
        1
    lhx2008  
    OP
       2018-12-06 00:00:32 +08:00
    还有很多,比如

    public static <T> void copy(List<? super T> dest, List<? extends T> src)

    第一个参数可不可以也变成

    List<T> dest
    ywcjxf1515
        2
    ywcjxf1515  
       2018-12-06 00:30:56 +08:00 via iPad
    如果类 A 实现或者继承了 T,那么 ListIterator < A >对象也是能传进这个方法的,那么之后,obj 指向的是一个子类对象,也就是类型 A 的对象,这与返回值类型是兼容的
    FrankFang128
        3
    FrankFang128  
       2018-12-06 01:06:17 +08:00
    List<? extends T> 有多种可能,但是 List<T> 只有一种可能。
    chocotan
        4
    chocotan  
       2018-12-06 01:20:12 +08:00
    不一样的
    给方法再加个 T 类型参数就能看出来了
    ryougifujino
        5
    ryougifujino  
       2018-12-06 01:21:51 +08:00 via Android
    不是多此一举吧,假如传入的是子类,但是返回的数据可以向上转型呀
    ryougifujino
        6
    ryougifujino  
       2018-12-06 01:24:28 +08:00 via Android
    “因为 T 就是传入的 ListIterator 的泛型定的,根本没有再向下转型的可能了。”这里应该是向上转型才对
    lhx2008
        7
    lhx2008  
    OP
       2018-12-06 07:58:41 +08:00 via Android
    @ywcjxf1515
    @ryougifujino
    @FrankFang128

    没有意义吧,这个是静态泛型,每次泛型都是每次传的参数进去确定的
    lhx2008
        8
    lhx2008  
    OP
       2018-12-06 08:00:17 +08:00 via Android
    @chocotan 加参数一样的,一楼的不就加了一个参数,但是没有任何区别
    lhx2008
        9
    lhx2008  
    OP
       2018-12-06 08:06:53 +08:00 via Android
    谢谢大家回复,我知道是可以扩大范围,但是这个 T 就是在变量传入那个时刻才确定的,不是提前确定好,然后你第二次传进去的时候可以传一个父类或者子类的东西。(比如像 Stream 里面的)


    我有个想法,如果要证明这两种写法确实不一样,可以写一段代码调用这两种参数的写法,一个可以传进去,一个传不进去,但是我想不出来。
    wqlin
        10
    wqlin  
       2018-12-06 08:50:53 +08:00   ❤️ 1
    谈一下我的理解。
    Java 类型系统中 数组 和 集合 是会让人迷惑的。比如有两个类型,A 和 B,其中 A 是 B 的子类。那么 []A 也是 []B 的子类,可以这么写

    ```
    []A a = {...};
    []B b = a;
    ```

    但是使用集合时,比如 List。List<A> 和 List<B> 没有半毛钱关系,这两个类型完全没有联系。
    那么如何在集合中表达类型的上下限呢?就需要用到 ? 占位符、extends 和 super。
    ? 是类型占位符,表示这是一个类型,但是具体什么类型未知。比如 List<?> 表示一个未知类型的 List,但是这不是 raw List。
    ? 通常和 extends、super 一起使用。作为方法参数时,比如 List<T>,那么 List<? extends T> 可以接受任何 List<E>,其中 E 是 T 的子类,类型上限为 T ; List<? super T> 可以接受任何 List<E>,E 是 T 的超类,类型下线为 T。
    一个例子是实现 泛型 Number 相加
    ```
    static long sum(List<? extends Number> numbers) {
    long summation = 0;
    for (Number number : numbers) {
    summation += number.longValue();
    }
    return summation;
    }
    ```
    那么 List<Integer>、List<Double> 等,传入 sum 中:
    ```
    List<Integer> myInts = asList(1, 2, 3, 4, 5);
    List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
    List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
    System.out.println(sum(myInts));
    System.out.println(sum(myLongs));
    System.out.println(sum(myDoubles));
    ```
    如果单纯定义为:
    ```
    static long sum(List<Number> numbers) {
    long summation = 0;
    for (Number number : numbers) {
    summation += number.longValue();
    }
    return summation;
    }
    ```
    是没有什么意义的,这时传入 myInts 和 myLongs 会产生编译错误。


    ```
    public static <T> void copy(List<? super T> dest, List<? extends T> src)
    ```
    那么实例化时,比如说 T 是 Number。那么可以将 List<Integer> 拷贝到 List<Number> 甚至 List<Object> 中:
    ```
    List<Integer> myInts = asList(1,2,3,4);
    List<Double> myDoubles = asList(3.14, 6.28);
    List<Object> myObjs = newArrayList<Object>();
    copy(myInts, myObjs);
    copy(myDoubles, myObjs);
    ```

    最后谈一下 PECS 原则。如果我们只想从集合中读取元素,那么应该使用协变;如果我们只想向集合中加入元素,那么应该使用逆变,这也被称为 PECS 原则 (Produer Extends, Consumer Super)。
    楼主如果感兴趣的话,还可以搜一下 Java 类型系统的协变、逆变看一下
    HiJackXD
        11
    HiJackXD  
       2018-12-06 08:54:56 +08:00 via iPhone
    @lhx2008 如果现在又有个方法 只支持传入 T 及其父类 ,那么楼主例子中的方法返回的对象就必须手动转一次才能传入这个方法。
    lhx2008
        12
    lhx2008  
    OP
       2018-12-06 08:55:22 +08:00 via Android
    @wqlin 就拿 copy 函数说吧,T 的类型是第一个参数定的,super 不 super 都一样。第二个参数只要是 extendes 第一个参数的 T 就行了。加了 super 不是画蛇添足?
    wqlin
        13
    wqlin  
       2018-12-06 09:03:45 +08:00   ❤️ 1
    @lhx2008 #12 第二个参数 extends T 是不能调用 add 方法的,会报编译错误的。
    比如申明了
    ```
    List<? extends Number> myNums = new ArrayList<Integer>();
    ```
    只能从 myNums 中读取元素,赋值给 Number 类型(还不能是其他类型):
    ```
    Number n = myNums.get(0);
    ```
    如果调用 add 会直接报编译错误:
    ```
    myNums.add(45L); //compiler error
    ```
    类似的,super 只能写不能读:

    ```
    List<? super Number> myNums = new ArrayList<>();
    myNums.add(1L); // legal
    myNums.add(0.1); // legal
    ```
    读会报错:
    ```
    Number myNum = myNums.get(0); //compiler-error
    ```
    sagaxu
        14
    sagaxu  
       2018-12-06 09:05:16 +08:00 via Android   ❤️ 1
    这是泛型的协变和逆变,pecs 原则了解一下
    https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super

    JAVA 开发竟然没看过 effective java?
    lhx2008
        15
    lhx2008  
    OP
       2018-12-06 09:08:20 +08:00
    @HiJackXD 如果传入 ListIterator<? super Integer> 无论参数加不加 extends T,都返回 Object
    如果传入 ListIterator<? super Integer> ,无论加不加 extends T,返回都是 Integer,没有区别
    lhx2008
        16
    lhx2008  
    OP
       2018-12-06 09:11:55 +08:00
    @wqlin 我知道,现在问题不是泛型扩大的问题,我把 copy 函数复制出来

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
    throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
    (src instanceof RandomAccess && dest instanceof RandomAccess)) {
    for (int i=0; i<srcSize; i++)
    dest.set(i, src.get(i));
    } else {
    ListIterator<? super T> di=dest.listIterator();
    ListIterator<? extends T> si=src.listIterator();
    for (int i=0; i<srcSize; i++) {
    di.next();
    di.set(si.next());
    }
    }
    }


    我们,现在改它第一个参数的泛型,改成 T

    public static <T> void copy(List<T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
    throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
    (src instanceof RandomAccess && dest instanceof RandomAccess)) {
    for (int i=0; i<srcSize; i++)
    dest.set(i, src.get(i));
    } else {
    ListIterator<? super T> di=dest.listIterator();
    ListIterator<? extends T> si=src.listIterator();
    for (int i=0; i<srcSize; i++) {
    di.next();
    di.set(si.next());
    }
    }
    }

    我的前提想法是,这两个函数没有区别,所以我得出的结论是,第一个函数加 super T 是没有意义的。
    aaronysj
        17
    aaronysj  
       2018-12-06 09:14:57 +08:00
    不是一个可以能传子类对象一个只能传自己吗
    lhx2008
        18
    lhx2008  
    OP
       2018-12-06 09:15:44 +08:00
    突然在 stackoverflow 上面看到了 https://stackoverflow.com/questions/34985220/differences-between-copylist-super-t-dest-list-extends-t-src-and-co
    总算有人和我想法一样了,好累
    liuxey
        19
    liuxey  
       2018-12-06 09:20:14 +08:00
    这就是类型限定,第一个入参 List 泛型可以是 T 的子类,而第二个只能是 List<T>
    用 wqlin 的例子就是第一个能传 List<Number> List<Integer> List<Double> 等。。然后返回 Number
    而第二种写法只能传 List<Number>返回 Number
    wqlin
        20
    wqlin  
       2018-12-06 09:26:20 +08:00 via Android
    @lhx2008 有区别啊。比如说 T 是 Number,你的 copy 只能将 List<Integer> 拷贝到 List<Number> 中。那如果我想将
    List<Integer> 拷贝到 List<Object> 就要用第一种写法了
    lhx2008
        21
    lhx2008  
    OP
       2018-12-06 09:31:36 +08:00
    @wqlin 但是 T 不是你想定就定的,是按第一个参数的泛型定的

    你传 copy (List<Object> ,List<Integer>) ,T 就是 Object
    你传 copy(List<Number>, List<Integer> ),T 就是 Number

    都没有问题
    szq8014
        22
    szq8014  
       2018-12-06 09:33:47 +08:00
    协变 & 逆变 +1
    wqlin
        23
    wqlin  
       2018-12-06 09:40:05 +08:00 via Android
    @lhx2008 我到没怎么用过 Collections 中的 get 函数,只是表达下 T 和 ? super T 区别。不过为啥是第一个参数而不是第二个参数?
    Foredoomed
        24
    Foredoomed  
       2018-12-06 09:42:08 +08:00
    super 的作用并不是你能放什么类型在这个集合里,而是指定了集合的引用类型,具体的自己网上搜吧。
    LucasLee92
        25
    LucasLee92  
       2018-12-06 09:43:11 +08:00
    @lhx2008 两个例子不都是用父类来接受子类,java oo 思想的一种表现
    ZiLong
        26
    ZiLong  
       2018-12-06 10:28:18 +08:00
    @lhx2008 恩,这个链接解释的还是蛮清楚的
    Raymon111111
        27
    Raymon111111  
       2018-12-06 10:36:04 +08:00
    可以传 Integer 返回 Number 啊
    FrankFang128
        28
    FrankFang128  
       2018-12-06 13:54:38 +08:00
    @lhx2008 List<T> 只能把元素当做 T 操作,List<? extends T> 可以把元素当做 T 的子类操作,还是不一样的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3013 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 14:40 · PVG 22:40 · LAX 06:40 · JFK 09:40
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.