副标题#e#
1. 什么是泛型?
泛型(Generic type 可能 generics)是对 Java 语言的范例系统的一种扩展,以支持建设可以按范例举办参数化的类。可以把范例参数看作是利用参数化范例时指定的范例的一个占位符,就像要领的形式参数是运行时通报的值的占位符一样。
可以在荟萃框架(Collection framework)中看到泛型的念头。譬喻,Map 类答允您向一个 Map 添加任意类的工具,纵然最常见的环境是在给定映射(map)中生存某个特定范例(好比 String)的工具。
因为 Map.get() 被界说为返回 Object,所以一般必需将 Map.get() 的功效强制范例转换为期望的范例,如下面的代码所示:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
要让措施通过编译,必需将 get() 的功效强制范例转换为 String,而且但愿功效真的是一个 String。可是有大概或人已经在该映射中生存了不是 String 的对象,这样的话,上面的代码将会抛出 ClassCastException。
抱负环境下,您大概会得出这样一个概念,即 m 是一个 Map,它将 String 键映射到 String 值。这可以让您消除代码中的强制范例转换,同时得到一个附加的范例查抄层,该查抄层可以防备有人将错误范例的键或值生存在荟萃中。这就是泛型所做的事情。
2. 泛型的长处
Java 语言中引入泛型是一个较大的成果加强。不只语言、范例系统和编译器有了较大的变革,以支持泛型,并且类库也举办了大翻修,所以很多重要的类,好比荟萃框架,都已经成为泛型化的了。这带来了许多长处:
范例安详。 泛型的主要方针是提高 Java 措施的范例安详。通过知道利用泛型界说的变量的范例限制,编译器可以在一个高得多的水平上验证范例假设。没有泛型,这些假设就只存在于措施员的脑子中(可能假如幸运的话,还存在于代码注释中)。
Java 措施中的一种风行技能是界说这样的荟萃,即它的元素或键是民众范例的,好比“String 列表”可能“String 到 String 的映射”。通过在变量声明中捕捉这一附加的范例信息,泛型答允编译器实施这些附加的范例约束。范例错误此刻就可以在编译时被捕捉了,而不是在运行时看成 ClassCastException 展示出来。将范例查抄从运行时挪到编译时有助于您更容易找到错误,并可提高措施的靠得住性。
消除强制范例转换。 泛型的一个附带长处是,消除源代码中的很多强制范例转换。这使得代码越发可读,而且淘汰了堕落时机。
尽量淘汰强制范例转换可以低落利用泛型类的代码的罗嗦水平,可是声明泛型变量会带来相应的罗嗦。较量下面两个代码例子。
该代码不利用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);
该代码利用泛型:
List<Integer> li = new ArrayList<Integer>();
li.put(new Integer(3));
Integer i = li.get(0);
在简朴的措施中利用一次泛型变量不会低落罗嗦水平。可是对付多次利用泛型变量的大型措施来说,则可以累积起来低落罗嗦水平。
潜在的机能收益。 泛型为较大的优化带来大概。在泛型的初始实现中,编译器将强制范例转换(没有泛型的话,措施员会指定这些强制范例转换)插入生成的字节码中。可是更多范例信息可用于编译器这一事实,为将来版本的 JVM 的优化带来大概。
由于泛型的实现方法,支持泛型(险些)不需要 JVM 或类文件变动。所有事情都在编译器中完成,编译器生成雷同于没有泛型(和强制范例转换)时所写的代码,只是更能确保范例安详罢了。
3. 泛型用法的例子
泛型的很多最佳例子都来自荟萃框架,因为泛型让您在生存在荟萃中的元素上指定范例约束。思量这个利用 Map 类的例子,个中涉及必然水平的优化,即 Map.get() 返回的功效将确实是一个 String:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
假如有人已经在映射中安排了不是 String 的其他对象,上面的代码将会抛出 ClassCastException。泛型答允您表达这样的范例约束,即 m 是一个将 String 键映射到 String 值的 Map。这可以消除代码中的强制范例转换,同时得到一个附加的范例查抄层,这个查抄层可以防备有人将错误范例的键或值生存在荟萃中。
下面的代码示例展示了 JDK 5.0 中荟萃框架中的 Map 接口的界说的一部门:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
留意该接口的两个附加物:
范例参数 K 和 V 在类级此外规格说明,暗示在声明一个 Map 范例的变量时指定的范例的占位符。
在 get()、put() 和其他要领的要领签名中利用的 K 和 V。
为了赢得利用泛型的长处,必需在界说或实例化 Map 范例的变量时为 K 和 V 提供详细的值。以一种相对直观的方法做这件事:
Map<String, String> m = new HashMap<String, String>();
m.put("key", "blarg");
String s = m.get("key");
当利用 Map 的泛型化版本时,您不再需要将 Map.get() 的功效强制范例转换为 String,因为编译器知道 get() 将返回一个 String。
#p#分页标题#e#
在利用泛型的版本中并没有淘汰键盘录入;实际上,比利用强制范例转换的版本需要做更多键入。利用泛型只是带来了附加的范例安详。因为编译器知道关于您将放进 Map 中的键和值的范例的更多信息,所以范例查抄从执行时挪到了编译时,这会提高靠得住性并加速开拓速度。
向后兼容
在 Java 语言中引入泛型的一个重要方针就是维护向后兼容。尽量 JDK 5.0 的尺度类库中的很多类,好比荟萃框架,都已经泛型化了,可是利用荟萃类(好比 HashMap 和 ArrayList)的现有代码将继承不加修改地在 JDK 5.0 中事情。虽然,没有操作泛型的现有代码将不会赢得泛型的范例安详长处。
4. 泛型基本
4.1 范例参数
在界说泛型类或声明泛型类的变量时,利用尖括号来指定形式范例参数。形式范例参数与实际范例参数之间的干系雷同于形式要领参数与实际要领参数之间的干系,只是范例参数暗示范例,而不是暗示值。
泛型类中的范例参数险些可以用于任何可以利用类名的处所。譬喻,下面是 java.util.Map 接口的界说的摘录:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
Map 接口是由两个范例参数化的,这两个范例是键范例 K 和值范例 V。(不利用泛型)将会接管或返回 Object 的要领此刻在它们的要领签名中利用 K 或 V,指示附加的范例约束位于 Map 的规格说明之下。
当声明可能实例化一个泛型的工具时,必需指定范例参数的值:
Map<String, String> map = new HashMap<String, String>();
留意,在本例中,必需指定两次范例参数。一次是在声明变量 map 的范例时,另一次是在选择 HashMap 类的参数化以便可以实例化正确范例的一个实例时。
编译器在碰着一个 Map<String, String> 范例的变量时,知道 K 和 V 此刻被绑定为 String,因此它知道在这样的变量上挪用 Map.get() 将会获得 String 范例。
除了异常范例、列举或匿名内部类以外,任何类都可以具有范例参数。
4.2 定名范例参数
推荐的定名约定是利用大写的单个字母名称作为范例参数。这与 C++ 约定有所差异(参阅 附录 A:与 C++ 模板的较量),并反应了大大都泛型类将具有少量范例参数的假定。对付常见的泛型模式,推荐的名称是:
K —— 键,好比映射的键。
V —— 值,好比 List 和 Set 的内容,可能 Map 中的值。
E —— 异常类。
T —— 泛型。
4.3 泛型不是协变的
关于泛型的夹杂,一个常见的来历就是假设它们像数组一样是协变的。其实它们不是协变的。List<Object> 不是 List<String> 的父范例。
假如 A 扩展 B,那么 A 的数组也是 B 的数组,而且完全可以在需要 B[] 的处所利用 A[]:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代码是有效的,因为一个 Integer 是一个 Number,因而一个 Integer 数组是 一个 Number 数组。可是对付泛型来说则否则。下面的代码是无效的:
List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
最初,大大都 Java 措施员以为这缺少协变很烦人,可能甚至是“坏的(broken)”,可是之所以这样有一个很好的原因。假如可以将 List<Integer> 赋给 List<Number>,下面的代码就会违背泛型应该提供的范例安详:
List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
numberList.add(new Float(3.1415));
因为 intList 和 numberList 都是有别名的,假如答允的话,上面的代码就会让您将不是 Integers 的对象放进 intList 中。可是,正如下一屏将会看到的,您有一个越发机动的方法来界说泛型。
#p#副标题#e#
4.4 范例通配符
假设您具有该要领:
void printList(List l) {
for (Object o : l)
System.out.println(o);
}
#p#分页标题#e#
上面的代码在 JDK 5.0 上编译通过,可是假如试图用 List<Integer> 挪用它,则会获得告诫。呈现告诫是因为,您将泛型(List<Integer>)通报给一个只理睬将它看成 List(所谓的原始范例)的要领,这将粉碎利用泛型的范例安详。
假如试图编写像下面这样的要领,那么将会怎么样?
void printList(List<Object> l) {
for (Object o : l)
System.out.println(o);
}
它仍然不会通过编译,因为一个 List<Integer> 不是 一个 List<Object>(正如前一屏 泛型不是协变的 中所学的)。这才真正烦人 —— 此刻您的泛型版本还没有普通的非泛型版本有用!
办理方案是利用范例通配符:
void printList(List<?> l) {
for (Object o : l)
System.out.println(o);
}
上面代码中的问号是一个范例通配符。它读作“问号”。List<?> 是任何泛型 List 的父范例,所以您完全可以将 List<Object>、List<Integer> 或 List<List<List<Flutzpah>>> 通报给 printList()。
4.5 范例通配符的浸染
范例通配符中引入了范例通配符,这让您可以声明 List<?> 范例的变量。您可以对这样的 List 做什么呢?很是利便,可以从中检索元素,可是不能添加元素。原因不是编译器知道哪些要领修改列表哪些要领不修改列表,而是(大大都)变革的要领比稳定革的要领需要更多的范例信息。下面的代码则事情得很好:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
System.out.println(lu.get(0));
为什么该代码能事情呢?对付 lu,编译器一点都不知道 List 的范例参数的值。可是编译器较量智慧,它可以做一些范例推理。在本例中,它揣度未知的范例参数必需扩展 Object。(这个特定的推理没有太大的跳跃,可是编译器可以作出一些很是令人服气的范例推理,后头就会看到(在底层细节一节中)。所以它让您挪用 List.get() 并揣度返回范例为 Object。
另一方面,下面的代码不能事情:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.add(new Integer(43)); // error
在本例中,对付 lu,编译器不能对 List 的范例参数作出足够严密的推理,以确定将 Integer 通报给 List.add() 是范例安详的。所以编译器将不答允您这么做。
以免您仍然认为编译器知道哪些要领变动列表的内容哪些不变动列表内容,请留意下面的代码将能事情,因为它不依赖于编译器必需知道关于 lu 的范例参数的任何信息:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.clear();
4.6 泛型要领
(在 范例参数 一节中)您已经看到,通过在类的界说中添加一个形式范例参数列表,可以将类泛型化。要领也可以被泛型化,不管它们界说在个中的类是不是泛型化的。
泛型类在多个要领签名间实施范例约束。在 List<V> 中,范例参数 V 呈此刻 get()、add()、contains() 等要领的签名中。当建设一个 Map<K, V> 范例的变量时,您就在要领之间宣称一个范例约束。您通报给 add() 的值将与 get() 返回的值的范例沟通。
雷同地,之所以声明泛型要领,一般是因为您想要在该要领的多个参数之间宣称一个范例约束。譬喻,下面代码中的 ifThenElse() 要领,按照它的第一个参数的布尔值,它将返回第二个或第三个参数:
public <T> T ifThenElse(boolean b, T first, T second) {
return b ? first : second;
}
留意,您可以挪用 ifThenElse(),而不消显式地汇报编译器,您想要 T 的什么值。编译器不必显式地被奉告 T 将具有什么值;它只知道这些值都必需沟通。编译器答允您挪用下面的代码,因为编译器可以利用范例推理来揣度出,替代 T 的 String 满意所有的范例约束:
String s = ifThenElse(b, "a", "b");
雷同地,您可以挪用:
Integer i = ifThenElse(b, new Integer(1), new Integer(2));
可是,编译器不答允下面的代码,因为没有范例会满意所需的范例约束:
String s = ifThenElse(b, "pi", new Float(3.14));
为什么您选择利用泛型要领,而不是将范例 T 添加到类界说呢?(至少)有两种环境应该这样做:
当泛型要领是静态的时,这种环境下不能利用类范例参数。
当 T 上的范例约束对付要领真正是局部的时,这意味着没有在沟通类的另一个要领签名中利用沟通范例 T 的约束。通过使得泛型要领的范例参数对付要领是局部的,可以简化关闭范例的签名。
4.7 有限制范例
在前一屏 泛型要领 的例子中,范例参数 V 是无约束的或无限制的范例。有时在还没有完全指定范例参数时,需要对范例参数指定附加的约束。
思量例子 Matrix 类,它利用范例参数 V,该参数由 Number 类来限制:
public class Matrix<V extends Number> { … }
#p#分页标题#e#
编译器答允您建设 Matrix<Integer> 或 Matrix<Float> 范例的变量,可是假如您试图界说 Matrix<String> 范例的变量,则会呈现错误。范例参数 V 被判定为由 Number 限制 。在没有范例限制时,假设范例参数由 Object 限制。这就是为什么前一屏 泛型要领 中的例子,答允 List.get() 在 List<?> 上挪用时返回 Object,纵然编译器不知道范例参数 V 的范例。
5. 一个简朴的泛型类
5.1 编写根基的容器类
此时,您可以开始编写简朴的泛型类了。到今朝为止,泛型类最常见的用例是容器类(好比荟萃框架)可能值持有者类(好比 WeakReference 或 ThreadLocal)。我们来编写一个类,它雷同于 List,充当一个容器。个中,我们利用泛型来暗示这样一个约束,即 Lhist 的所有元素将具有沟通范例。为了实现起来简朴,Lhist 利用一个牢靠巨细的数组来生存值,而且不接管 null 值。
Lhist 类将具有一个范例参数 V(该参数是 Lhist 中的值的范例),并将具有以下要领:
public class Lhist<V> {
public Lhist(int capacity) { … }
public int size() { … }
public void add(V value) { … }
public void remove(V value) { … }
public V get(int index) { … }
}
要实例化 Lhist,只要在声明时指定范例参数和想要的容量:
Lhist<String> stringList = new Lhist<String>(10);
5.2 实现结构函数
在实现 Lhist 类时,您将会碰着的第一个拦路石是实现结构函数。您大概会像下面这样实现它:
public class Lhist<V> {
private V[] array;
public Lhist(int capacity) {
array = new V[capacity]; // illegal
}
}
这好像是分派后备数组最自然的一种方法,可是不幸的是,您不能这样做。详细原因很巨大,当进修到底层细节一节中的“擦除”主题时,您就会大白。分派后备数组的实现方法很离奇且违反直觉。下面是结构函数的一种大概的实现(该实现利用荟萃类所回收的要领):
public class Lhist<V> {
private V[] array;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
}
别的,也可以利用反射来实例化数组。可是这样做需要给结构函数通报一个附加的参数 —— 一个类常量,好比 Foo.class。后头在 Class<T> 一节中将接头类常量。
5.3 实现要领
实现 Lhist 的要领要容易得多。下面是 Lhist 类的完整实现:
public class Lhist<V> {
private V[] array;
private int size;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
public void add(V value) {
if (size == array.length)
throw new IndexOutOfBoundsException(Integer.toString(size));
else if (value == null)
throw new NullPointerException();
array[size++] = value;
}
public void remove(V value) {
int removalCount = 0;
for (int i=0; i<size; i++) {
if (array[i].equals(value))
++removalCount;
else if (removalCount > 0) {
array[i-removalCount] = array[i];
array[i] = null;
}
}
size -= removalCount;
}
public int size() { return size; }
public V get(int i) {
if (i >= size)
throw new IndexOutOfBoundsException(Integer.toString(i));
return array[i];
}
}
留意,您在将会接管或返回 V 的要领中利用了形式范例参数 V,可是您一点也不知道 V 具有什么样的要领或域,因为这些对泛型代码是不行知的。
5.4 利用 Lhist 类
利用 Lhist 类很容易。要界说一个整数 Lhist,只需要在声明和结构函数中为范例参数提供一个实际值即可:
Lhist<Integer> li = new Lhist<Integer>(30);
编译器知道,li.get() 返回的任何值都将是 Integer 范例,而且它还强制通报给 li.add() 或 li.remove() 的任何对象都是 Integer。除了实现结构函数的方法很离奇之外,您不需要做任何十分非凡的工作以使 Lhist 是一个泛型类。
6. Java类库中的泛型
6.1 荟萃类
到今朝为止,Java 类库中泛型支持存在最多的处所就是荟萃框架。就像容器类是 C++ 语言中模板的主要念头一样(参阅附录 A:与 C++ 模板的较量)(尽量它们随后用于许多此外用途),改进荟萃类的范例安详是 Java 语言中泛型的主要念头。荟萃类也充当如何利用泛型的模子,因为它们演示了泛型的险些所有的尺度能力和方言。
#p#分页标题#e#
所有的尺度荟萃接口都是泛型化的 —— Collection<V>、List<V>、Set<V> 和 Map<K,V>。雷同地,荟萃接口的实现都是用沟通范例参数泛型化的,所以 HashMap<K,V> 实现 Map<K,V> 等。
荟萃类也利用泛型的很多“能力”和方言,好比上限通配符和下限通配符。譬喻,在接口 Collection<V> 中,addAll 要领是像下面这样界说的:
interface Collection<V> {
boolean addAll(Collection<? extends V>);
}
该界说组合了通配符范例参数和有限制范例参数,答允您将 Collection<Integer> 的内容添加到 Collection<Number>。
假如类库将 addAll() 界说为接管 Collection<V>,您就不能将 Collection<Integer> 的内容添加到 Collection<Number>。不是限制 addAll() 的参数是一个与您将要添加到的荟萃包括沟通范例的荟萃,而有大概成立一个更公道的约束,即通报给 addAll() 的荟萃的元素 适合于添加到您的荟萃。有限制范例答允您这样做,而且利用有限制通配符使您不需要利用另一个不会用在其他任那里所的占位符名称。
应该可以将 addAll() 的范例参数界说为 Collection<V>。可是,这不单没什么用,并且还会改变 Collection 接口的语义,因为泛型版本的语义将会差异于非泛型版本的语义。这叙述了泛型化一个现有的类要比界说一个新的泛型类可贵多,因为您必需留意不要变动类的语义可能粉碎现有的非泛型代码。
作为泛型化一个类(假如不小心的话)如何会变动其语义的一个越发微妙的例子,留意 Collection.removeAll() 的参数的范例是 Collection<?>,而不是 Collection<? extends V>。这是因为通报殽杂范例的荟萃给 removeAll() 是可接管的,而且越发限制地界说 removeAll 将会变动要领的语义和有用性。
6.2 其他容器类
除了荟萃类之外,Java 类库中尚有几个其他的类也充当值的容器。这些类包罗 WeakReference、SoftReference 和 ThreadLocal。它们都已经在其包括的值的范例上泛型化了,所以 WeakReference<T> 是对 T 范例的工具的弱引用,ThreadLocal<T> 则是到 T 范例的线程局部变量的句柄。
6.3 泛型不止用于容器
泛型最常见最直观的利用是容器类,好比荟萃类或引用类(好比 WeakReference<T>)。Collection<V> 中范例参数的寄义很明明 —— “一个所有值都是 V 范例的荟萃”。雷同地,ThreadLocal<T> 也有一个明明的表明 —— “一个其范例是 T 的线程局部变量”。可是,泛型规格说明中没有指定容积。
像 Comparable<T> 或 Class<T> 这样的类中范例参数的寄义越发微妙。有时,就像 Class<T> 中一样,范例变量主要是辅佐编译器举办范例推理。有时,就像隐含的 Enum<E extends Enum<E>> 中一样,范例变量只是在类条理布局上加一个约束。
6.3.1 Comparable<T>
Comparable 接口已经泛型化了,所以实现 Comparable 的工具声明它可以与什么范例举办较量。(凡是,这是工具自己的范例,可是有时也大概是父类。)
public interface Comparable<T> {
public boolean compareTo(T other);
}
所以 Comparable 接口包括一个范例参数 T,该参数是一个实现 Comparable 的类可以与之较量的工具的范例。这意味着假如界说一个实现 Comparable 的类,好比 String,就必需不只声明类支持较量,还要声明它可与什么较量(凡是是与它自己较量):
public class String implements Comparable<String> { … }
此刻来思量一个二元 max() 要领的实现。您想要接管两个沟通范例的参数,二者都是 Comparable,而且彼此之间是 Comparable。幸运的是,假如利用泛型要领和有限制范例参数的话,这相当直观:
public static <T extends Comparable<T>> T max(T t1, T t2) {
if (t1.compareTo(t2) > 0)
return t1;
else
return t2;
}
在本例中,您界说了一个泛型要领,在范例 T 上泛型化,您约束该范例扩展(实现) Comparable<T>。两个参数都必需是 T 范例,这暗示它们是沟通范例,支持较量,而且彼此可较量。容易!
更好的是,编译器将利用范例推理来确定当挪用 max() 时 T 的值暗示什么意思。所以基础不消指定 T,下面的挪用就能事情:
String s = max("moo", "bark");
编译器将计较出 T 的预定值是 String,因此它将举办编译和范例查抄。可是假如您试图用不实现 Comparable<X> 的 类 X 的参数挪用 max(),那么编译器将不答允这样做。
6.3.2 Class<T>
#p#分页标题#e#
类 Class 已经泛型化了,可是许多人一开始都感受其泛型化的方法很杂乱。Class<T> 中范例参数 T 的寄义是什么?事实证明它是所引用的类接口。怎么会是这样的呢?那是一个轮回推理?假如不是的话,为什么这样界说它?
在以前的 JDK 中,Class.newInstance() 要领的界说返回 Object,您很大概要将该返回范例强制转换为另一种范例:
class Class {
Object newInstance();
}
可是利用泛型,您界说 Class.newInstance() 要领具有一个越发特定的返回范例:
class Class<T> {
T newInstance();
}
如何建设一个 Class<T> 范例的实例?就像利用非泛型代码一样,有两种方法:挪用要领 Class.forName() 可能利用类常量 X.class。Class.forName() 被界说为返回 Class<?>。另一方面,类常量 X.class 被界说为具有范例 Class<X>,所以 String.class 是 Class<String> 范例的。
URL:http://www.bianceng.cn/Programming/Java/201608/50385.htm
让 Foo.class 是 Class<Foo> 范例的有什么长处?大的长处是,通过范例推理的魔力,可以提高利用反射的代码的范例安详。别的,还不需要将 Foo.class.newInstance() 强制范例转换为 Foo。
思量一个要领,它从数据库检索一组工具,并返回 JavaBeans 工具的一个荟萃。您通过反射来实例化和初始化建设的工具,可是这并不料味着范例安详必需完全被抛至脑后。思量下面这个要领:
public static<T> List<T> getRecords(Class<T> c, Selector s) {
// Use Selector to select rows
List<T> list = new ArrayList<T>();
for (/* iterate over results */) {
T row = c.newInstance();
// use reflection to set fields from result
list.add(row);
}
return list;
}
可以像下面这样简朴地挪用该要领:
List<FooRecord> l = getRecords(FooRecord.class, fooSelector);
编译器将会按照 FooRecord.class 是 Class<FooRecord> 范例的这一事实,揣度 getRecords() 的返回范例。您利用类常量来结构新的实例并提供编译器在范例查抄中要用到的范例信息。
6.3.3 用 Class<T> 替换 T[]
Collection 接口包括一个要领,用于将荟萃的内容复制到一个挪用者指定范例的数组中:
public Object[] toArray(Object[] prototypeArray) { … }
toArray(Object[]) 的语义是,假如通报的数组足够大,就会利用它来生存功效,不然,就会利用反射分派一个沟通范例的新数组。一般来说,单独通报一个数组作为参数来提供想要的返回范例是一个小能力,可是在引入泛型之前,这是与要领交换范例信息最利便的方法。
有了泛型,就可以用一种越发直观的方法来做这件事。不像上面这样界说 toArray(),泛型 toArray() 大概看起来像下面这样:
public<T> T[] toArray(Class<T> returnType)
挪用这样一个 toArray() 要领很简朴:
FooBar[] fba = something.toArray(FooBar.class);
Collection 接口还没有改变为利用该技能,因为这会粉碎很多现有的荟萃实现。可是假如利用泛型重新构建 Collection,则虽然会利用该方言来指定它想要返回值是哪种范例。
6.3.4 Enum<E>
JDK 5.0 中 Java 语言另一个增加的特性是列举。当您利用 enum 要害字声明一个列举时,编译器就会在内部为您生成一个类,用于扩展 Enum 并为列举的每个值声明静态实例。所以假如您说:
public enum Suit {HEART, DIAMOND, CLUB, SPADE};
编译器就会在内部生成一个叫做 Suit 的类,该类扩展 java.lang.Enum<Suit> 并具有叫做 HEART、DIAMOND、CLUB 和 SPADE 的常量(public static final)成员,每个成员都是 Suit 类。
与 Class 一样,Enum 也是一个泛型类。可是与 Class 差异,它的签名稍微更巨大一些:
class Enum<E extends Enum<E>> { . . . }
这毕竟是什么意思?这莫非不会导致无限递归?
我们慢慢来阐明。范例参数 E 用于 Enum 的各类要领中,好比 compareTo() 或 getDeclaringClass()。为了这些要领的范例安详,Enum 类必需在列举的类上泛型化。
所以 extends Enum<E> 部门如何领略?该部门又具有两个部门。第一部门指出,作为 Enum 的范例参数的类自己必需是 Enum 的子范例,所以您不能声明一个类 X 扩展 Enum<Integer>。第二部门指出,任何扩展 Enum 的类必需通报它自己 作为范例参数。您不能声明 X 扩展 Enum<Y>,纵然 Y 扩展 Enum。
总之,Enum 是一个参数化的范例,只可觉得它的子范例实例化,而且这些子范例然后将按照子范例来担任要领。幸运的是,在 Enum 环境下,编译器为您做这些事情,一切都很好。
6.3.5 与非泛型代码彼此操纵
#p#分页标题#e#
数百万行现有代码利用已经泛型化的 Java 类库中的类,好比荟萃框架、Class 和 ThreadLocal。JDK 5.0 中的改造不要粉碎所有这些代码是很重要的,所以编译器答允您在不指定其范例参数的环境下利用泛型类。
虽然,以“旧方法”干事没有新方法安详,因为忽略了编译器筹备提供的范例安详。假如您试图将 List<String> 通报给一个接管 List 的要领,它将可以或许事情,可是编译器将会发出一个大概丧失范例安详的告诫,即所谓的“unchecked conversion(不查抄转换)”告诫。
没有范例参数的泛型,好比声明为 List 范例而不是 List<Something> 范例的变量,叫做原始范例。原始范例与参数化范例的任何实例化是赋值兼容的,可是这样的赋值会生成 unchecked-conversion 告诫。
为了消除一些 unchecked-conversion 告诫,假设您禁绝备泛型化所有的代码,您可以利用通配符范例参数。利用 List<?> 而不利用 List。List 是原始范例;List<?> 是具有未知范例参数的泛型。编译器将以差异的方法看待它们,并很大概发出更少的告诫。
无论在哪种环境下,编译器在生成字节码时城市生成强制范例转换,所以生成的字节码在每种环境下都不会比没有泛型时更不安详。假如您设法通过利用原始范例或类文件来粉碎范例安详,就会获得与不利用泛型时获得的沟通的 ClassCastException 或 ArrayStoreException。
7. Java 泛型的领略与等价实现
泛型是JAVA SE 1.5的新特性,泛型的本质是参数化范例,也就是说所操纵的数据范例被指定为一个参数。这种参数范例可以用在类、接口和要领的建设中,别离称为泛型类、泛型接口、泛型要领。
JAVA语言引入泛型的长处是安详简朴。
在JAVA SE 1.5之前,没有泛型的环境的下,通过对范例Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制范例转换,而这种转换是要求开拓者对实际参数范例可以预知的环境下举办的。对付强制范例转换错误的环境,编译器大概不提示错误,在运行的时候才呈现异常,这是一个安详隐患。
泛型的长处是在编译的时候查抄范例安详,而且所有的强制转换都是自动和隐式的,提高代码的重用率。
泛型在利用中尚有一些法则和限制:
1、泛型的范例参数只能是类范例(包罗自界说类),不能是简朴范例。
2、同一种泛型可以对应多个版本(因为参数范例是不确定的),差异版本的泛型类实例是不兼容的。
3、泛型的范例参数可以有多个。
4、泛型的参数范例可以利用extends语句,譬喻<T extends superclass>。习惯上成为“有界范例”。
5、泛型的参数范例还可以是通配符范例。譬喻Class<?> classType = Class.forName(java.lang.String);
泛型尚有接口、要领等等,内容许多,需要耗费一番工夫才气领略把握并纯熟应用。在此给出我曾经相识泛型时候写出的两个例子(按照看的印象写的),实现同样的成果,一个利用了泛型,一个没有利用,通过比拟,可以很快学会泛型的应用,学会这个根基上学会了泛型70%的内容。
预计你快等不及了,此刻就贴出源码:
例子一:利用了泛型
public class Gen<T> { private T ob; //界说泛型成员变量 public Gen(T ob) { this.ob = ob; } public T getOb() { return ob; } public void setOb(T ob) { this.ob = ob; } public void showTyep() { System.out.println("T的实际范例是: " + ob.getClass().getName()); } } public class GenDemo { public static void main(String[] args){ //界说泛型类Gen的一个Integer版本 Gen<Integer> intOb=new Gen<Integer>(88); intOb.showTyep(); int i= intOb.getOb(); System.out.println("value= " + i); System.out.println("----------------------------------"); //界说泛型类Gen的一个String版本 Gen<String> strOb=new Gen<String>("Hello Gen!"); strOb.showTyep(); String s=strOb.getOb(); System.out.println("value= " + s); } }
#p#分页标题#e#
例子二:没有利用泛型
public class Gen2 { private Object ob; //界说一个通用范例成员 public Gen2(Object ob) { this.ob = ob; } public Object getOb() { return ob; } public void setOb(Object ob) { this.ob = ob; } public void showTyep() { System.out.println("T的实际范例是: " + ob.getClass().getName()); } } public class GenDemo2 { public static void main(String[] args) { //界说类Gen2的一个Integer版本 Gen2 intOb = new Gen2(new Integer(88)); intOb.showTyep(); int i = (Integer) intOb.getOb(); System.out.println("value= " + i); System.out.println("----------------------------------"); //界说类Gen2的一个String版本 Gen2 strOb = new Gen2("Hello Gen!"); strOb.showTyep(); String s = (String) strOb.getOb(); System.out.println("value= " + s); } }
8. Java5泛型的用法,T.class的获取
Java 5的泛型语法已经有太多书讲了,这里不再打字贴书。GP必然有用,否则Java和C#不会约好了似的同时开始支持GP。但各人也清楚,GP和Ruby式的动态OO语言属于差异的意识形态,假如是一人一票,我想大部门的平民措施员更热衷动态OO语言的平白自然。但假如禁绝备跳槽到支持JSR223的动态语言,那照旧看看GP吧。
胡乱总结泛型的四点浸染:
第一是泛化,可以拿个T代表任意范例。但GP是被C++严苛的静态性逼出来的,落到Java、C#这样的花语平原里—-所有工具除几个原始范破例都派生于Object,再加上Java的反射成果,Java的Collection库没有范型一样过得好好的。
第二是泛型 + 反射,原本因为Java的泛型拿不到T.class而以为泛型没用,最近才方才学到通过反射的API来获取T的Class,后述。
第三是收敛,就是增加了范例安详,淘汰了强制范例转换的代码。这点倒是Java Collection向来的弱项。
第四是可以在编译期搞许多对象,好比MetaProgramming。但除非能完全关闭于框架内部,框架的利用者和扩展者都不消进修这些对象的用法,不然那就是自绝于人民的票房毒药。C++的MetaProgramming好锋利吧,但比拟一下Python拿Meta Programming生造一个Class出来的轻便语法,就大白什么才是真正的喝采又叫座。
所以,作为一个架构设计师,应该利用上述的第2,3项用法,在框架类里共同利用反射和泛型,使得框架的本领更强; 同时回收收敛特性,本着对人民认真的精力,用泛型使框架越发范例安详,更少强制范例转换。
擦拭法制止了Java的流血破裂 :
各人常常骂Java GP的擦拭法实现,但我以为多亏于它的中庸特性—假如你用就是范型,不消就是普通Object,制止了Java阵营又要经验一场to be or not to be的破裂。
最大的例子莫过Java 5的Collection 框架, 好比有些同学僵持认为本身不会呆子到范例堕落,并且难以忍受每个界说的处所都要带一个泛型界说List〈Book〉,不消强制范例转换所省下的代码还不足N处界说花的(对了,java内里还没有tyepdef…..),因此对范型十分不伤风,这时就要齐齐感激这个搽拭法让你依然可以对一个泛型框架保持非泛型的用法了…
通过反射得到 T.class:
不知为何书上不怎么讲这个,是差沙汇报我才知道的,最经典的应用见Hibernate wiki的Generic Data Access Objects, 代码如下
abstract public class BaseHibernateEntityDao<T> extends HibernateDaoSupport { private Class<T> entityClass; public BaseHibernateEntityDao() { entityClass =(Class<T>) ((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0]; } public T get(Serializable id) { T o = (T) getHibernateTemplate().get(entityClass, id); } }
英华就是这句了:
Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
#p#分页标题#e#
泛型之后,所有BaseHibernateEntityDao的子类只要界说了泛型,就无需再重载getEnttityClass(),get()函数和find()函数,销益挺明明的,所以SpringSide的Dao基类绝不踌躇就泛型了。
不外擦拭法的大棒仍在,所以子类的泛型语法可不能乱写,最正确的用法只有:
public class BookDao extends BaseHibernateEntityDao<Book>
9. Java 5.0泛型编程之泛型范例
Java5.0的新特性之一是引入了泛型范例和泛型要领。一个泛型范例通过利用一个或多个范例变量来界说,并拥有一个或多个利用一个范例变量作为一个参数可能返回值的占位符。譬喻,范例java.util.List<E>是一个泛型范例:一个list,其元素的范例被占位符E描写。这个范例有一个名为add()的要领,被声明为有一个范例为E的参数,同时,有一个get()要领,返回值被声明为E范例。
利用泛型范例,你应该为范例变量具体指明实际的范例,形成一个就像List<String>雷同的参数化范例。[1]指明这些特另外范例信息的原因是编译器据此可以或许在编译期为您提供很强的范例查抄,加强您的措施的范例安详性。举个例子来说,您有一个只能保持String工具的List,那么这种范例查抄就可以或许阻止您往内里插手String[]工具。同样的,增加的范例信息使编译器可以或许为您做一些范例转换的工作。好比,编译器知道了一个List<String>有个get()要领,其返回值是一个String工具,因此您不再需要去将返回值由一个Object强制转换为String。
Java.util包中的荟萃类在java5.0中已经被做成了泛型,也许您将会在您的措施中频繁的利用到他们。范例安详的荟萃类就是一个泛型范例的典范案例。即便您从没有界说过您本身的泛型范例甚至从未用过除了java.util中的荟萃类以外的泛型范例,范例安详的荟萃类的长处也是极有意义的一个符号——他们证明白这个主要的新语言特性的巨大性。
我们从摸索范例安详的荟萃类中的根基的泛型用法开始,进而研究更多利用泛型范例的巨大细节。然后我们接头范例参数通配符和有界通配符。描画了如何利用泛型今后,我们阐发如何编写本身的泛型范例和泛型要领。我们对付泛型的接头将竣事于一趟对付JavaAPI的焦点中重要的泛型范例的观光。这趟路程将摸索这些范例以及他们的用法,路程的目标是为了让您对泛型如何事情这个问题有个深入的领略。
范例安详荟萃类
Java.util类包包括了Java荟萃框架(Java Collections Framework),这是一批包括工具的set、工具的list以及基于key-value的map。第五章将谈到荟萃类。这里,我们接头的是在java5.0中荟萃类利用范例参数来界定荟萃中的工具的范例。这个接头并不适合java1.4或更早期版本。假如没有泛型,对付荟萃类的利用需要措施员记着每个荟萃中元素的范例。当您在java1.4种建设了一个荟萃,您知道您放入到荟萃中的工具的范例,可是编译器不知道。您必需小心地往个中插手一个符合范例的元素,当需要从荟萃中获取元素时,您必需显式的写强制范例转换以将他们从Object转换为他们真是的范例。考查下边的java1.4的代码。
public static void main(String[] args) {
// This list is intended to hold only strings.
// The compiler doesn’t know that so we have to remember ourselves.
List wordlist = new ArrayList();
// Oops! We added a String[] instead of a String.
// The compiler doesn’t know that this is an error.
wordlist.add(args);
// Since List can hold arbitrary objects, the get() method returns
// Object. Since the list is intended to hold strings, we cast the
// return value to String but get a ClassCastException because of
// the error above.
String word = (String)wordlist.get(0);
}
泛型范例办理了这段代码中的显示的范例安详问题。Java.util中的List或是其他荟萃类已经利用泛型重写过了。就像前面提到的, List被从头界说为一个list,它中间的元素范例被一个范例可变的名称为E的占位符描写。Add()要领被从头界说为期望一个范例为E的参数,用于替换以前的Object,get()要领被从头界说为返回一个E,替换了以前的Object。
在java5.0中,当我们申明一个List可能建设一个ArrayList的实例的时候,我们需要在泛型范例的名字后头紧跟一对“<>”,尖括号中写入我们需要的实际的范例。好比,一个保持String的List应该写成“List<String>”。需要留意的是,这很是象给一个要领传一个参数,区别是我们利用范例而不是值,同时利用尖括号而不是圆括号
#p#分页标题#e#
Java.util的荟萃类中的元素必需是工具化的,他们不能是根基范例。泛型的引入并没有改变这点。泛型不能利用根基范例:我们不能这样来申明——Set<char>可能List<int>。记着,无论如何,java5.0中的自动打包和自动解包特性使得利用Set<Character>可能List<Integer>和直接利用char和int值一样利便。
在Java5.0中,上面的例子将被重写为如下方法:
public static void main(String[] args) {
// This list can only hold String objects
List<String> wordlist = new ArrayList<String>();
// args is a String[], not String, so the compiler won’t let us do this
wordlist.add(args); // Compilation error!
// We can do this, though.
// Notice the use of the new for/in looping statement
for(String arg : args) wordlist.add(arg);
// No cast is required. List<String>.get() returns a String.
String word = wordlist.get(0);
}
值得留意的是代码量其实并没有比本来谁人没有泛型的例子少几多。利用“(String)”这样的范例转换被替换成了范例参数“<String>”。差异的是范例参数需要且仅需要声明一次,而list可以或许被利用任何多次,不需要范例转换。在更长点的例子代码中,这一点将越发明明。纵然在那些看上去泛型语法比非泛型语法要冗长的例子里,利用泛型依然长短常有代价的——特另外范例信息答允编译器在您的代码里执行更强的错误查抄。以前只能在运行起才气发明的错误此刻可以或许在编译时就被发明。另外,以前为了处理惩罚范例转换的异常,我们需要添加特另外代码行。假如没有泛型,那么当产生范例转换异常的时候,一个ClassCastException异常就会被从实际代码中抛出。
就像一个要领可以利用任意数量的参数一样,类答允利用多个范例变量。接口Java.util.Map就是一个例子。一个Map浮现了从一个key的工具到一个value的工具的映射干系。接口Map申明白一个范例变量来描写key的范例而另一个范例变量来描写value的范例。举个例子来说,假设您但愿做一个String工具到Integer工具的映射干系:
public static void main(String[] args) {
// A map from strings to their position in the args[] array
Map<String,Integer> map = new HashMap<String,Integer>();
// Note that we use autoboxing to wrap i in an Integer object.
for(int i=0; i < args.length; i++)
map.put(args[i], i);
// Find the array index of a word. Note no cast is required!
Integer position = map.get("hello");
// We can also rely on autounboxing to convert directly to an int,
// but this throws a NullPointerException if the key does not exist in the map
int pos = map.get("world");
}
象List<String>这个一个参数范例其自己也是也一个范例,也可以或许被用于看成其他范例的一个范例变量值。您大概会看到这样的代码:
// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();
// The compiler knows all the types and we can write expressions
// like this without casting. We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];
// Here’s how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];
在上面的代码里,java.util.List<E>和java.util.Map<K,V>的get()要领返回一个范例为E的list元素可能一个范例为V的map元素。留意,无论如何,泛型范例可以或许更紧密的利用他们的变量。在本书中的参考章节查察List<E>,您将会看到它的iterator( )要领被声明为返回一个Iterator<E>。这意味着,这个要领返回一个跟list的实际的参数范例一样的一个参数范例的实例。为了详细的说明这点,下面的例子提供了不利用get(0)要领来获取一个List<String>的第一个元素的要领。
List<String> words = // …initialized elsewhere…
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
10. 领略泛型范例
本段将对泛型范例的利用细节做进一步的探讨,以实验说明下列问题:
不带范例参数的利用泛型的效果
参数化范例的体系
一个关于编译期泛型范例的范例安详的裂痕和一个用于确保运行期范例安详的补丁
为什么参数化范例的数组不是范例安详的
未经处理惩罚的范例和不被查抄的告诫
#p#分页标题#e#
纵然被重写的Java荟萃类带来了泛型的长处,在利用他们的时候您也不被要求说明范例变量。一个不带范例变量的泛型范例被认为是一个未经处理惩罚的范例(raw type)。这样,5.0版本以前的java代码仍然可以或许运行:您显式的编写所有范例转换就像您已经这样写的一样,您大概会被一些来自编译器的贫苦所困扰。查察下列存储差异范例的工具到一个未经处理惩罚的List:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
这段代码在java1.4下运行得很好。假如您用java5.0来编译它,javac编译了,可是会打印出这样的“诉苦”:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
假如我们插手-Xlint参数后从头编译,我们会看到这些告诫:
Test.java:6: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add("hello");
Test.java:7: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add(new Integer(123));
编译在add()要领的挪用上给出了告诫,因为它不可以或许确信插手到list中的值具有正确的范例。它汇报我们说我们利用了一个未经处理惩罚的范例,它不能验证我们的代码是范例安详的。留意,get()要领的挪用是没有问题的,因为可以或许被得到的元素已经安详的存在于list中了。
假如您不想利用任何的java5.0的新特性,您可以简朴的通过带-source1.4标志来编译他们,这样编译器就不会再“诉苦”了。假如您不能这样做,您可以忽略这些告诫,通过利用一个
“@SuppressWarnings("unchecked
")”注解(查察本章的4.3节)隐瞒这些告诫信息可能进级您的代码,插手范例变量描写。[2]下列示例代码,编译的时候不再会有告诫但仍然答允您往list中放入差异的范例的工具。
List<Object> l = new ArrayList<Object>();
l.add("hello");
l.add(123); // autoboxing
Object o = l.get(0);
参数化范例的体系
参数化范例有范例体系,就像一般的范例一样。这个别系基于工具的范例,而不是变量的范例。这里有些例子您可以实验:
ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l; // okay
Collection<Integer> n = l; // okay
ArrayList<Number> o = l; // error
Collection<Object> p = (Collection<Object>)l; // error, even with cast
一个List<Integer>是一个Collection<Integer>,但不是一个List<Object>。这句话不容易领略,假如您想领略为什么泛型这样做,这段值得看一下。考查这段代码:
List<Integer> li = new ArrayList<Integer>();
li.add(123);
// The line below will not compile. But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1); // Can’t cast a String to Integer!
这就是为什么List<Integer>不是一个List<Object>的原因,固然List<Integer>中所有的元素事实上是一个Object的实例。假如答允转换成List<Object>,那么转换后,理论上非整型的工具也将被答允添加到list中。
1
运行时范例安详
就像我们所见到的,一个List<X>不答允被转换为一个List<Y>,纵然这个X可以或许被转换为Y。然而,一个List<X>可以或许被转换为一个List,这样您就可以通过担任的要领来做这样的工作。
这种将参数化范例转换为非参数化范例的本领对付向下兼容是须要的,可是它会在泛型所带来的范例安详体系上凿个
裂痕
:
// Here’s a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;
// This line is a bug, but it compiles and runs.
// The
Java
5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we’d never even get the warning.
l.add("hello");
// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);
#p#分页标题#e#
泛型仅提供了编译期的范例安详。假如您利用java5.0的编译器来编译您的代码而且没有获得任何告诫,这些编译器的查抄可以或许确保您的代码在运行期也是范例安详的。假如您得到了告诫可能利用了像未经处理惩罚的范例那样修改您的荟萃的代码,那么您需要增加一些步调来确保运行期的范例安详。您可以通过利用java.util.Collections中的checkedList()和checkedMap( )要领来做到这一步。这些要领将把您的荟萃打包成一个wrapper荟萃,从而在运行时查抄确认只有正确范例的值可以或许被置入荟萃众。下面是一个可以或许补上范例安详裂痕的一个例子:
// Here’s a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);
// Now widen the checked list to the raw type
List l = cli;
// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");
参数化范例的数组
在利用泛型范例的时候,数组需要出格的思量。回想一下,假如T是S的父类(可能接口),那么范例为S的数组S[],同时又是范例为T的数组T[]。正因为如此,每次您存放一个工具到数组中时,Java表明器都必需举办查抄以确保您放入的工具范例与要存放的数组所答允的范例是匹对的。譬喻,下列代码在运行期会查抄失败,抛出一个ArrayStoreException异常:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1; // 1 autoboxed to an Integer, throws ArrayStoreException
固然编译时obj是一个Object[],可是在运行时它是一个String[],它不答允被用于存放一个Integer。
当我们利用泛型范例的时候,仅仅依靠运行时的数组存放异常查抄是不足的,因为一个运行时举办的查抄并不可以或许获取编译时的范例参数信息。查察下列代码:
List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali; // No ArrayStoreException
String s = wordlists[0].get(0); // ClassCastException!
假如上面的代码被答允,那么运行时的数组存储查抄将会乐成:没有编译时的范例参数,代码简朴地存储一个ArrayList到一个ArrayList[]数组,很是正确。既然编译器不能阻止您通过这个要领来战胜范例安详,那么它转而阻止您建设一个参数化范例的数组。所以上述情节永远不会产生,编译器在第一行就开始拒绝编译了。
留意这并不是一个在利用数组时利用泛型的全部的约束,这仅仅是一个建设一个参数化范例数组的约束。我们将在进修如何写泛型要领时再来接头这个话题。
范例参数通配符
假设我们需要写一个要领来显示一个List中的元素。[3]在以前,我们只需要象这样写段代码:
public static void printList(PrintWriter out, List list) { for(int i=0, n=list.size(); i < n; i++) { if (i > 0) out.print(", "); out.print(list.get(i).toString()); } }
在Java5.0中,List是一个泛型范例,假如我们试图编译这个要领,我们将会获得unchecked告诫。为了办理这些告诫,您大概需要这样来修改这个要领:
public static void printList(PrintWriter out, List<Object> list) { for(int i=0, n=list.size(); i < n; i++) { if (i > 0) out.print(", "); out.print(list.get(i).toString()); } }
这段代码可以或许编译通过同时不会有告诫,可是它并不长短常地有效,因为只有那些被声明为List<Object>的list才会被答允利用这个要领。还记得么,雷同于List<String>和List<Integer>这样的List并不能被转型为List<Object>。事实上我们需要一个范例安详的printList()要领,它可以或许接管我们传入的任何List,而不体贴它被参数化为什么。办理步伐是利用范例参数通配符。要领可以被修改成这样:
public static void printList(PrintWriter out, List<?> list) { for(int i=0, n=list.size(); i < n; i++) { if (i > 0) out.print(", "); Object o = list.get(i); out.print(o.toString()); } }
#p#分页标题#e#
这个版本的要领可以或许被编译过,没有告诫,并且可以或许在任何我们但愿利用的处所利用。通配符“?”暗示一个未知范例,范例List<?>被读作“List of unknown”
作为一般原则,假如范例是泛型的,同时您并不知道可能并不体贴值的范例,您应该利用“?”通配符来取代一个未经处理惩罚的范例。未经处理惩罚的范例被答允仅是为了向下兼容,并且应该只可以或许被答允呈此刻老的代码中。留意,无论如何,您不能在挪用结构器时利用通配符。下面的代码是犯科的:
List<?> l = new ArrayList<?>();
建设一个不知道范例的List是毫无原理的。假如您建设了它,那么您必需知道它将保持的元素是什么范例的。您可以在随后的要领中不体贴元素范例而去遍历这里list,可是您需要在您建设它的时候描写元素的范例。假如你确实需要一个List来保持任何范例,那么您只能这么写:
List<Object> l = new ArrayList<Object>();
从上面的printList()例子中,必需要搞清楚List<?>既不是List<Object>也不是一个未经处理惩罚的List。一个利用通配符的List<?>有两个重要的特性。第一,考查雷同于get()的要领,他们被声明返回一个值,这个值的范例是范例参数中指定的。在这个例子中,范例是“unknown”,所以这些要领返回一个Object。既然我们期望的是挪用这个object的toString()要领,措施可以或许很好的满意我们的意愿。
第二,考查List的雷同add()的要领,他们被声明为接管一个参数,这个参数被范例参数所界说。出人意表的是,当范例参数是未确定的,编译器不答允您挪用任何有不确定参数范例的要领——因为它不能确认您传入了一个得当的值。一个List(?)实际上是只读的——既然编译器不答允我们挪用雷同于add(),set(),addAll()这类的要领。
界定通配符
让我们在我们本来的例子上作些小小的稍微巨大一点的窜改。假设我们但愿写一个sumList()要领来计较list中Number范例的值的合计。在以前,我们利用未经处理惩罚的List,可是我们不想放弃范例安详,同时不得不处理惩罚来自编译器的unchecked告诫。可能我们可以利用List<Number>,那样的话我们就不能挪用List<Integer>、List<Double>中的要领了,而事实上我们需要挪用。假如我们利用通配符,那么我们实际上不能获得我们期望的范例安详,我们不能确定我们的要领被什么样的List所挪用,Number?照旧Number的子类?甚至,String?这样的一个要领也许会被写成这样:
public static double sumList(List<?> list) { double total = 0.0; for(Object o : list) { Number n = (Number) o; // A cast is required and may fail total += n.doubleValue(); } return total; }
要修改这个要领让它变得真正的范例安详,我们需要利用界定通配符(bounded wildcard),可以或许确保List的范例参数是未知的,但又是Number可能Number的子类。下面的代码才是我们想要的:
public static double sumList(List<? extends Number> list) {
double total = 0.0;
for(Number n : list) total += n.doubleValue();
return total;
}
范例List<? extends Number>可以被领略为“Number未知子类的List”。领略这点很是重要,在这段文字中,Number被认为是其自身的子类。
留意,这样的话,那些范例转换已经不再需要了。我们并不知道list中元素的详细范例,可是我们知道他们可以或许向上转型为Number,因此我们可以把他们从list中把他们看成一个Number工具取出。利用一个for/in轮回可以或许稍微封装一下从list中取出元素的进程。普遍性的原则是当您利用一个界定通配符时,雷同于List中的get()要领的那些要领将返回一个范例为上界的值。因此假如我们在for/in轮回中挪用list.get(),我们将获得一个Number。在前一节说到利用通配符时雷同于list.add()这种要领中的限制依然有效:举个例子来说,假如编译器答允我们挪用这类要领,我们就可以将一个Integer放到一个声明为仅保持Short值的list中去。
#p#分页标题#e#
同样可行的是利用下界通配符,差异的是用super替换extends。这个能力在被挪用的要领上有一点差异的浸染。在实际应用中,下界通配符要比上界通配符用得少。我们将在后头的章节里接头这个问题。