北京北大青鸟学校讲解:Java泛型的基础知识(一)

北京北大青鸟学校学术部今年将继续讲解关于Java泛型的一些技术知识,关于Java泛型的定义、相关例子等请参考之前的两篇文章,在这里就不做陈述了。

类型参数
北京北大青鸟学校讲师介绍:在定义泛型类或声明泛型类的变量时,使用尖括号来指定形式类型参数。形式类型参数与实际类型参数之间的关系类似于形式方法参数与实际方法参数之间的关系,只是类型参数表示类型,而不是表示值。

泛型类中的类型参数几乎可以用于任何可以使用类名的地方。例如,下面是 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 类型。(北大青鸟课程

除了异常类型、枚举或匿名内部类以外,任何类都可以具有类型参数。

命名类型参数
北京北大青鸟学校讲师介绍:推荐的命名约定是使用大写的单个字母名称作为类型参数。这与 C++ 约定有所不同(参阅 附录 A:与 C++ 模板的比较),并反映了大多数泛型类将具有少量类型参数的假定。对于常见的泛型模式,推荐的名称是:
K —— 键,比如映射的键。
V —— 值,比如 List 和 Set 的内容,或者 Map 中的值。
E —— 异常类。
T —— 泛型。

泛型不是协变的
关于泛型的混淆,一个常见的来源就是假设它们像数组一样是协变的。其实它们不是协变的。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 中。但是,正如下一屏将会看到的,您有一个更加灵活的方式来定义泛型。(北大青鸟课程

类型通配符
北京北大青鸟学校讲师介绍:假设您具有该方法:
void printList(List l) {
for (Object o : l)
    System.out.println(o);
}

上面的代码在 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()。

类型通配符的作用
北京北大青鸟学校讲师介绍:前一屏 类型通配符 中引入了类型通配符,这让您可以声明 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();(北京北大青鸟学校,未完待续)

北大青鸟网上报名
北大青鸟招生简章