A.2 众所周知的事实

在使用 ListSet 这样的集合时,可以将元素的类名置于尖括号中,以声明所包含元素的类型:

  1. List<String> strings = new ArrayList<String>();
  2. Set<Employee> employees = new HashSet<Employee>();

A.2 众所周知的事实 - 图1 通过在之后的示例代码中引入钻石运算符(diamond operator),Java 7 能在一定程度上简化语法。由于等号左侧的引用已经声明了集合以及所包含的类型(如 ListList),等号右侧的实例化无须再次声明。我们可以将其简写为 new ArrayList<>(),而不必将类型置于尖括号中。

声明集合的数据类型可以实现两个目的:

  • 能避免不慎将错误的类型置于集合中
  • 无须再将检索到的值强制转换为合适的类型

如例 A-1 所示,在声明 strings 变量之后,就只能向集合添加 String 实例,并在检索到某项时自动获得一个 String

例 A-1 简单的泛型示例

  1. List<String> strings = new ArrayList<>();
  2. strings.add("Hello");
  3. strings.add("World");
  4. // strings.add(new Date()); ➊
  5. // Integer i = strings.get(0); ➊
  6.  
  7. for (String s : strings) {
  8. System.out.printf("%s has length %d%n", s, s.length());
  9. }

❶ 无法编译

❷ for-each 循环了解所包含的数据类型为 String

对插入过程应用类型安全(type safety)很方便,但开发人员很少会犯这个错误。不过,如果不必首先强制转换就可以处理检索类型,能极大简化代码。1

1在整个职业生涯中,我从未不慎将错误的类型添加到列表中。不过即便只是考虑到糟糕的语法,去掉强制转换过程也是值得的。

另一个众所周知的事实是,无法为泛型集合添加基本数据类型(primitive type)。换言之,目前尚无法定义 ListList。2 幸运的是,Java 1.5 在引入泛型的同时也引入了自动装箱和拆箱。因此,如果希望在泛型类型(generic type)中储存基本数据类型,可以通过包装类(wrapper class)声明该类型,如例 A-2 所示。

2Java 10(Valhalla 项目)已提出将基本数据类型添加到集合中。

例 A-2 在泛型集合中使用基本数据类型

  1. List<Integer> ints = new ArrayList<>();
  2. ints.add(3); ints.add(1); ints.add(4);
  3. ints.add(1); ints.add(9); ints.add(2);
  4. System.out.println(ints);
  5.  
  6. for (int i : ints) {
  7. System.out.println(i);
  8. }

可以看到,Java 在插入时将 int 值包装在 Integer 实例中,并在检索时从 Integer 实例中取出这些值。尽管装箱和拆箱的效率有待商榷,但代码确实很容易编写。

此外,Java 开发人员耳熟能详的一点是,如果一个类使用了泛型,那么类型本身采用尖括号中的大写字母表示。例如,Javadoc 对 java.util.List 接口的描述如下:

  1. public interface List<E> extends Collection<E>

其中 E 是类型参数(type parameter),且接口中的方法使用相同的类型参数。例 A-3 显示了 List 接口声明的部分方法。

例 A-3 List 接口声明的部分方法

  1. boolean add(E e)
  2. boolean addAll(Collection<? extends E> c)
  3. void clear()
  4. boolean contains(Object o)
  5. boolean containsAll(Collection<?> c)
  6. E get(int index)

❶ 类型参数 E 用作参数或返回类型

有界通配符

❸ 与类型本身无关的方法

未知类型

可以看到,某些方法使用声明的泛型类型 E 作为参数或返回类型,某些方法(特别是 clearcontains)完全不使用类型,还有部分方法使用问号作为通配符。

请注意,在非泛型类中声明泛型方法是合法的。这种情况下,泛型参数被声明为方法签名的一部分。以工具类 java.util.Collections 为例,它定义了以下静态方法:

  1. static <T> List<T> emptyList()
  2. static <K,V> Map<K,V> emptyMap()
  3. static <T> boolean addAll(Collection<? super T> c, T... elements)
  4. static <T extends Object & Comparable<? super T>>
  5. T min(Collection<? extends T> coll)

如上所示,emptyListaddAllmin 这三种方法声明了泛型参数 TemptyList 方法通过 T 来指定 List 中包含的类型,而 emptyMap 方法在泛型映射中使用 KV 来表示键的类与值的类。

addAll 方法声明了泛型类型 T,并使用 Collection c 作为方法的第一个参数,T 类型的可变参数列表作为第二个参数。? super T 是一种有界通配符(bounded wildcard),稍后将对此做讨论。

min 方法可以看出泛型类型是如何提供安全性的,但其签名结构或许不那么一目了然。后面将详细讨论该方法的签名,目前不妨这样理解:T 是有界的,它既是 Object 的子类,又实现了 Comparable 接口,其中 Comparable 定义为 TT 的任何父类。min 方法的参数与 TT 的任何子类的 Collection 有关。

最后,通配符将众所周知的语法以我们不那么熟悉的形式表现出来。例如,某些语法看似继承,但实际上根本不是。