泛型

1、定义

泛型是java1.5版本引进的概念。
有两种定义:

1、在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象。
2、在程序编码中一些包含参数的类。其参数可以代表类或对象等等。

不论使用哪个定义,泛型的参数在真正使用泛型时都必须作出指明。

上面两种定义不是很好理解,个人理解的话,想象成可以在定义时替代整型、字符串类型、对象类的一个集合代表类。

2、常用术语

2.1、类型擦除

下面这段代码,会输出什么?

		List<String> sList = new ArrayList<>();
        List<Integer> iList = new ArrayList<>();
        System.out.println(sList.getClass() == iList.getClass());

输出结果:

true

泛型信息只存在代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
对于List 和 List而言它们的Class类型在 jvm 中的 Class 都是 List.class。泛型类型信息被擦除了。

指定的 String 和 Integer 到底体现在哪里呢?

答案是体现在类编译的时候。当JVM进行类编译时会进行泛型检查,如果一个集合被声明为String类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。
也就是说:泛型只存在与编译阶段,而不存在于运行阶段。在编译后的class文件中,是没有泛型这个概念的。

2.2、通配符

Rider<? extends Animal> rider1 = new Rider<Horse>();
Rider<? super Horse> rider4 = new Rider<Animal>();

上段代码中 extends 和 super关键字就是泛型通配符。

在介绍泛型通配符之前,需要对编译时类型和运行时类型有一个基本的了解,才能更好的理解通配符的使用。

先定义一个父类(Animal) 和一个子类(Horse) :

public class Animal {
}
public class Horse extends Animal {
}

再创建一个Horse对象

Horse horse = new Horse();

上面这段代码中,horse实例指向的对象,在编译时类型和运行时类型都是Horse类型。

但是我们有时候可以使用下面的这种写法

Animal horse1 = new Horse();

使用Animal类型的变量指向了一个Horse对象 ,因为在java中允许把一个子类的对象直接赋值给一个父类的引用变量,我们称之为[向上转型]。

但是问题来了,此时horse1变量所指向的对象,在编译时类型和运行时类型是什么呢?
答案是:horse1变量所指向的对象,其在编译时的类型就是Animal类型,而在运行时的类型就是Horse类型。

这是为什么呢?
因为在编译的时候,JVM只知道Animal类变量指向了一个对象,并且这个对象是Animal的之类的对象或自身对象,其具体的类型并不确定,有可能是Horse,也有可能是Mule类型。而为了安全方面考虑,JVM此时将horse1变量指向的对象定义为Animal类型。因为无论其实Horse类型还是Mule类型,他们都可以安全转为Animal类型。
而在运行时阶段,JVM通过初始化知道了它指向一个Horse对象,所以在其运行时的类型就是Horse类型。

2.2.1、向上转型

上面有了一个父类Animal和一个子类Horse,这时候再增加一个骑手类,Rider类。Rider类定义了一些基本的行为:

public class Rider<T> {
    private List<T> list;

    public Rider() {
        list = new ArrayList<T>();
    }

    public void ride(T item) {
        list.add(item);
    }

    public T get() {
        return list.get(0);
    }
}

骑手类Rider类定义了一个T泛型类型,可以接受任何类型,如骑手可以骑马,骑一些动物等,主要是具备骑这个行为。

例如定义一个骑手,代码片段如下:

Rider<Animal> rider=new Rider<Animal>();

例如骑手骑乘一些动物,代码片段如下:

rider.ride(new Animal());
rider.ride(new Horse());

按照java向上转型的原则,我们可以这样定义一个骑手:

Rider<Animal> rider1=new Rider<Horse>();

但上面这段代码在编译的时候会报错:Incompatible types.。
按理说,这种写法应该没问题,因为java支持向上转型。
错误的原因就是:java并不支持泛型的向上转型,所以不能够使用上面的写法。

但是可以采用泛型通配符来解决这个问题。
正确的代码片段如下:

Rider<? extends Animal> rider1 = new Rider<Horse>();

上面这行代码标识:Rider可以指向任何Animal类对象,或者任何Aimal的子类对象。
Horse是Animal的之类,所以可以正常编译通过。

2.2.2、extends通配符的缺陷

虽然通过extends通配符支持了java泛型的向上转型,但是这种方式是有缺陷的,那就是:其无法向Rider中添加任何对象,只能从中读取对象。

Rider<? extends Animal> rider1 = new Rider<Horse>();
rider1.ride(new Horse());//编译报错
rider1.ride(new Mule());//编译报错
Animal animal1 = rider1.get();//编译成功

可以看到,当我们让骑手骑马和骡子时,会发现编译错误。但是我们可以从中取出被骑的动物。
那为什么我们会无法让骑手骑动物呢?
这个还得从这还得从我们对骑手的定义说起。

Rider<? extends Animal> rider = new Rider<XXX>();

在上面对骑手的定义种,rider可以指向任何Animal对象,或者是Animal子类对象。也就是说,plate属性指向的对象其在运行时可以是Horse类型,也可以是Mule类型。
如下面的代码片段都是正确的:

        Rider<? extends Animal> rider1 = new Rider<Horse>();
        Rider<? extends Animal> rider2 = new Rider<Mule>();

在还没有具体运行时,JVM并不知道我们要让骑手骑什么动物,到底是马还是骡子,完全不知道。既然不能确定要骑的什么动物,那么JVM就干脆什么都不给放,避免出错。
鉴于此,所以当使用extends通配符时,无法向其中添加任何东西。
但是为什么可以取出数据呢?
因为无论是取出马还是骡子,都可以通过向上转型用Animal类型的变量指向它,这在java中都是允许的。

Animal animal1 = rider1.get();//编译成功
Horse horse1 = rider1.get();//编译错误

在上面的代码片段中,当使用一个Horse类型的变量指向一个从骑手类里取出待骑对象马时,是会提示错误的。
所以当使用extends通配符时,可以取出所有东西。

总结,通过 extends 关键字可以实现向上转型。但是却失去了部分的灵活性,即不能往其中添加任何东西,只能取出东西。

2.2.3、super通配符的缺陷

与extends通配符相似的而另一个通配符是:super通配符,其特性与extends完全相反。同样的也有缺陷:super通配符可以存入对象,但是取出对象的时候受到限制。
使用示例片段如下:

		Rider<? super Horse> rider3 = new Rider<Horse>();
        Rider<? super Horse> rider4 = new Rider<Animal>();
        Rider<? super Horse> rider5 = new Rider<Object>();

上面代码表示:Rider 变量可以指向一个特定类型的 Rider 对象,只要这个特定类型是 Horse 或 Horse 的父类。Object也是Animal的父类,同理关系可以继承。

也就是说rider5指向的具体类型可以是Horse的父级,JVM在编译的时候肯定无法判断具体是哪个类型。但是JVM能确定的是,任何Horse的子类都可以转为Horse类型,但任何Horse的父类都无法转换为Horse类型。

所以对于使用了super通配符的情况,我们只能存入T类型及T类型的子类对象。

		rider4.ride(new Horse());
        rider4.ride(new Warhorse());
        rider4.ride(new Animal());//编译错误

当我们传入Horse的父类Animal时就会报编译错误。

而当我们取出数据的时候,也是类似的,JVM在编译的时候知道,我们具体的运行时类型可以是任何Horse的父类,那么为了安全起见,我们就用一个最顶层的父级来指定取出的数据,这样就可以避免发生强制类型转换异常了。

		Object object = rider4.get();
        Horse object = rider4.get();//编译错误

从上面的代码可以知道,当使用Horse类型的变量指向Rider取出的对象,会出现编译错误,而使用Objec类型的变量指向Rider取出的对象,则可以正常通过。
也就是说对于使用了super通配符的情况,我们取出的时候只能用Object类型的变量指向取出的对象。

3、总结

1、当你的情景是生产者类型,需要获取资源以供生产时,我们建议使用 extends 通配符,因为使用了 extends 通配符的类型更适合获取资源。

2、当你的场景是消费者类型,需要存入资源以供消费时,我们建议使用 super 通配符,因为使用 super 通配符的类型更适合存入资源。

3、如果你既想存入,又想取出,那么你最好还是不要使用 extends 或 super 通配符。

泛型_第1张图片

微信公众号一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。

你可能感兴趣的:(Java,java,泛型,Generic)