泛型:super、extend、?

2021-01-07  本文已影响0人  青叶小小

先讲结果:

看个例子

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 == clz1 ?= true

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

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

泛型信息只存在于代码编译阶段,编译器编译完带有泛形的java程序后,生成的class文件中与泛型相关的信息会被擦除掉,以此使程序运行效率不受到影响,这个过程叫做类型擦除,也就是说泛型类和普通类在 java 虚拟机内是一样的。

再来看个例子

泛型0.png

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

因为泛型的擦除,main 中传入的泛型在 Test 中变成了 Object,因此无法与 Printer 这个类绑定,也就无法调用 print 方法!

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

擦除带来的问题

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

例如:

if(obj instanceof T);
T t = new T();
T[] ts = new T[10];

解决擦除带来的问题

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()));
    }
}

打印结果:

true
true

解决创建类型实例

解决办法是使用工厂。

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());
    }
}

打印结果:

1

解决创建泛型数组

不能创建泛型数组的情况下,一般的解决方案是使用 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);
    }
}

打印结果:

5

边界

正是因为有了擦除,把类型信息擦除了,所以,用无界泛型参数调用的方法只是那些可以用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();
    }
}

这里需要注意的是,extends 后面跟的第一个边界,可以为类或接口,之后的均为接口

通配符和泛型上界和下界

为什么要用通配符和边界?

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编译器不允许这个操作,会报错:『装苹果的盘子』无法转换成『装水果的盘子』

error: incompatible types: Plate<Apple> can not be converted to Plate<Fruit>

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

  • 苹果 IS-A 水果
  • 装苹果的盘子 NOT-IS-A 装水果的盘子

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


上界<? extends Class>

111.png 上边界.jpg

可见,指定了下边界,却不能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>

222.png 下边界.jpg

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

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

PECS原则

上一篇 下一篇

猜你喜欢

热点阅读