Java 泛型的通配符
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 <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 <B>
或者List <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中。
超级通配符边界
列表<? 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 <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);