A.5 Java 8 API示例
接下来,我们将讨论 Java 8 引入的一些新方法。
A.5.1 Stream.max方法
在 java.util.stream.Stream 接口中,max 方法的签名如下:
Optional<T> max(Comparator<? super T> comparator)
注意 Comparator 中使用的下界通配符。通过应用所提供的 Comparator,max 方法将返回流中最大的元素。由于流在为空时可能没有返回值,max 方法的返回类型为 Optional。如果找到最大值,max 方法将其包装在 Optional,否则返回空 Optional。
为简单起见,考虑例 A-13 显示的 Employee POJO。
例 A-13 简单的
EmployeePOJO
- public class Employee {
- private int id;
- private String name;
- public Employee(int id, String name) {
- this.id = id;
- this.name = name;
- }
- // 其他方法
- }
例 A-14 创建了一个员工集合并转换为 Stream,然后通过 max 方法查找具有最大 id 和最大 name(按字母顺序排序 5)的员工。实现采用匿名内部类来强调 Comparator 可以是 Employee 或 Object 类型。
5严格来说是按字典序(lexicographical order)排序,即大写字母位于小写字母之前。
例 A-14 查找最大的
Employee
- List<Employee> employees = Arrays.asList(
- new Employee(1, "Seth Curry"),
- new Employee(2, "Kevin Durant"),
- new Employee(3, "Draymond Green"),
- new Employee(4, "Klay Thompson"));
- Employee maxId = employees.stream()
- .max(new Comparator<Employee>() { ➊
- @Override
- public int compare(Employee e1, Employee e2) {
- return e1.getId() - e2.getId();
- }
- }).orElse(Employee.DEFAULT_EMPLOYEE);
- Employee maxName = employees.stream()
- .max(new Comparator<Object>() { ➋
- @Override
- public int compare(Object o1, Object o2) {
- return o1.toString().compareTo(o2.toString());
- }
- }).orElse(Employee.DEFAULT_EMPLOYEE);
- System.out.println(maxId); ➌
- System.out.println(maxName); ➍
❶ Comparator 的匿名内部类实现
❷ Comparator 的匿名内部类实现
❸ Klay Thompson(最大 ID 为 4)
❹ Seth Curry(最大姓名以字母 S 开头)
我们可以利用 Employee 类中的方法编写 Comparator,不过仅使用 Object 类定义的方法(如 toString)同样可行。由于 max 方法的定义中使用了通配符 super(Comparator comparator)),Comparator 既可以是 Employee,也可以是 Object。
然而,没有人会这样编写代码。符合 Java 8 习惯用法的实现如例 A-15 所示。
例 A-15 查找最大的
Employee(Java 8 习惯用法)
- import static java.util.Comparator.comparing;
- import static java.util.Comparator.comparingInt;
- // 创建员工列表
- Employee maxId = employees.stream()
- .max(comparingInt(Employee::getId))
- .orElse(Employee.DEFAULT_EMPLOYEE);
- Employee maxName = employees.stream()
- .max(comparing(Object::toString))
- .orElse(Employee.DEFAULT_EMPLOYEE);
- System.out.println(maxId);
- System.out.println(maxName);
上述代码显然更为简洁,但它不像匿名内部类那样强调有界通配符。
A.5.2 Stream.map方法
Stream 接口还定义了一个名为 map 的方法,它传入 Function,包括两个参数,均使用通配符:
<R> Stream<R> map(Function<? super T,? extends R> mapper)
map 方法对流中的每个元素(T 类型)应用 mapper 函数,将其转换为 R 类型 6 的一个实例。因此,map 方法的返回类型为 Stream。
6Java API 使用 T 表示单个输入变量,或 T 和 U 表示两个输入变量,以此类推。API 通常使用 R 表示返回变量。而对于映射,API 使用 K 表示键,V 表示值。
由于 Stream 被定义为具有类型参数 T 的泛型类(generic class),map 方法不必在签名中再定义变量 T,但需要使用另一个类型参数 R,以便在返回类型之前出现在签名中。如果 Stream 不是泛型类,map 方法将声明两个参数 T 和 R。
java.util.function.Function 接口定义了两个类型参数,第一个(输入参数)是从 Stream 消费的类型,第二个(输出参数)是函数产生的对象类型。通配符意味着在指定参数时,输入参数必须与 Stream 的类型相同或更高,而输出类型可以是返回流类型的任何子类型。
从 PECS 原则的角度来看,
Function接口的定义或许令人困惑,因为类型是反向的。不过只要记住Function消费T并产生R,就能理解为何super后跟T,而extends后跟R。
map 方法的应用如例 A-16 所示。
例 A-16 将
List映射到List
List<String> names = employees.stream().map(Employee::getName).collect(toList());List<String> strings = employees.stream().map(Object::toString).collect(toList());
可以看到,Function 声明了两个泛型变量,分别用于输入和输出。在第一个代码段中,方法引用 Employee::getName 使用流中的 Employee 作为输入,并返回 String 作为输出。
在第二个代码段中,由于通配符 super 的缘故,程序将输入变量作为 Object(而非 Employee)的方法处理。输出类型原则上可以是包含 String 子类的 List,但由于 String 被声明为 final,不存在任何子类。
接下来,我们讨论 Java 8 引入的部分方法签名。
A.5.3 Comparator.comparing方法
例 A-15 使用了 Comparator 接口定义的静态方法 comparing。Comparator 接口从 Java 1.0 起就已存在,开发人员或许惊讶于该接口目前包含的方法是如此之多。Java 8 将函数式接口定义为包含单一抽象方法(single abstract method)的接口。Comparator 属于函数式接口,所包含的单一抽象方法为 compare,它传入两个均为泛型类型 T 的参数。根据第一个参数小于、等于或大于第二个参数,compare 方法将分别返回负整数、0 或正整数 7。
7有关比较器的讨论请参见范例 4.1。
而 comparing 方法的签名如下:
static <T,U extends Comparable<? super U>> Comparator<T> comparing(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.comparingByKey与Map.Entry.comparingByValue 方法
最后,我们编写程序将员工添加到 Map(键为员工 ID,值为员工姓名),并根据 ID 或姓名进行排序,然后打印结果。
第一步是将员工添加到 Map。借由静态方法 Collectors.toMap,只需一行代码就能实现:
// 使用ID作为键,将员工添加到映射Map<Integer, Employee> employeeMap = employees.stream().collect(Collectors.toMap(Employee::getId, Function.identity()));
toMap 方法的签名如下:
static <T, K, U> Collector<T, ?, Map<K, U>> toMap(Function<? super T,? extends K> keyMapper,Function<? super T,? extends U> valueMapper)
Collectors 是一种工具类(仅包含静态方法),提供 Collector 接口的实现。
从 toMap 方法的签名可以看到,它传入两个函数作为参数,一个用于生成键,另一个用于在输出映射中生成值。toMap 方法的返回类型为 Collector,它定义了三种泛型参数。
根据 Javadoc 的描述,Collector 接口的签名如下:
public interface Collector<T,A,R>
三种泛型类型的定义如下。
T:归约操作的输入元素类型;A:归约操作的可变累加类型(通常隐藏为实现细节);R:归约操作的结果。
Employee::getId 相当于 toMap 方法签名中的 keyMapper。换言之,T 是 Integer;而结果 R 是 Map 接口的实现,它使用 Integer 替换 K,Employee 替换 U。
有意思的是,Collector 接口定义中的变量 A 是 Map 接口的实际实现。它可能是 HashMap8,但我们不得而知,因为结果用作 toMap 方法的参数,无法被观察到。不过在 Collector 中,类型使用无界通配符 ?,这意味着类型在内部要么仅使用 Object 类中的方法,要么使用 Map 接口中不特定于类型的方法。实际上,在调用 keyMapper 和 valueMapper 函数后,类型仅使用 Map 接口新增的默认方法 merge。
8在引用实现中的确是 HashMap。
为实现排序,Java 8 为 Map.Entry 接口引入了静态方法 comparingByKey 和 comparingByValue。如例 A-17 所示,程序根据键对映射元素排序,然后打印结果。
例 A-17 根据键对映射元素排序并打印结果
Map<Integer, Employee> employeeMap = employees.stream().collect(Collectors.toMap(Employee::getId, Function.identity())); ➊System.out.println("Sorted by key:");employeeMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {System.out.println(entry.getKey() + ": " + entry.getValue()); ➋});
❶ 使用 ID 作为键,将员工添加到 Map
❷ 根据 ID 对员工排序,然后打印结果
comparingByKey 方法的签名如下:
static <K extends Comparable<? super K>,V>Comparator<Map.Entry<K,V>> comparingByKey()
comparingByKey 方法不传入任何参数,它返回一个比较 Map.Entry 实例的 Comparator。由于我们根据键比较员工姓名,键 K 的声明泛型类型必须是 Comparable 的子类型,才能执行实际的比较操作。当然,Comparable 本身定义了泛型类型 K 或 K 的某种父类型,这意味着 compareTo 方法可以使用 K 类(或更高)的属性。
根据键进行排序的结果如下:
Sorted by key:1: Seth Curry2: Kevin Durant3: Draymond Green4: Klay Thompson
根据值进行排序则有些复杂。如果不了解泛型类型的相关知识,就很难理解错误的成因。 comparingByValue 方法的签名如下:
static <K,V extends Comparable<? super V>> Comparator<Map.Entry<K,V>>comparingByValue()
与 comparingByKey 方法不同,在 comparingByValue 方法中,V 必须是 Comparable 的子类型。
根据值排序时,很容易写出下面这样的代码:
// 根据员工姓名排序,然后打印结果(无法编译)employeeMap.entrySet().stream().sorted(Map.Entry.comparingByValue()).forEach(entry -> {System.out.println(entry.getKey() + ": " + entry.getValue());});
不过代码无法编译,程序会提示错误:
Java: incompatible types: inference variable V has incompatible boundsequality constraints: generics.Employeeupper bounds: java.lang.Comparable<? super V>
原因在于映射中的值是 Employee 的实例,但 Employee 并未实现 Comparable。好在 comparingByValue 方法还包括一种重载形式:
static <K,V> Comparator<Map.Entry<K,V>> comparingByValue(Comparator<? super V> cmp)
comparingByValue 方法传入 Comparator 作为参数,并返回一个新的 Comparator,它根据值比较各个 Map.Entry 元素。对映射值排序的正确方式如例 A-18 所示。
例 A-18 根据值对映射元素排序并打印结果
// 根据员工姓名排序,然后打印结果System.out.println("Sorted by name:");employeeMap.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.comparing(Employee::getName))).forEach(entry -> {System.out.println(entry.getKey() + ": " + entry.getValue());});
通过为 comparing 方法提供方法引用 Employee::getName,就能实现按员工姓名的自然顺序排序:
Sorted by name:3: Draymond Green2: Kevin Durant4: Klay Thompson1: Seth Curry
希望上述示例能提供足够的背景知识,以免读者在阅读和使用 Java API 时对泛型感到困惑。
A.5.5 类型擦除
使用 Java 这样的语言开发时,如何保持长久以来的向后兼容性让人颇费脑筋,开发团队为此做了不少努力。以泛型为例,与泛型有关的信息将在编译阶段被删除,从而不会为参数化类型创建新的类,避免了可能出现的运行时错误。这称为类型擦除(type erasure)。
由于所有操作均在后台完成,开发人员真正需要了解的是在编译时:
- 有界类型参数被替换为参数边界;
- 无界类型参数被替换为
Object; - 在需要时插入类型强制转换;
- 生成桥接方法(bridge method)以保持多态(polymorphism)。
对类型而言,结果相当简单。Map 接口定义了两种泛型类型,其中 K 代表键,V 代表值。在实例化 Map 时,编译器分别用 Integer 和 Employee 替换 K 和 V。
从 Map.Entry.comparingByKey 方法的签名可以看到,键被声明为 K extends Comparable,这使得类中所有出现的 K 都会被替换为 Comparable。
Function 接口定义了两种泛型类型 T 和 R,所包含的单一抽象方法为:
R apply(T t)
从 Stream.map 方法的签名可以看到,其边界为 Function。观察例 A-16 中的 map 方法:
List<String> names = employees.stream().map(Employee::getName).collect(Collectors.toList());
Function 采用 Employee 替换 T(因为这是一个由员工构成的流),采用 String 替换 R(因为 getName 的返回类型为 String)。
关于类型擦除的讨论大致如此,但某些极端情况并未考虑在内。感兴趣的读者可以参考 Java 官方教程(Java Tutorials),不过类型擦除或许是所有技术中最简单的概念。
从 PECS 原则的角度来看,