0
点赞
收藏
分享

微信扫一扫

泛型:super、extend、?

舍予兄 2021-09-30 阅读 150
Java

先讲结果:

  • super 是指定泛型的『下界』;
  • extend 是指定泛型的『上界』;
  • ?是通配符;

看个例子

public class Main {
public static void main(String[] args) {
Class clz0 = new ArrayList<String>().getClass();
Class clz1 = new ArrayList<Integer>().getClass();
System.out.println("clz0 == clz1 ?= " + (clz0 == clz1));
}
}

打印结果如下:

明明 clz0 是 String 的,clz1 是 Integer 的,为什么打印出他们是同一种类型呢?


Java的泛型就是使用擦除来实现的(JDK最开始是不支持的,JDK1.5 版本引进的概念,为了兼容以前,在编译成 class 后类型被擦除)。因此,当你在使用泛型的时候,任何信息都被擦除,你所知道的就是你在使用一个对象。所以 List< Integer> 和 List< String> 在运行时,会被擦除成他们的原生类型List。

再来看个例子

我们看到,IDE直接提示错误,编译报错。

那如何才能调用 print 方法呢?
这就是我们本篇内容要讲的了:。
我们可以将 Test<T> 改为 Test<T extend Printer>,通过指定泛型的上边界,告诉编译器泛型T必需具有类型Printer 或者从 Printer 导出的类型。

擦除带来的问题

泛型不能用于显性地引用运行时类型的操作之中,例如 转型,instanceof 和 new 操作(包括 new一个对象,new一个数组),因为所有关于参数的类型信息都在运行时丢失了,所以任何在运行时需要获取类型信息的操作都无法进行工作。

例如:

解决擦除带来的问题

1. 解决instanceof

使用 instanceof 会失败,是因为类型信息已经被擦除,因此我们可以引入类型标签Class< T>,就可以转用动态的 isInstance()。

class A{}
class B extends A {}

public class Main<T> {
private Class<T> clz;
public Main(Class<T> clz) {
this.clz = clz;
}

public boolean compare(Object o) {
return clz.isInstance(o);
}

public static void main(String[] args) {
Main<A> a = new Main<>(A.class);
System.out.println(a.compare(new A()));
System.out.println(a.compare(new B()));
}
}

打印结果:

解决创建类型实例

解决办法是使用工厂。

interface Factory<T>{
T create();
}

class Product<T> {
public <P extends Factory<T>> Product(P factory) {
factory.create();
}
}

class ProductFactory implements Factory<Integer> {
@Override
public Integer create() {
Integer num = 1;
System.out.println(num);
return num;
}
}

public class Main {
public static void main(String[] args) {
new Product<>(new ProductFactory());
}
}

打印结果:

解决创建泛型数组

不能创建泛型数组的情况下,一般的解决方案是使用 ArrayList 代替泛型数组。因为ArrayList 内部就是使用数组,因此使用 ArrayList 能够获取数组的行为,和由泛型提供的编译器的类型安全。

但是假如,某种特定的场合,你仍然要使用泛型数组,推荐的方式是使用 类型标签+Array.newInstance 来实现,并用注解 @SuppressWarnings(“unchecked”) 抑制住警告

public class Main<T> {
private Class<T> clz;
public Main(Class<T> clz) {
this.clz = clz;
}

@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[]) Array.newInstance(clz, size);
}

public static void main(String[] args) {
Main<Integer> main = new Main<>(Integer.class);
Integer[] as = main.create(5);
System.out.println(as.length);
}
}

打印结果:

边界

正是因为有了擦除,把类型信息擦除了,所以,用无界泛型参数调用的方法只是那些可以用object调用的方法。但是,如果给定边界,将这个参数限制为某个类型的子集,就可以使用这些类型子集来调用方法。

  • 指定上界
interface A { void a(); }

public class Main<T extends A> {
T t;
public Main(T t) {
this.t = t;
}
public void test() {
t.a();
}
}

可见,类型T已经可以调用 A 的 a 方法了。

  • 指定多个上界
interface A { void a(); }
interface B { void b(); }
interface C { void c(); }

public class Main<T extends A & B & C> {
T t;
public Main(T t) {
this.t = t;
}
public void test() {
t.a();
t.b();
t.c();
}
}

通配符和泛型上界和下界

class Fruit {}
class Apple extends Fruit {}

有一个最简单的容器:Plate类。表示盘子里可以放一个泛型的『东西』。我们可以对这个东西做最简单的『放』和『取』的动作:set() / get()

class Plate<T> {
private T item;
public Plate(T item) { this.item = item; }
public void set(T item) { this.item = item; }
public T get() { return this.item; }
}

现在我定义一个『水果盘子』,逻辑上水果盘子可以装苹果:

Plate<Fruit> p = new Plate<Apple>(new Apple());

很不幸的是:Java编译器不允许这个操作,会报错:『装苹果的盘子』无法转换成『装水果的盘子』

实际上编译器脑袋里认定的逻辑是:

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以,我们也就不能将 Plate<Apple>的引用传递给 Plate<Fruit>。
因此,为了让泛型用起来稍微舒服点,SUN的大脑们就想出了 <? extends T> 和 <? super T>的办法,以此让『水果盘子』和『苹果盘子』之间发生点关系。


上界<? extends Class>

可见,指定了下边界,却不能add任何类型,甚至Object都不行,除了 null,因为 null 代表任何类型。List< ? extends Fruit> 可以解读为,“具有任何从Fruit继承的类型”,但实际上,它意味着,它没有指定具体类型。对于编译器来说,当你指定了一个 List< ? extends Fruit>add 的参数也变成了“? extends Fruit”。因此编译器并不能了解这里到底需要哪种 Fruit 的子类型,因此他不会接受任何类型的 Fruit。

然而,containindexof 却能执行,这是因为,这两个方法的参数是 Object,不涉及任何的通配符,所以编译器允许它调用。

list.get(0) 能够执行是因为,当item在此list存在时,编译器能够确定他是Apple的子类,所以能够安全获得。

下界<? super Class>

这里可以看到,list.add(new Fruit()) 这句不能编译成功,这是因为 List< ? super Apple> 表示“具有Apple的父类的列表”。但是为什么 add(new Fruit())不能成功 呢?正是因为『?代表Apple的父类』,但是编译器不知道你要添加哪种Apple的父类,因此不能安全地添加。

对于 super,get 返回的是 Object,因为编译器不能确定列表中的是Apple的哪个子类,所以只能返回 Object。

PECS原则

  • 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
  • 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
  • 如果既要存又要取,那么就不要使用任何通配符。
举报

相关推荐

0 条评论