A.5 Java 8 API示例

接下来,我们将讨论 Java 8 引入的一些新方法。

A.5.1 Stream.max方法

java.util.stream.Stream 接口中,max 方法的签名如下:

  1. Optional<T> max(Comparator<? super T> comparator)

注意 Comparator 中使用的下界通配符。通过应用所提供的 Comparatormax 方法将返回流中最大的元素。由于流在为空时可能没有返回值,max 方法的返回类型为 Optional。如果找到最大值,max 方法将其包装在 Optional,否则返回空 Optional

为简单起见,考虑例 A-13 显示的 Employee POJO。

例 A-13 简单的 Employee POJO

  1. public class Employee {
  2. private int id;
  3. private String name;
  4.  
  5. public Employee(int id, String name) {
  6. this.id = id;
  7. this.name = name;
  8. }
  9.  
  10. // 其他方法
  11. }

例 A-14 创建了一个员工集合并转换为 Stream,然后通过 max 方法查找具有最大 id 和最大 name(按字母顺序排序 5)的员工。实现采用匿名内部类来强调 Comparator 可以是 EmployeeObject 类型。

5严格来说是按字典序(lexicographical order)排序,即大写字母位于小写字母之前。

例 A-14 查找最大的 Employee

  1. List<Employee> employees = Arrays.asList(
  2. new Employee(1, "Seth Curry"),
  3. new Employee(2, "Kevin Durant"),
  4. new Employee(3, "Draymond Green"),
  5. new Employee(4, "Klay Thompson"));
  6.  
  7. Employee maxId = employees.stream()
  8. .max(new Comparator<Employee>() {
  9. @Override
  10. public int compare(Employee e1, Employee e2) {
  11. return e1.getId() - e2.getId();
  12. }
  13. }).orElse(Employee.DEFAULT_EMPLOYEE);
  14.  
  15. Employee maxName = employees.stream()
  16. .max(new Comparator<Object>() {
  17. @Override
  18. public int compare(Object o1, Object o2) {
  19. return o1.toString().compareTo(o2.toString());
  20. }
  21. }).orElse(Employee.DEFAULT_EMPLOYEE);
  22.  
  23. System.out.println(maxId);
  24. System.out.println(maxName);

Comparator 的匿名内部类实现

Comparator 的匿名内部类实现

❸ Klay Thompson(最大 ID 为 4)

❹ Seth Curry(最大姓名以字母 S 开头)

我们可以利用 Employee 类中的方法编写 Comparator,不过仅使用 Object 类定义的方法(如 toString)同样可行。由于 max 方法的定义中使用了通配符 superComparator comparator)),Comparator 既可以是 Employee,也可以是 Object

然而,没有人会这样编写代码。符合 Java 8 习惯用法的实现如例 A-15 所示。

例 A-15 查找最大的 Employee(Java 8 习惯用法)

  1. import static java.util.Comparator.comparing;
  2. import static java.util.Comparator.comparingInt;
  3.  
  4. // 创建员工列表
  5.  
  6. Employee maxId = employees.stream()
  7. .max(comparingInt(Employee::getId))
  8. .orElse(Employee.DEFAULT_EMPLOYEE);
  9.  
  10. Employee maxName = employees.stream()
  11. .max(comparing(Object::toString))
  12. .orElse(Employee.DEFAULT_EMPLOYEE);
  13.  
  14. System.out.println(maxId);
  15. System.out.println(maxName);

上述代码显然更为简洁,但它不像匿名内部类那样强调有界通配符。

A.5.2 Stream.map方法

Stream 接口还定义了一个名为 map 的方法,它传入 Function,包括两个参数,均使用通配符:

  1. <R> Stream<R> map(Function<? super T,? extends R> mapper)

map 方法对流中的每个元素(T 类型)应用 mapper 函数,将其转换为 R 类型 6 的一个实例。因此,map 方法的返回类型为 Stream

6Java API 使用 T 表示单个输入变量,或 TU 表示两个输入变量,以此类推。API 通常使用 R 表示返回变量。而对于映射,API 使用 K 表示键,V 表示值。

由于 Stream 被定义为具有类型参数 T 的泛型类(generic class),map 方法不必在签名中再定义变量 T,但需要使用另一个类型参数 R,以便在返回类型之前出现在签名中。如果 Stream 不是泛型类,map 方法将声明两个参数 TR

java.util.function.Function 接口定义了两个类型参数,第一个(输入参数)是从 Stream 消费的类型,第二个(输出参数)是函数产生的对象类型。通配符意味着在指定参数时,输入参数必须与 Stream 的类型相同或更高,而输出类型可以是返回流类型的任何子类型。

A.5 Java 8 API示例 - 图1 从 PECS 原则的角度来看,Function 接口的定义或许令人困惑,因为类型是反向的。不过只要记住 Function 消费 T 并产生 R,就能理解为何 super 后跟 T,而 extends 后跟 R

map 方法的应用如例 A-16 所示。

例 A-16 将 List 映射到 List

  1. List<String> names = employees.stream()
  2. .map(Employee::getName)
  3. .collect(toList());
  4. List<String> strings = employees.stream()
  5. .map(Object::toString)
  6. .collect(toList());

可以看到,Function 声明了两个泛型变量,分别用于输入和输出。在第一个代码段中,方法引用 Employee::getName 使用流中的 Employee 作为输入,并返回 String 作为输出。

在第二个代码段中,由于通配符 super 的缘故,程序将输入变量作为 Object(而非 Employee)的方法处理。输出类型原则上可以是包含 String 子类的 List,但由于 String 被声明为 final,不存在任何子类。

接下来,我们讨论 Java 8 引入的部分方法签名。

A.5.3 Comparator.comparing方法

例 A-15 使用了 Comparator 接口定义的静态方法 comparingComparator 接口从 Java 1.0 起就已存在,开发人员或许惊讶于该接口目前包含的方法是如此之多。Java 8 将函数式接口定义为包含单一抽象方法(single abstract method)的接口。Comparator 属于函数式接口,所包含的单一抽象方法为 compare,它传入两个均为泛型类型 T 的参数。根据第一个参数小于、等于或大于第二个参数,compare 方法将分别返回负整数、0 或正整数 7。

7有关比较器的讨论请参见范例 4.1。

comparing 方法的签名如下:

  1. static <T,U extends Comparable<? super U>> Comparator<T> comparing(
  2. Function<? super T,? extends U> keyExtractor)

观察 comparing 方法的参数可以看到,其名称为 keyExtractor,类型为 Function。与之前类似,Function 定义了两个泛型类型,分别用于输入和输出。输入的下界由输入类型 T 指定,而输出的上界由输出类型 U 指定。参数名在这里作为键使用:函数采用某种方法提取出需要排序的属性,comparing 方法通过返回 Comparator 来完成这项工作。

我们希望使用给定属性 U 对流排序,因此 U 必须实现 Comparable。换言之,在声明 U 时,U 必须要继承 Comparable。当然,Comparable 本身是一种类型化接口(typed interface),其类型通常为 U,但也可以是 U 的任何超类。

comparing 方法最终返回的是 Comparator,然后 Stream 接口定义的其他方法使用 Comparator 对流排序,结果流与原始流的类型相同。

comparing 方法的用法请参见例 A-15。

A.5.4 Map.Entry.comparingByKeyMap.Entry.comparingByValue 方法

最后,我们编写程序将员工添加到 Map(键为员工 ID,值为员工姓名),并根据 ID 或姓名进行排序,然后打印结果。

第一步是将员工添加到 Map。借由静态方法 Collectors.toMap,只需一行代码就能实现:

  1. // 使用ID作为键,将员工添加到映射
  2. Map<Integer, Employee> employeeMap = employees.stream()
  3. .collect(Collectors.toMap(Employee::getId, Function.identity()));

toMap 方法的签名如下:

  1. static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
  2. Function<? super T,? extends K> keyMapper,
  3. Function<? super T,? extends U> valueMapper)

Collectors 是一种工具类(仅包含静态方法),提供 Collector 接口的实现。

toMap 方法的签名可以看到,它传入两个函数作为参数,一个用于生成键,另一个用于在输出映射中生成值。toMap 方法的返回类型为 Collector,它定义了三种泛型参数。

根据 Javadoc 的描述,Collector 接口的签名如下:

  1. public interface Collector<T,A,R>

三种泛型类型的定义如下。

  • T:归约操作的输入元素类型;
  • A:归约操作的可变累加类型(通常隐藏为实现细节);
  • R:归约操作的结果。

Employee::getId 相当于 toMap 方法签名中的 keyMapper。换言之,TInteger;而结果 RMap 接口的实现,它使用 Integer 替换 KEmployee 替换 U

有意思的是,Collector 接口定义中的变量 AMap 接口的实际实现。它可能是 HashMap8,但我们不得而知,因为结果用作 toMap 方法的参数,无法被观察到。不过在 Collector 中,类型使用无界通配符 ?,这意味着类型在内部要么仅使用 Object 类中的方法,要么使用 Map 接口中不特定于类型的方法。实际上,在调用 keyMappervalueMapper 函数后,类型仅使用 Map 接口新增的默认方法 merge

8在引用实现中的确是 HashMap

为实现排序,Java 8 为 Map.Entry 接口引入了静态方法 comparingByKeycomparingByValue。如例 A-17 所示,程序根据键对映射元素排序,然后打印结果。

例 A-17 根据键对映射元素排序并打印结果

  1. Map<Integer, Employee> employeeMap = employees.stream()
  2. .collect(Collectors.toMap(Employee::getId, Function.identity()));
  3. System.out.println("Sorted by key:");
  4. employeeMap.entrySet().stream()
  5. .sorted(Map.Entry.comparingByKey())
  6. .forEach(entry -> {
  7. System.out.println(entry.getKey() + ": " + entry.getValue());
  8. });

❶ 使用 ID 作为键,将员工添加到 Map

❷ 根据 ID 对员工排序,然后打印结果

comparingByKey 方法的签名如下:

  1. static <K extends Comparable<? super K>,V>
  2. Comparator<Map.Entry<K,V>> comparingByKey()

comparingByKey 方法不传入任何参数,它返回一个比较 Map.Entry 实例的 Comparator。由于我们根据键比较员工姓名,键 K 的声明泛型类型必须是 Comparable 的子类型,才能执行实际的比较操作。当然,Comparable 本身定义了泛型类型 KK 的某种父类型,这意味着 compareTo 方法可以使用 K 类(或更高)的属性。

根据键进行排序的结果如下:

  1. Sorted by key:
  2. 1: Seth Curry
  3. 2: Kevin Durant
  4. 3: Draymond Green
  5. 4: Klay Thompson

根据值进行排序则有些复杂。如果不了解泛型类型的相关知识,就很难理解错误的成因。 comparingByValue 方法的签名如下:

  1. static <K,V extends Comparable<? super V>> Comparator<Map.Entry<K,V>>
  2. comparingByValue()

comparingByKey 方法不同,在 comparingByValue 方法中,V 必须是 Comparable 的子类型。

根据值排序时,很容易写出下面这样的代码:

  1. // 根据员工姓名排序,然后打印结果(无法编译)
  2. employeeMap.entrySet().stream()
  3. .sorted(Map.Entry.comparingByValue())
  4. .forEach(entry -> {
  5. System.out.println(entry.getKey() + ": " + entry.getValue());
  6. });

不过代码无法编译,程序会提示错误:

  1. Java: incompatible types: inference variable V has incompatible bounds
  2. equality constraints: generics.Employee
  3. upper bounds: java.lang.Comparable<? super V>

原因在于映射中的值是 Employee 的实例,但 Employee 并未实现 Comparable。好在 comparingByValue 方法还包括一种重载形式:

  1. static <K,V> Comparator<Map.Entry<K,V>> comparingByValue(
  2. Comparator<? super V> cmp)

comparingByValue 方法传入 Comparator 作为参数,并返回一个新的 Comparator,它根据值比较各个 Map.Entry 元素。对映射值排序的正确方式如例 A-18 所示。

例 A-18 根据值对映射元素排序并打印结果

  1. // 根据员工姓名排序,然后打印结果
  2. System.out.println("Sorted by name:");
  3. employeeMap.entrySet().stream()
  4. .sorted(Map.Entry.comparingByValue(Comparator.comparing(Employee::getName)))
  5. .forEach(entry -> {
  6. System.out.println(entry.getKey() + ": " + entry.getValue());
  7. });

通过为 comparing 方法提供方法引用 Employee::getName,就能实现按员工姓名的自然顺序排序:

  1. Sorted by name:
  2. 3: Draymond Green
  3. 2: Kevin Durant
  4. 4: Klay Thompson
  5. 1: Seth Curry

希望上述示例能提供足够的背景知识,以免读者在阅读和使用 Java API 时对泛型感到困惑。

A.5.5 类型擦除

使用 Java 这样的语言开发时,如何保持长久以来的向后兼容性让人颇费脑筋,开发团队为此做了不少努力。以泛型为例,与泛型有关的信息将在编译阶段被删除,从而不会为参数化类型创建新的类,避免了可能出现的运行时错误。这称为类型擦除(type erasure)。

由于所有操作均在后台完成,开发人员真正需要了解的是在编译时:

  • 有界类型参数被替换为参数边界;
  • 无界类型参数被替换为 Object
  • 在需要时插入类型强制转换;
  • 生成桥接方法(bridge method)以保持多态(polymorphism)。

对类型而言,结果相当简单。Map 接口定义了两种泛型类型,其中 K 代表键,V 代表值。在实例化 Map 时,编译器分别用 IntegerEmployee 替换 KV

Map.Entry.comparingByKey 方法的签名可以看到,键被声明为 K extends Comparable,这使得类中所有出现的 K 都会被替换为 Comparable

Function 接口定义了两种泛型类型 TR,所包含的单一抽象方法为:

  1. R apply(T t)

Stream.map 方法的签名可以看到,其边界为 Function。观察例 A-16 中的 map 方法:

  1. List<String> names = employees.stream()
  2. .map(Employee::getName)
  3. .collect(Collectors.toList());

Function 采用 Employee 替换 T(因为这是一个由员工构成的流),采用 String 替换 R(因为 getName 的返回类型为 String)。

关于类型擦除的讨论大致如此,但某些极端情况并未考虑在内。感兴趣的读者可以参考 Java 官方教程(Java Tutorials),不过类型擦除或许是所有技术中最简单的概念。