Java 泛型的通配符

时间:2020-01-09 10:36:00  来源:igfitidea点击:

Java 泛型(Generic)的通配符是Java Generics中的一种机制,旨在使将某个类(例如A)的集合转换为A的子类或者超类的集合成为可能。本文介绍了方法。

基本泛型集合赋值问题

假设我们具有以下类层次结构:

public class A { }
public class B extends A { }
public class C extends A { }

类B和C都从A继承。

然后看一下这两个List变量:

List<A> listA = new ArrayList<A>();
List<B> listB = new ArrayList<B>();

我们可以将listA设置为指向listB吗?或者将" listB"设置为指向" listA"?换句话说,这些分配是否有效:

listA = listB;

listB = listA;

在这两种情况下,答案都是"否"。原因如下:

在" listA"中,我们可以插入作为A的实例或者A的子类(B和C)的对象。如果可以这样做:

List<B> listB = listA;

那么我们可能会冒listA包含非B对象的风险。然后,当我们尝试从" listB"中取出对象时,可能会冒出将非B对象(例如A或者C)取出的风险。这打破了listB变量声明的约定。

listB分配给listA也会带来问题。这项任务,更具体地说:

listA = listB;

如果可以进行分配,则可以将A和C实例插入到listB指向的List <B>中。我们可以通过声明为List &lt;A>listA引用来做到这一点。因此,我们可以将非B对象插入声明为包含B(或者B子类)实例的列表中。

什么时候需要这种分配?

当创建对特定类型的集合进行操作的可重用方法时,需要进行本文前面显示的类型的分配。

假设我们有一种方法可以处理List的元素,例如打印出"列表"中的所有元素。这是这种方法的外观:

public void processElements(List<A> elements){
   for(A o : elements){
      System.out.println(o.getValue());
   }
}

这个方法迭代一个A实例的列表,并调用getValue()方法(假设类A有一个名为getValue()的方法)。

正如我们在本文前面已经看到的那样,我们不能使用List &lt;B>或者List &lt;C>类型的变量作为参数来调用此方法。

通用通配符

通用通配符运算符是上述问题的解决方案。通用通配符针对两个主要需求:

  • 从通用集合中读取
  • 插入通用集合

有三种使用通用通配符定义集合(变量)的方法。这些是:

List<?>           listUknown = new ArrayList<A>();
List<? extends A> listUknown = new ArrayList<A>();
List<? super   A> listUknown = new ArrayList<A>();

以下各节说明了这些通配符的含义。

未知通配符

List <?>表示键入未知类型的列表。可以是List <A>,List <B>,List <String>等。

由于我们不知道List键入的类型,因此只能从集合中读取,并且只能将读取的对象视为Object实例。这是一个例子:

public void processElements(List<?> elements){
   for(Object o : elements){
      System.out.println(o);
   }
}

现在可以使用任何通用的"列表"作为参数来调用" processElements()"方法。例如,"列表<A>","列表<B>","列表<C>","列表<String>"等。这是一个有效的示例:

List<A> listA = new ArrayList<A>();

processElements(listA);

扩展通配符边界

列表<?extends A>是指对象的"列表",它们是A类或者A子类(例如B和C)的实例。

当我们知道集合中的实例是A的实例或者A的子类时,可以安全地读取集合的实例并将其强制转换为A实例。这是一个例子:

public void processElements(List<? extends A> elements){
   for(A a : elements){
      System.out.println(a.getValue());
   }
}

现在,我们可以使用List <A>,List <B>或者List <C>来调用processElements()方法。因此,所有这些示例都是有效的:

List<A> listA = new ArrayList<A>();
processElements(listA);

List<B> listB = new ArrayList<B>();
processElements(listB);

List<C> listC = new ArrayList<C>();
processElements(listC);

processElements()方法仍然无法将元素插入列表中,因为我们不知道作为参数传递的列表是否键入到类A,B或者C中。

超级通配符边界

列表&lt? super A>表示列表被键入为A类或者A的超类。

当我们知道列表的类型为A或者A的超类时,可以安全地将A的实例或者A的子类(例如B或者C)插入列表中。这是一个例子:

public static void insertElements(List<? super A> list){
    list.add(new A());
    list.add(new B());
    list.add(new C());
}

此处插入的所有元素都是A实例,或者是A的超类的实例。由于B和C都扩展了A,因此,如果A具有超类,则B和C也将是该超类的实例。

现在,我们可以使用List &lt;A>或者类型为A的超类的List调用insertElements()。因此,此示例现在有效:

List<A>      listA      = new ArrayList<A>();
insertElements(listA);

List<Object> listObject = new ArrayList<Object>();
insertElements(listObject);

但是,insertElements()方法不能从列表中读取,除非它将读取的对象强制转换为Object。调用insertElements()时列表中已经存在的元素可以是A或者A的超类的任何类型,但是无法确切知道它是哪个类。但是,由于任何类最终都是"对象"的子类,如果将它们强制转换为"对象",则可以从列表中读取对象。因此,这是有效的:

Object object = list.get(0);

但这是无效的:

A object = list.get(0);