1.3 构造函数引用

问题

用户希望将方法引用作为流的流水线(stream pipeline)的一部分,以实例化某个对象。

方案

在方法引用中使用 new 关键字。

讨论

在讨论 Java 8 引入的新语法时,通常会提及 lambda 表达式、方法引用以及流。例如,我们希望将一份人员列表转换为相应的姓名列表。例 1-13 的代码段显示了解决这个问题的两种方案。

例 1-13 将人员列表转换为姓名列表

  1. List<String> names = people.stream()
  2. .map(person -> person.getName())
  3. .collect(Collectors.toList());
  4. // 或者采用以下方案
  5. List<String> names = people.stream()
  6. .map(Person::getName)
  7. .collect(Collectors.toList());

❶ lambda 表达式

❷ 方法引用

那么,是否存在其他解决方案呢?如何根据字符串列表来创建相应的 Person 引用列表呢?尽管仍然可以使用方法引用,不过这次我们改用关键字 new,这种语法称为构造函数引用(constructor reference)。

为了说明构造函数引用的用法,我们首先创建一个 Person 类,它是最简单的 Java 对象(POJO)。Person 类的唯一作用是包装一个名为 name 的简单字符串特性,如例 1-14 所示。

例 1-14 Person

  1. public class Person {
  2. private String name;
  3.  
  4. public Person() {}
  5.  
  6. public Person(String name) {
  7. this.name = name;
  8. }
  9.  
  10. // getter和setter
  11.  
  12. // equals、hashCode与toString方法
  13. }

如例 1-15 所示,给定一个字符串集合,通过 lambda 表达式或构造函数引用,可以将其中的每个字符串映射到 Person 类。

例 1-15 将字符串转换为 Person 实例

  1. List<String> names =
  2. Arrays.asList("Grace Hopper", "Barbara Liskov", "Ada Lovelace",
  3. "Karen Spärck Jones");
  4.  
  5. List<Person> people = names.stream()
  6. .map(name -> new Person(name))
  7. .collect(Collectors.toList());
  8.  
  9. // 或采用以下方案
  10.  
  11. List<Person> people = names.stream()
  12. .map(Person::new)
  13. .collect(Collectors.toList());

❶ 使用 lambda 表达式来调用构造函数

❷ 使用构造函数引用来实例化 Person

Person::new 的作用是引用 Person 类中的构造函数。与所有 lambda 表达式类似,由上下文决定执行哪个构造函数。由于上下文提供了一个字符串,使用单参数的 String 构造函数。

  • 复制构造函数

复制构造函数(copy constructor)传入一个 Person 参数,并返回一个具有相同特性的新 Person,如例 1-16 所示。

例 1-16 Person 的复制构造函数

  1. public Person(Person p) {
  2. this.name = p.name;
  3. }

如果需要将流代码从原始实例中分离出来,复制构造函数将很有用。假设我们有一个人员列表,先将其转换为流,再转换回列表,那么引用不会发生变化,如例 1-17 所示。

例 1-17 将列表转换为流,再转换回列表

  1. Person before = new Person("Grace Hopper");
  2.  
  3. List<Person> people = Stream.of(before)
  4. .collect(Collectors.toList());
  5. Person after = people.get(0);
  6.  
  7. assertTrue(before == after);
  8.  
  9. before.setName("Grace Murray Hopper");
  10. assertEquals("Grace Murray Hopper", after.getName());

❶ 对象相同

❷ 使用 before 引用修改人名

after 引用中的人名已被修改

如例 1-18 所示,可以通过复制构造函数来切断连接。

例 1-18 使用复制构造函数

  1. people = Stream.of(before)
  2. .map(Person::new)
  3. .collect(Collectors.toList());
  4.  
  5. after = people.get(0);
  6. assertFalse(before == after);
  7. assertEquals(before, after);
  8.  
  9. before.setName("Rear Admiral Dr. Grace Murray Hopper");
  10. assertFalse(before.equals(after));

❶ 使用复制构造函数

❷ 对象不同

❸ 但二者是等效的

可以看到,当调用 map 方法时,上下文是 Person 实例的流。因此,Person::new 调用构造函数,它传入一个 Person 实例并返回一个等效的新实例,同时切断了 beforeafter 引用之间的连接。3

  • 可变参数构造函数

接下来,我们为 Person POJO 添加一个可变参数构造函数(varargs constructor),如例 1-19 所示。

例 1-19 构造函数 Person 传入 String 的可变参数列表

  1. public Person(String... names) {
  2. this.name = Arrays.stream(names)
  3. .collect(Collectors.joining(" "));
  4. }

上述构造函数传入零个或多个字符串参数,并使用空格作为定界符将这些参数拼接在一起。

那么,如何调用这个构造函数呢?任何传入零个或多个字符串参数(由逗号隔开)的客户端都会调用这个构造函数。一种方案是利用 String 类定义的 split 方法,它传入一个定界符并返回一个 String 数组。

  1. String[] split(String delimiter)

因此,例 1-20 中的代码将列表中的每个字符串拆分为单个单词,并调用可变参数构造函数。

例 1-20 可变参数构造函数的应用

  1. names.stream()
  2. .map(name -> name.split(" "))
  3. .map(Person::new)
  4. .collect(Collectors.toList());

❶ 创建字符串流

❷ 映射到字符串数组流

❸ 映射到 Person

❹ 收集到 Person 列表

在本例中,map 方法的上下文包含 Person::new 构造函数引用,它是一个字符串数组流,因此将调用可变参数构造函数。如果为该构造函数添加一个简单的打印语句:

  1. System.out.println("Varargs ctor, names=" + Arrays.asList(names));

则输出如下结果:

  1. Varargs ctor, names=[Grace, Hopper]
  2. Varargs ctor, names=[Barbara, Liskov]
  3. Varargs ctor, names=[Ada, Lovelace]
  4. Varargs ctor, names=[Karen, Spärck, Jones]
  • 数组

构造函数引用也可以和数组一起使用。如果希望采用 Person 实例的数组(Person[])而非列表,可以使用 Stream 接口定义的 toArray 方法,它的签名为:

  1. <A> A[] toArray(IntFunction<A[]> generator)

toArray 方法采用 A 表示返回数组的泛型类型(generic type)。数组包含流的元素,由所提供的 generator 函数创建。我们甚至还能使用构造函数引用,如例 1-21 所示。

例 1-21 创建 Person 引用的数组

  1. Person[] people = names.stream()
  2. .map(Person::new)
  3. .toArray(Person[]::new);

Person 的构造函数引用

Person 数组的构造函数引用

toArray 方法参数创建了一个大小合适的 Person 引用数组,并采用经过实例化的 Person 实例进行填充。

构造函数引用其实是方法引用的别称,通过关键字 new 调用构造函数。同样,由上下文决定调用哪个构造函数。在处理流时,构造函数引用可以提供很大的灵活性。

3需要说明的是,将葛丽丝 • 霍普(Grace Hopper)将军作为本书代码中的“对象”绝无冒犯之意。作者深信,尽管霍普将军已于 1992 年去世,她的水平仍然是作者所无法企及的。(葛丽丝 • 霍普是美国海军准将,也是全球最早的程序员之一。霍普开发了 COBOL 语言,被誉为“COBOL 之母”,计算机术语 bug 和 debug 也是由霍普团队首先使用并流传开来的。——译者注)

另见

有关方法引用的讨论,参见范例 1.2。