1.2 方法引用

问题

用户希望使用方法引用来访问某个现有的方法,并将其作为 lambda 表达式进行处理。

方案

使用双冒号表示法(::)将实例引用或类名与方法分开。

讨论

如果说 lambda 表达式本质上是将方法作为对象进行处理,那么方法引用就是将现有方法作为 lambda 表达式进行处理。

例如,java.lang.Iterable 接口的 forEach 方法传入 Consumer 作为参数。如例 1-8 所示,Consumer 可以作为 lambda 表达式或方法引用来实现。

例 1-8 利用方法引用访问 println 方法

  1. Stream.of(3, 1, 4, 1, 5, 9)
  2. .forEach(x -> System.out.println(x));
  3. Stream.of(3, 1, 4, 1, 5, 9)
  4. .forEach(System.out::println);
  5. Consumer<Integer> printer = System.out::println;
  6. Stream.of(3, 1, 4, 1, 5, 9)
  7. .forEach(printer);

➊ 使用 lambda 表达式

➋ 使用方法引用

➌ 将方法引用赋给函数式接口

双冒号表示法在 System.out 实例上提供了对 println 方法的引用,它属于 PrintStream 类型的引用。方法引用的末尾无须括号。在本例中,程序将流的所有元素打印到标准输出。2

2讨论 lambda 表达式或方法引用时,很难不涉及流,第 3 章将专门讨论流。目前可以这样理解:流依次产生一系列元素,但不会将它们存储在任何位置,也不会对原始源进行修改。

1.2 方法引用 - 图1 如果编写一个只有一行的 lambda 表达式来调用方法,不妨考虑改用等价的方法引用。

与 lambda 语法相比,方法引用具有几个(不那么显著的)优点。首先,方法引用往往更短。其次,方法引用通常包括含有该方法的类的名称。这两点使得代码更易于阅读。

如例 1-9 所示,方法引用也可以和静态方法一起使用。

例 1-9 在静态方法中使用方法引用

  1. Stream.generate(Math::random)
  2. .limit(10)
  3. .forEach(System.out::println);

❶ 静态方法

❷ 实例方法

Stream 接口定义的 generate 方法传入 Supplier 作为参数。Supplier 是一个函数式接口,其单一抽象方法 get 不传入任何参数且只生成一个结果。Math 类的 random 方法与 get 方法的签名相互兼容,因为 random 方法同样不传入任何参数,且产生一个 0 到 1 之间、均匀分布的双精度伪随机数。方法引用 Math::random 表示该方法是 Supplier 接口的实现。

由于 Stream.generate 方法产生的是一个无限流(infinite stream),我们通过 limit 方法限定只生成 10 个值,然后使用方法引用 System.out::println 将这些值打印到标准输出,作为 Consumer 的实现。

语法

方法引用包括以下三种形式,其中一种存在一定的误导性。

object::instanceMethod

  引用特定对象的实例方法,如 System.out::println

Class::staticMethod

  引用静态方法,如 Math::max

Class::instanceMethod

  调用特定类型的任意对象的实例方法,如 String::length

最后一种形式或许令人困惑,因为在 Java 开发中,一般只通过类名来调用静态方法。请记住,lambda 表达式和方法引用在任何情况下都不能脱离上下文存在。以对象引用为例,上下文提供了方法的参数。对于 System.out::println,等效的 lambda 表达式为(如例 1-8 中的上下文所示):

  1. // 相当于System.out::println
  2. x -> System.out.println(x)

上下文提供了 x 的值,它被用作方法的参数。

静态方法 max 与之类似:

  1. // 相当于Math::max
  2. (x,y) -> Math.max(x,y)

此时,上下文需要提供两个参数,lambda 表达式返回较大的参数。

“通过类名来调用实例方法”语法的解释有所不同,其等效的 lambda 表达式为:

  1. // 相当于String::length
  2. x -> x.length()

此时,当上下文提供 x 的值时,它将用作方法的目标而非参数。

1.2 方法引用 - 图2 如果通过类名引用一个传入多个参数的方法,则上下文提供的第一个元素将作为方法的目标,其他元素作为方法的参数。

例 1-10 显示了相应的代码。

例 1-10 从类引用(class reference)调用多参数实例方法

  1. List<String> strings =
  2. Arrays.asList("this", "is", "a", "list", "of", "strings");
  3. List<String> sorted = strings.stream()
  4. .sorted((s1, s2) -> s1.compareTo(s2))
  5. .collect(Collectors.toList());
  6. List<String> sorted = strings.stream()
  7. .sorted(String::compareTo)
  8. .collect(Collectors.toList());

➊ 方法引用及其等效的 lambda 表达式

Stream 接口定义的 sorted 方法传入 Comparator 作为参数,其单一抽象方法为 int compare(String other)sorted 方法将每对字符串提供给比较器,并根据返回整数的符号对它们进行排序。在本例中,上下文是一对字符串。方法引用语法(采用类名 String)调用第一个元素(lambda 表达式中的 s1)的 compareTo 方法,并使用第二个元素 s2 作为该方法的参数。

在流处理中,如果需要处理一系列输入,则会频繁使用方法引用中的类名来访问实例方法。例 1-11 显示了对流中各个 String 调用 length 方法。

例 1-11 使用方法引用在 String 上调用 length 方法

  1. Stream.of("this", "is", "a", "stream", "of", "strings")
  2. .map(String::length)
  3. .forEach(System.out::println);

❶ 通过类名访问实例方法

❷ 通过对象引用访问实例方法

程序调用 length 方法将每个字符串转换为一个整数,然后打印所有结果。

方法引用本质上属于 lambda 表达式的一种简化语法。lambda 表达式在实际中更常见,因为每个方法引用都存在一个等效的 lambda 表达式,反之则不然。对于例 1-11 中的方法引用,其等效的 lambda 表达式如例 1-12 所示。

例 1-12 方法引用的等效 lambda 表达式

  1. Stream.of("this", "is", "a", "stream", "of", "strings")
  2. .map(s -> s.length())
  3. .forEach(x -> System.out.println(x));

对任何 lambda 表达式来说,上下文都很重要。为避免歧义,不妨在方法引用的左侧使用 thissuper

另见

用户也可以使用方法引用语法来调用构造函数,相关讨论参见范例 1.3。第 2 章将讨论 java.util.function 包以及本范例中出现的 Supplier 接口。