Java基础进阶--泛型

什么是泛型?

泛型,就是对传入类型的一种限定,它是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基础进阶–注解.

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值