1.Java泛型的介绍
- 在JDK 1.5之前,Java是没有泛型功能的,那个时候诸如List之类的数据结构可以存储任意类型的数据,取出数据的时候也需要手动向下转型才行,这不仅麻烦,而且很危险。比如说我们在同一个List中存储了字符串和整型这两种数据,但是在取出数据的时候却无法区分具体的数据类型,如果手动将它们强制转成同一种类型,那么就会抛出类型转换异常。
- JDK1.5开始支持泛型,但是Java的泛型不支持协变和逆变,因为可能会带来bug。
- 不过在某些场景下,支持协变和逆变并不会带来问题,因此开放 ? extends T 来支持协变,但是禁止T出现在输入的位置;开放 ? super T 来支持逆变,但是禁止T出现在输出的位置上。
2.协变和逆变的概念
在编程语言中,有两个特殊性质分别叫:
- 协变 covariance:子类的泛型类型,也属于泛型类型的子类(即,子类的泛型类型对象,可以赋值给父类的泛型类型的引用)
- 逆变 contravariance:父类的泛型类型,属于泛型类型的子类(即,父类的泛型类型对象,可以赋值给子类的泛型类型的引用)
3.Java中协变和逆变的现状
但是,Java的泛型是不支持协变和逆变的,如下的代码是错误的:
TextView textView = new Button(context);
// 👆 这是多态,因为Button是TextView的子类(这是Android中Java的知识)
List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = buttons;
// 👆 多态用在这里会报错 incompatible types: List<Button> cannot be converted to List<TextView>
这是因为Java 的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。
在 Java 里用数组做类似的事情,是不会报错的,这是因为数组并没有在编译时擦除类型:
TextView[] textViews = new TextView[10];
Button[] buttons1 = new Button[10];
textViews = buttons1;
4.满足Java的语法以及受它的约束,以此来使用协变与逆变
通配符 ? 号,? extends T 表示类型的上界(协变),? super T 表示类型的下界(逆变)
- 协变的约束:在使用这个对象时,不能调用它的参数中包含类型参数(即中的T)的方法,也不能给它的包含类型参数的字段赋值,除了空值。简单来说,就是你只能用它,不能修改它。在只想使用,不需要修改的情况下,可以使用协变,以此来扩大变量或参数的接收范围,让程序更灵活。
- 逆变的约束:在使用这个对象时,不能调用返回值包含类型参数的方法,也不能获得包含类型参数的字段值
有人总结为 PECS法则:" Producer extends, Consumer super "
生产者(这个类型只用来产出)使用 extens,消费者(这个类型只用来消费)就使用 super
5.约束分析
为什么 ? extends T 只支持针对类型参数的产出呢?因为在这种场景下,我们按道理来说应该是获得T类型的对象中的一些属性,那么只要是T的子类,就必定有T的字段和方法,这是Java中继承的性质,那么针对类型参数的产出,就必定是不会出错的;但是我们关于类型参数的修改却是不行的,因为这个对象是T的子类,我们既不能将父类赋值给子类,我们也不能将同属T子类的兄弟类赋值给它,这都是逻辑不合理的。
为什么 ? super T 只支持针对类型参数的消费呢?因为在这种场景下,我们向对象中设置类型参数是合理的,因为是T的父类,因此我们将子类设置到父类中本身就是一个正常的操作,或者说这本来就是多态的意义;但是如果从对象获得关于类型参数的产出,由于是T的父类,所以说它并不一定有T的属性,这个是无法保证的。
6.非限定通配符
如果单独使用 ? 号,而不带extends和super,即非限定通配符,它使得可以接收任何泛型类型对象
private static void printCollection(List<?> originList) {
for (Object i : originList) {
System.out.println(i);
}
}
当使用?时,这表示它可以包含任何类型的对象,这在读取操作中非常有用,因为你可以从中获取任何类型的对象,它们都会被视为Object类型。
它其实就相当于 ? extends Object。所以换句话来说,它只拥有协变的特性,获得的对象都是Object类型,且不允许进行修改。
7.JDK中协变举例与分析
- 例子一:List.addAll()
List ->
boolean addAll(Collection<? extends E> c);
这个方法的功能是将c追加到调用方list的末尾。
为什么支持协变呢,因为该函数实际上是将c中的各元素依次添加到调用方list中,而子类赋值给父类本身就是合理的。
使用:
List<Runnable> runnables = new ArrayList<>();
List<Thread> threads = new ArrayList<>();
runnables.addAll(threads);
- 例子二:Collections.unmodifiableList()
Collections ->
public static <T> List<T> unmodifiableList(List<? extends T> list)
该方法的功能是返回指定list的不可变视图。
这里的list是支持协变,因为你传入子类的泛型类型,最终返回值是父类的泛型类型,这很合理。
8.JDK中逆变举例与分析
- 例子一:Collections.sort()
Collections ->
public static <T> void sort(List<T> list, Comparator<? super T> c)
这是一个根据c来对list进行排序的函数。
这里的c为什么可以支持逆变呢?因为传入给c的对象泛型是T的父类,那么用父类中的属性来进行排序那有什么问题呢。例如狗继承自动物,我用动物的身高来进行排序,那狗狗list根据这个标准来排序是没有问题的。
使用:
public class AnimalComparetor implements Comparator<Animal> {
@Override
public int compare(Animal o1, Animal o2) {
return o1.height.compareTo(o2.height);
}
}
Collections.sort(dogList, new AnimalComparetor());
- 例子二:Collections.copy()
Collections ->
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());
}
}
}
这是一个将src复制到dest中的函数。
这里的dest支持逆变,src支持协变,因为是将src中的元素依次复制到dest这个list中,而子类赋值给父类本身就是没有问题,因此这里分别开放协变和逆变可以增加函数的应用范围。且可以看到,dest针对T只进行了设入,src针对T只进行了读取。
- 例子三:Stream.forEach()
Stream<T> ->
void forEach(Consumer<? super T> action);
这是一个用于遍历流中的数据的方法。
这里支持逆变,即遍历项时可以直接得到父类。这当然是合理的,这是主动收窄范围了,因为只能拿到父类的属性和方法了。
使用:
List<Sportsman> sportsmanList = new ArrayList<>();
sportsmanList.add(new Sportsman("YaoMing", 40));
sportsmanList.add(new Sportsman("LiNing", 60));
sportsmanList.add(new Sportsman("MaLong", 38));
sportsmanList.stream().forEach(new Consumer<AbleBodiedPerson>() {
@Override
public void accept(AbleBodiedPerson sportsman) {
System.out.println("祖籍 " + sportsman.ancestral );
}
});
- 例子四:Stream.filter()
Stream<T> ->
Stream<T> filter(Predicate<? super T> predicate);
该函数的功能是通过predicate中的条件,保留原流中符合条件的元素,返回新的流。
这里predicate的泛型类型支持逆变,因为用父类的字段或方法来约束子类是合理的,子类是可以调用父类的字段或方法的
使用:这里用String的父类CharSequence的方法length()来约束元素的长度
Pattern pattern = Pattern.compile("\\s+");
Stream<String> stream = pattern.splitAsStream(originStr); //originStr为hello there how are you doing today
Stream<String> streamFilter = stream.filter((CharSequence s) -> s.length() > 3);
streamFilter.forEach(System.out::println);
//log日志:
hello
there
doing
today
9.泛型声明设置上界
Java在声明泛型类(包括接口)或方法时,可以使用 extends 来设置上界,将泛型类型参数限制为某个类型的子集。还可以设置多重上界。
class Monster<T extends Animal> {
...
}
//多重上界
class Monster<T extends Animal & Food> {
...
}
但是super在泛型方法或类(包括接口)中用于类型参数的声明是不允许的.
注意:这里说的泛型设置上界是在类或方法声明的时候,和前面讲的声明变量时的泛型类型是完全不同的东西,这里并没有 ?
参考文章:
Kotlin 的泛型