概述
Java基础进阶--泛型
- 什么是泛型?
- 为什么要使用泛型?
- 没有泛型的世界
- 有泛型的世界
- 泛型方法
- 泛型的类型限定
- 泛型类型的多重限定
- 泛型类型的擦除
- 桥方法
- 泛型与反射
- 使用泛型带来的副作用
- 使用泛型后,不能传入基本类型
- 使用泛型后,不能使用 instanceof 操作符
- 泛型在静态方法和静态中的问题
- 泛型类型中的方法冲突
- 没法创建泛型实例
- 数组是没有泛型的
- 泛型的继承和子类型
- 通配符
- 上届通配符
- 下届通配符
- 非限定通配符
- PECS原则
- 通过反射调用被通配符限定的方法。
- 泛型的灵活转型
什么是泛型?
泛型,就是对传入类型的一种限定,它是JDK5中引入的一种参数化类型特性。
那么,为什么要做这样的限定呢,或者说为什么要使用泛型呢。
为什么要使用泛型?
在说为什么使用泛型之前呢,我们讲解一个小故事。
有一对男女朋友,小明和小莉。小莉呢,出生住在山东,山东盛产苹果,所以她不爱吃苹果,她爱吃香蕉。而小明呢,出生在广西,广西盛产香蕉,所以他不爱吃香蕉,他爱吃苹果。
过年了,小明带小莉回家去看望家长,小明拿了一个水果盘子,让他的妈妈洗点水果给小莉吃,这时候小明出去买菜。小明的妈妈 就想:“我们这里盛产香蕉,香蕉早都吃够了,我给小莉准备苹果吃吧”。小明的妈妈就给小莉上了一盘子苹果。
结果小莉看到一盘子苹果,气的夺门而出。
没有泛型的世界
我们这里让故事中的人物登场:
class XiaoMing{
public Plate createPlate() {
return new Plate();
}
}
class XiaoLi{
public void eat(Banana banana){
}
}
class Fruit{
}
class Apple extends Fruit{
}
class Banana extends Fruit{
}
class Plate{
private Fruit fruit;
public void setFruit(Fruit fruit){
this.fruit = fruit;
}
public Fruit getFruit(){
return this.fruit;
}
}
class XiaoMingMa{
private Plate plate;
public Plate getPlate() {
return plate;
}
public void setPlate(Plate plate) {
this.plate = plate;
}
}
public class Story {
public static void main(String[] args) throws Exception {
XiaoMing xiaoMing = new XiaoMing();
XiaoLi xiaoLi = new XiaoLi();
XiaoMingMa xiaoMingMa = new XiaoMingMa();
Plate plate = xiaoMing.createPlate();
xiaoMingMa.setPlate(plate);
plate.setFruit(new Apple());
xiaoLi.eat((Banana) plate.getFruit());
}
}
这里我们看到因为我们在放水果的时候,没有做任何的限定,结果导致小莉想要吃的是香蕉,结果盘子里放的是苹果,导致程序的运行崩溃。运行后我们发现会报出这样的异常:
Exception in thread "main" java.lang.ClassCastException
类转换错误,也就是说苹果不能转成香蕉。
有泛型的世界
还是这个小故事:
小明听到小莉夺门而出,打了个电话好说歹说给小莉劝了回来,他们和好如初,决定去女方的家里再见见家长,
如果合适就选择结婚的日子。
小明和小莉来到了小莉的家里,小莉很细心,她告诉妈妈,小明家里盛产香蕉,所以他不喜欢吃香蕉,他喜欢吃
苹果。不要以为我们这里盛产苹果,我们吃太多了,就认为他也不喜欢。
小莉的妈妈听了小莉的话,给小明洗了一盘子苹果,结果小明吃的很开心,小莉的妈妈觉得双方很合适,结婚的
日子也定了下来。
这里我们使用泛型,将这个故事继续演绎下去:
class XiaoMing{
public void eat(Apple apple) {
}
}
class XiaoLi{
public Plate<Apple> createPlate() {
return new Plate<Apple>();
}
}
class Fruit{
}
class Apple extends Fruit{
}
class Banana extends Fruit{
}
public class Plate<T> {
private T fruit;
public T getFruit() {
return fruit;
}
public void setFruit(T t) {
this.fruit = t;
}
}
public class XiaoLiMa {
private Plate<Apple> plate;
public Plate<Apple> getPlate() {
return plate;
}
public void setPlate(Plate<Apple> plate) {
this.plate = plate;
}
}
public class Story {
public static void main(String[] args) throws Exception {
XiaoMing xiaoMing = new XiaoMing();
XiaoLi xiaoLi = new XiaoLi();
XiaoLiMa xiaoLiMa = new XiaoLiMa();
Plate<Apple> plate = xiaoLi.createPlate();
xiaoLiMa.setPlate(plate);
plate.setFruit(new Apple());
xiaoMing.eat(plate.getFruit());
}
}
这里我们对Plate使用了泛型,将Plate改成Plate<T>,然后再小莉创建盘子的时候,就规定了泛型类的传入类型只能是苹果,那么这样,这个盘子就只能装入苹果,如果setFruit传入一个香蕉对象的时候,就会报编译期错误。
所以这里使用泛型,就是为了限定传入的类型,将可能发生的错误提前显示在编译期,这样使程序更健壮。
这里我们发现,小明在从盘子里拿出来苹果吃的时候,并没有像之前小莉那样,将拿到的水果强行转换成香蕉。这就是因为我们使用了泛型,在使用的时候不需要进行强制类型转换,这就是使用泛型的另一大好处,可以更灵活、更方便的进行转型。
泛型方法
前面我们讲到的是在类中限定一个传入的类型,那么我们可不可以不在类中限定,而在方法之中做限定呢,答案是可以的。这种方法就是泛型方法,在修饰限定符和返回值中间加入<T>,来进行类型限定,在调用该方法时,传入指定的类型。
泛型的类型限定
在现实生活中,水果盘子按理说应该只可以装水果,那么我们可不可以规定一下,这个盘子是一个只能装水果的盘子呢。我们在这里再修改一下Plate这个泛型类。
public class Plate<T extends Fruit> {
private T fruit;
public T getFruit() {
return fruit;
}
public void setFruit(T t) {
this.fruit = t;
}
}
这里我们限定了T类型只能为Fruit的派生类,所以这里这个盘子就只能装水果或者水果类的派生类,不能装其他的东西了。
泛型类型的多重限定
这里我们T类型限定了继承Fruit这个类,那么这里可不可以同时还实现多个接口呢?答案是可以的。
比如这里我们有类A 类B 接口C 接口D,那么我们要限定这个泛型的类型要继承A类同时实现接口C和接口D,这样我们应该怎么写呢?
class A {
}
class B {
}
interface C {
}
interface D {
}
class Plate<T extends A & C & D>{
}
也就是说我们只要在中间加上这个符号&就可以了。
接下来我们注意了,我换几个写法,看看可不可以。
class Plate<T extends A & B>//不可以,因为类是单继承的,我们这里A和B都是类,编译器报错。
class Plate<T extends C & D>//可以,因为类可以实现多个接口,所以这里没有问题。
class Plate<T extends C & D & A>//不可以,当有多个继承关系的时候,要将类放在接口的前面,所以A要
放在前边才可以。
泛型类型的擦除
由于泛型是在SDK5之后才加入的,所以我们要考虑向下兼容的问题,那么实际上,在虚拟机里,我们是没有泛型这一个概念的,也就是说实际上,JAVA的泛型是一个伪泛型。
具体表现在哪里呢,这里我将Plate类转成字节码,我们再看一下。
// class version 51.0 (51)
// access flags 0x21
// signature <T:Lcom/example/java/demo/generic/Fruit;>Ljava/lang/Object;
// declaration: com/example/java/demo/generic/Plate<T extends com.example.java.demo.generic.Fruit>
public class com/example/java/demo/generic/Plate {
// compiled from: Plate.java
// access flags 0x2
// signature TT;
// declaration: t extends T
private Lcom/example/java/demo/generic/Fruit; t
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/Plate; L0 L1 0
// signature Lcom/example/java/demo/generic/Plate<TT;>;
// declaration: this extends com.example.java.demo.generic.Plate<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature ()TT;
// declaration: T getT()
public getT()Lcom/example/java/demo/generic/Fruit;
L0
LINENUMBER 7 L0
ALOAD 0
GETFIELD com/example/java/demo/generic/Plate.t : Lcom/example/java/demo/generic/Fruit;
ARETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/Plate; L0 L1 0
// signature Lcom/example/java/demo/generic/Plate<TT;>;
// declaration: this extends com.example.java.demo.generic.Plate<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void setT(T)
public setT(Lcom/example/java/demo/generic/Fruit;)V
L0
LINENUMBER 11 L0
ALOAD 0
ALOAD 1
PUTFIELD com/example/java/demo/generic/Plate.t : Lcom/example/java/demo/generic/Fruit;
L1
LINENUMBER 12 L1
RETURN
L2
LOCALVARIABLE this Lcom/example/java/demo/generic/Plate; L0 L2 0
// signature Lcom/example/java/demo/generic/Plate<TT;>;
// declaration: this extends com.example.java.demo.generic.Plate<T>
LOCALVARIABLE t Lcom/example/java/demo/generic/Fruit; L0 L2 1
// signature TT;
// declaration: t extends T
MAXSTACK = 2
MAXLOCALS = 2
}
这里通过字节码我们发现,get和set方法的参数是Fruit,并没有任何与泛型有关的信息。也就是说泛型的类型被擦除掉了。如果限定了一个继承的类或者实现的接口,那么就会将其擦除成这个类,或接口。也就是说,在字节码运行代码的时候,是没有泛型的类型的。
这里我们是有限定T类型必须是Fruit的派生类,那么如果T类型没有任何限定呢?
// class version 51.0 (51)
// access flags 0x21
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: com/example/java/demo/generic/Plate1<T>
public class com/example/java/demo/generic/Plate1 {
// compiled from: Plate1.java
// access flags 0x2
// signature TT;
// declaration: t extends T
private Ljava/lang/Object; t
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/Plate1; L0 L1 0
// signature Lcom/example/java/demo/generic/Plate1<TT;>;
// declaration: this extends com.example.java.demo.generic.Plate1<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature ()TT;
// declaration: T getT()
public getT()Ljava/lang/Object;
L0
LINENUMBER 7 L0
ALOAD 0
GETFIELD com/example/java/demo/generic/Plate1.t : Ljava/lang/Object;
ARETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/Plate1; L0 L1 0
// signature Lcom/example/java/demo/generic/Plate1<TT;>;
// declaration: this extends com.example.java.demo.generic.Plate1<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void setT(T)
public setT(Ljava/lang/Object;)V
L0
LINENUMBER 11 L0
ALOAD 0
ALOAD 1
PUTFIELD com/example/java/demo/generic/Plate1.t : Ljava/lang/Object;
L1
LINENUMBER 12 L1
RETURN
L2
LOCALVARIABLE this Lcom/example/java/demo/generic/Plate1; L0 L2 0
// signature Lcom/example/java/demo/generic/Plate1<TT;>;
// declaration: this extends com.example.java.demo.generic.Plate1<T>
LOCALVARIABLE t Ljava/lang/Object; L0 L2 1
// signature TT;
// declaration: t extends T
MAXSTACK = 2
MAXLOCALS = 2
}
通过字节码我们可以看到,类型被擦除掉,变成了Object类。也就是说没有限定的T类型,实际上就是<T extends Object>这个限定类型。
桥方法
何谓桥方法?我们这里还是先看一段代码。
这里呢,我们将Plate这个类定义成一个接口。
public interface Plate<T> {
void set(T t);
T get();
}
然后我们再创建一个AIPlate来实现这个接口。这里我们将类型限定为只能为Fruit类的派生类。
public class AIPlate<T extends Fruit> implements Plate<T> {
private T t;
@Override
public void set(T t) {
this.t = t;
}
@Override
public T get() {
return t;
}
}
这里我们在转成字节码之前,思考这样一个问题。Plate<T>这个接口在转成字节码的时候,根据我们前面讲过的类型擦除原则,转成字节码以后set get方法的参数被擦除成了Object。那么AIPlate这个类呢,set get方法的参数会被擦除成Fruit。那么问题就出现了,我们实现了Plate这个类,但是当我们的AIPlate这个类被擦除成了Fruit,是不是我们就没有实现Plate的set和get方法了?这个时候,桥方法就应运而生,这里我们还是转成字节码来看一下。
// class version 51.0 (51)
// access flags 0x21
// signature <T:Lcom/example/java/demo/generic/Fruit;>Ljava/lang/Object;Lcom/example/java/demo/generic/Plate<TT;>;
// declaration: com/example/java/demo/generic/AIPlate<T extends com.example.java.demo.generic.Fruit> implements com.example.java.demo.generic.Plate<T>
public class com/example/java/demo/generic/AIPlate implements com/example/java/demo/generic/Plate {
// compiled from: AIPlate.java
// access flags 0x2
// signature TT;
// declaration: t extends T
private Lcom/example/java/demo/generic/Fruit; t
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/AIPlate; L0 L1 0
// signature Lcom/example/java/demo/generic/AIPlate<TT;>;
// declaration: this extends com.example.java.demo.generic.AIPlate<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void set(T)
public set(Lcom/example/java/demo/generic/Fruit;)V
L0
LINENUMBER 8 L0
ALOAD 0
ALOAD 1
PUTFIELD com/example/java/demo/generic/AIPlate.t : Lcom/example/java/demo/generic/Fruit;
L1
LINENUMBER 9 L1
RETURN
L2
LOCALVARIABLE this Lcom/example/java/demo/generic/AIPlate; L0 L2 0
// signature Lcom/example/java/demo/generic/AIPlate<TT;>;
// declaration: this extends com.example.java.demo.generic.AIPlate<T>
LOCALVARIABLE t Lcom/example/java/demo/generic/Fruit; L0 L2 1
// signature TT;
// declaration: t extends T
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
// signature ()TT;
// declaration: T get()
public get()Lcom/example/java/demo/generic/Fruit;
L0
LINENUMBER 13 L0
ALOAD 0
GETFIELD com/example/java/demo/generic/AIPlate.t : Lcom/example/java/demo/generic/Fruit;
ARETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/AIPlate; L0 L1 0
// signature Lcom/example/java/demo/generic/AIPlate<TT;>;
// declaration: this extends com.example.java.demo.generic.AIPlate<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1041
public synthetic bridge get()Ljava/lang/Object;
L0
LINENUMBER 3 L0
ALOAD 0
INVOKEVIRTUAL com/example/java/demo/generic/AIPlate.get ()Lcom/example/java/demo/generic/Fruit;
ARETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/AIPlate; L0 L1 0
// signature Lcom/example/java/demo/generic/AIPlate<TT;>;
// declaration: this extends com.example.java.demo.generic.AIPlate<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1041
public synthetic bridge set(Ljava/lang/Object;)V
L0
LINENUMBER 3 L0
ALOAD 0
ALOAD 1
CHECKCAST com/example/java/demo/generic/Fruit
INVOKEVIRTUAL com/example/java/demo/generic/AIPlate.set (Lcom/example/java/demo/generic/Fruit;)V
RETURN
L1
LOCALVARIABLE this Lcom/example/java/demo/generic/AIPlate; L0 L1 0
// signature Lcom/example/java/demo/generic/AIPlate<TT;>;
// declaration: this extends com.example.java.demo.generic.AIPlate<T>
MAXSTACK = 2
MAXLOCALS = 2
}
结果发现,除了两个擦除后生成了两个参数类型为Fruit的set get方法,还生成了两个参数类型为Object的set get方法。方法的名称分别为:
public synthetic bridge get()Ljava/lang/Object;
public synthetic bridge set(Ljava/lang/Object;)V
这里因为要保证多态性,在转成字节码的时候,自动生成了两个桥方法,从而实现了Plate接口的set和get方法。
我们再来看一看这个桥方法里究竟做了什么?
我们通过set方法发现,这里
CHECKCAST com/example/java/demo/generic/Fruit
INVOKEVIRTUAL com/example/java/demo/generic/AIPlate.set (Lcom/example/java/demo/generic/Fruit;)V
CHECKCAST
字节码指令的作用就是检查类型,并强转成特定类型,这里就是Fruit。
然后调用了参数为Fruit的set方法。
也就是说桥方法就是做了一个强转的操作,并且调用了本身的set方法。
泛型与反射
这里我们再看一段代码:
public class TestType {
Map<String, String> map;
public static void main(String[] args) throws Exception {
Field f = TestType.class.getDeclaredField("map");
System.out.println(f.getGenericType());
System.out.println(f.getGenericType() instanceof ParameterizedType);
ParameterizedType pType = (ParameterizedType) f.getGenericType();
System.out.println(pType.getRawType());
for (Type type : pType.getActualTypeArguments()) {
System.out.println(type);
}
System.out.println(pType.getOwnerType());
}
}
这里运行后发现,我们还是能拿到Map里的类型信息。
java.util.Map<java.lang.String, java.lang.String>
true
interface java.util.Map
class java.lang.String
class java.lang.String
null
我们前边有讲过,这里类型在运行期应该是已经被擦除了,那为什么还是能通过反射拿到类型信息呢?
实际上,虽然我们的类型在运行期被擦除了,但是泛型信息还是保留在了类常量池中。我们还是可以通过反射的方法拿到类型信息的。
使用泛型带来的副作用
使用泛型后,不能传入基本类型
这里还是以代码举例
ArrayList<int> ints = new ArrayList<>();
ArrayList<Integer> integers = new ArrayList<>();
这里,我们在泛型信息中,没法传入int类型了,因为之前有说过,在字节码中会擦除成Object类型,而Object类型是没法存放int类型的,所以这里泛型也不能传入基本类型。
使用泛型后,不能使用 instanceof 操作符
if(strings instanceof ArrayList<String>){}
这里 instanceof 操作符也是不可以用的,因为经过擦除后,ArrayList<String>只剩下原始类型,泛型信息String不存在了,所以没法使用 instanceof。
泛型在静态方法和静态中的问题
我们来看一段代码:
class NormalClass<T>{
public static T one;//无法使用,编译期错误
public static T test(T t){}//无法使用,编译期错误
public static <T> T test1(T t){return t}//可以使用
}
泛型类是在具体实例化的时候,将类型传入的。这里静态变量或者静态方法不需要实例化就可以使用,所以我们无法知道具体的类型是什么,所以这里无法使用泛型。
而下一个静态方法由于又定义了一个<T>,所以这个T并不是NormalClass类中的那个T,所以在使用这个静态方法的时候,我们需要传入一个类型去使用,那么这样我们是知道具体传入的类型的,所以这里可以使用泛型。
泛型类型中的方法冲突
@Override
public void set(T t) {
}
public void set(Object o){
}
这里我们定义了两个方法,虽然在源码中参数是不同的,一个是T 一个是Object,但是由于在运行期会将类型擦除,所以上边的方法实际上会变成set(Object t),这样就会导致方法重复定义了。
没法创建泛型实例
public static <E> void append(List<E> list) {
E elem = new E();
list.add(elem);
}
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance();
list.add(elem);
}
第一段代码编译期就会提示错误,由于这里E的类型我们不确定,所以没法生成新的实例。
但是通过第二段代码,知道了类的类型,我们通过反射,还是可以生成实例的。
数组是没有泛型的
在说到这个问题,我们首先来了解一下另一个事情。还是先通过下面一段代码:
//如果A extends B,C extends B
B[] bArray = new B[10];
A[] aArray = new A[10];
bArray = aArray;
bArray[0] = new C();//运行时错误,报ArrayStoreException
这里因为bArray已经变成了A类型的数组,所以不可以再放C的实例了。
那么就得出来一个结论,如果A是B的子类,那么A数组也是B数组的子类。
这种关系在java里有一个名字,叫做数组的协变。
这里,由于如果数组存在泛型,在运行时类型会被擦除,从而丢失了这样的协变关系,所以数组是不允许使用泛型的。
泛型的继承和子类型
这里还是用盘子来做比较,我们知道苹果是水果的一种,也就是说苹果和水果之间有继承关系。那么,苹果盘子和水果盘子之间是否存在继承关系呢?这里,我们写一段代码来验证。
Fruit apple = new Apple();
AIPlate<Fruit> aiPlate = new AIPlate<Apple>();\编译期错误。
这里我们看到,产生了编译期错误,也就是说苹果盘子和水果盘子之间是不存在任何继承关系的。
那么,我们又要思考了,如果泛型中的类型一样,而基本类型中存在继承关系呢?
还是通过一段代码来验证:
public class BigPlate<T> extends AIPlate<T> {
}
public class ColorPlate<K, T> extends BigPlate<T> {
}
Plate<Apple> aiPlate = new AIPlate<>();
Plate<Apple> bigPlate = new BigPlate<>();
Plate<Apple> colorPlate = new ColorPlate<>();
结果发现,是可以生成新的实例。也就是说存在继承关系的类之间,只要是相同泛型类型,就存在继承关系。这里我们建立了一个ColorPlate<K,T>,虽然多了一个泛型类型,但是只要T类型是相同的,这里的继承关系就是成立的。
通配符
那么,我们有没有办法使苹果盘子和水果盘子发生关系呢?当然,是可以的,这里我们就需要使用通配符。
AIPlate<? extends Fruit> plate = new AIPlate<Apple>();
这里我们就可以将这个苹果盘子转成了水果盘子。
然后我们再向这个盘子里放一些水果看看。
plate.set(new Banana());//编译期错误
plate.set(new Apple());//编译期错误
plate.set(new Fruit());//编译期错误
结果发现,不行了,不论我们放的是Fruit的派生类,Apple或是Banana,或者是Fruit类自己,都不可以放了。这是因为,我们使用extends规定的是类型的上届,也就是说,只要是Fruit的派生类都可以。
在运行阶段,转成字节码以后,泛型类型被擦除掉,然后生成一个capture#1的标记。在我们像里边放元素的时候,实际是用capture#1来跟放入的元素比较。实际上,capture#1和任何类型都不匹配。
那么,编译器怎么知道这个是什么类型呢。
我们发现,我们不可以再往里边放东西了,那么,我们可不可以取东西呢,接下来还是写一段代码来验证:
Banana banana = plate.get();//编译期错误
Fruit fruit = plate.get();
Object object = plate.get();
结果发现,我们还是可以从里边取东西的,因为我们的类型是Fruit的派生类,所以可以使用Fruit来取里边的东西,当然,也可以用Object来取。但是,我们的编译期只知道它是Fruit的派生类,具体是什么水果呢,不知道,所以不可以用Fruit的任何一种派生类来取里边的东西。
上届通配符
<? extends Fruit> 这里我们使用的? extends这种通配符的形式就被称为上届通配符,他的类型规定的是上届,这种类型的通配符,我们只可以读,不可以写。
下届通配符
那么,我们继续思考,水果是食物的派生类,我们可不可以用装食物的盘子转成装水果的盘子呢?
AIPlate<? super Fruit> plate = new AIPlate<Food>();
然后我们再往里边放东西看看会怎么样?
plate.set(new Banana());
plate.set(new Apple());
plate.set(new Fruit());
这时候我们发现,我们可以放了,因为规定了下届是Fruit,所以放入比Fruit粒度小的都是可以的,也就是说可以放入任何一个Fruit的派生类。
那么我们可不可以取呢?
Banana banana = plate.get();//编译期错误
Fruit fruit = plate.get();//编译期错误
结果发现,我们取不到东西了。这是因为什么呢?我们规定了下届是Fruit,所以任何它的基类,都有可能拿到,编译器怎么会知道我们存的是什么呢,所以这里我们没法从里边取东西了。那么,可不可以用Object取呢?答案是可以的,因为Object类是一切类的基类,所以这里是可以用Object取的。
这里有一个问题困扰我很久,那么既然我们这里规定了类型的下届是Fruit,那么我们可不可以往里边放一个Fruit的基类呢?比如下面这样:
plate.set(new Food());//编译期错误
结果我们发现,不行了,我们没办法放任何Fruit的基类。这是为什么,既然规定类型的下届是Fruit,那么按理说这里应该也可以放Food啊。
实际上,我们可以这样理解,我们规定的类型下届,只有在初始化,或者传入这个参数的时候起到了作用。实际上,在使用装食物的盘子转成了水果盘子之后,它就变成了一个水果盘子。并不是说它就变成了一个可以放食物的盘子。那么我们在调用set方法的时候,实际上set的还是Fruit,所以这里是不可以装入Food的。
这里的概念容易混淆,重点提出加深一下理解。
<? super Fruit> 这里我们使用的? super这种通配符的形式就被称为下届通配符,类型限定为指定类型的基类,这里是Fruit的任何基类。这种限定符是只可以写,不可以读的。
非限定通配符
还有一种限定符,我们上面所说的上届通配符和下届通配符统称为限定通配符,也就是说它是有界限的。那么还有一种通配符,叫做非限定通配符,它的写法是这样的<?>
这种通配符我们不可以读,也不可以写。
它实际上就等同于<? extends Object>
那么会有人问了。既然不可以读,不可以写,拿这样的通配符我们要来干嘛呢?
实际上,虽然不可读不可写,但是还是可以用来做类型的安全检查。
这里具体怎么进行的安全检查呢,在之后我了解的更加深入之后会补充。
PECS原则
如果你只需要从集合中获得类型T,使用<? extends T>通配符
如果你只需要将类型T放到集合中,使用<? super T> 通配符
如果你既要获取又要放置元素,则不使用任何通配符。例如List<Apple>
PECS即 Producer extends Consumer super
那么,为什么要PECS原则呢,因为这样可以提升API的灵活性。
通过反射调用被通配符限定的方法。
虽然这里规定了上下界通配符,规定了只读或者只写,但是如果通过反射的方法还是可以调用。
AIPlate<Fruit> aiPlate = new AIPlate<>();
Method method = AIPlate.class.getMethod("set", Fruit.class);
method.invoke(aiPlate, new Apple());
method.invoke(aiPlate, new Food());//运行期错误,报 java.lang.IllegalArgumentException: argument type mismatch
但是,这里传入什么都可以了,虽然AIPlate限定了只能传入Fruit的子类,通过反射也是可以传入任意类型了,这样的调用,没有进行类型的验证,所以安全没有保证了。
那这种方法的应用场景是什么呢,在这个方法只提供给自己使用的时候,可以临时用这种方法,来进行赋值等操作。因为提供给别人,别人不知道你这个类型的限定是什么,所以也没法保证类型的安全。
泛型的灵活转型
这里我们再看下面一段代码:
这里定义一个方法
public static <T> void copy1(List<T> dest, List<T> src) {
Collections.copy(dest, src);
}
这个方法我们在使用的时候,传入的两个参数List的类型必须是相同的才可以。
那么如果我们想要将Fruit的List调用copy方法,拷贝一个Banana的List里的元素,这时候怎么办?
那么我们就使用通配符,我们这样定义。
public static <T> void copy2(List<? super T> dest, List<T> src) {
Collections.copy(dest, src);
}
然后我们这样使用
List<Banana> bananas = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>(10);
copy2(fruits, bananas);
这样我们就可以将香蕉复制到水果的集合中去了。
这里我们再思考一下,香蕉和食物有没有关系?水果是食物的派生类,香蕉又是水果的派生类,那么我们可不可以将香蕉复制到一个食物的集合中去呢?答案是可以的,这里我们看下面这段代码:
首先定义一个方法
public static <T> void copy3(List<? super T> dest, List<? extends T> src) {
Collections.copy(dest, src);
}
这里我们传入的参数分别做了一个限定,第一个参数为T类型的基类,第二个参数为T类型的派生类。
我们就可以这样使用:
List<Food> dest3 = new ArrayList<>(10);
dest3.add(new Food());
List<GreenApple> src3 = new ArrayList<>(10);
src3.add(new GreenApple());
GenericDemo.<Fruit>copy3(dest3, src3);
dest3.add(new Food());
在调用的时候,我规定了泛型类型为Fruit,这样就满足了上述的要求。
然后最后还可以像食物的集合里继续添加其他的食物。
这样我们就完成了一个灵活的转型,这也是应用泛型的一个好处。
这里我们来看Collections的源码:
public static <T> void copy(List<? super T> var0, List<? extends T> var1) {
int var2 = var1.size();
if (var2 > var0.size()) {
throw new IndexOutOfBoundsException("Source does not fit in dest");
} else {
if (var2 < 10 || var1 instanceof RandomAccess && var0 instanceof RandomAccess) {
for(int var6 = 0; var6 < var2; ++var6) {
var0.set(var6, var1.get(var6));
}
} else {
ListIterator var3 = var0.listIterator();
ListIterator var4 = var1.listIterator();
for(int var5 = 0; var5 < var2; ++var5) {
var3.next();
var3.set(var4.next());
}
}
}
}
那么这里为什么传入的参数是这样写的,就应该很好理解了。
下一篇:Java基础进阶–注解.
最后
以上就是感动树叶为你收集整理的Java基础进阶--泛型的全部内容,希望文章能够帮你解决Java基础进阶--泛型所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复