泛型是java1.5版本引进的概念。
有两种定义:
1、在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象。
2、在程序编码中一些包含参数的类。其参数可以代表类或对象等等。
不论使用哪个定义,泛型的参数在真正使用泛型时都必须作出指明。
上面两种定义不是很好理解,个人理解的话,想象成可以在定义时替代整型、字符串类型、对象类的一个集合代表类。
下面这段代码,会输出什么?
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。泛型类型信息被擦除了。
答案是体现在类编译的时候。当JVM进行类编译时会进行泛型检查,如果一个集合被声明为String类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。
也就是说:泛型只存在与编译阶段,而不存在于运行阶段。在编译后的class文件中,是没有泛型这个概念的。
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类型。
上面有了一个父类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的之类,所以可以正常编译通过。
虽然通过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 关键字可以实现向上转型。但是却失去了部分的灵活性,即不能往其中添加任何东西,只能取出东西。
与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类型的变量指向取出的对象。
1、当你的情景是生产者类型,需要获取资源以供生产时,我们建议使用 extends 通配符,因为使用了 extends 通配符的类型更适合获取资源。
2、当你的场景是消费者类型,需要存入资源以供消费时,我们建议使用 super 通配符,因为使用 super 通配符的类型更适合存入资源。
3、如果你既想存入,又想取出,那么你最好还是不要使用 extends 或 super 通配符。
微信公众号:一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。