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

2018-12-05 23:38:37 +08:00
 lhx2008

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) 是一样的,那为什么要多此一举呢?

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

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


我有个想法,如果要证明这两种写法确实不一样,可以写一段代码调用这两种参数的写法,一个可以传进去,一个传不进去,但是我想不出来。
wqlin
2018-12-06 08:50:53 +08:00
谈一下我的理解。
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
2018-12-06 08:54:56 +08:00
@lhx2008 如果现在又有个方法 只支持传入 T 及其父类 ,那么楼主例子中的方法返回的对象就必须手动转一次才能传入这个方法。
lhx2008
2018-12-06 08:55:22 +08:00
@wqlin 就拿 copy 函数说吧,T 的类型是第一个参数定的,super 不 super 都一样。第二个参数只要是 extendes 第一个参数的 T 就行了。加了 super 不是画蛇添足?
wqlin
2018-12-06 09:03:45 +08:00
@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
2018-12-06 09:05:16 +08:00
这是泛型的协变和逆变,pecs 原则了解一下
https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super

JAVA 开发竟然没看过 effective java?
lhx2008
2018-12-06 09:08:20 +08:00
@HiJackXD 如果传入 ListIterator<? super Integer> 无论参数加不加 extends T,都返回 Object
如果传入 ListIterator<? super Integer> ,无论加不加 extends T,返回都是 Integer,没有区别
lhx2008
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
2018-12-06 09:14:57 +08:00
不是一个可以能传子类对象一个只能传自己吗
lhx2008
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
2018-12-06 09:20:14 +08:00
这就是类型限定,第一个入参 List 泛型可以是 T 的子类,而第二个只能是 List<T>
用 wqlin 的例子就是第一个能传 List<Number> List<Integer> List<Double> 等。。然后返回 Number
而第二种写法只能传 List<Number>返回 Number
wqlin
2018-12-06 09:26:20 +08:00
@lhx2008 有区别啊。比如说 T 是 Number,你的 copy 只能将 List<Integer> 拷贝到 List<Number> 中。那如果我想将
List<Integer> 拷贝到 List<Object> 就要用第一种写法了

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

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

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

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

© 2021 V2EX