Upgrading to Java 8——第二章 Method References(方法引用)

时间:2023-01-16 16:40:57

概述

很多java 方法会使用函数式接口作为参数。例如,java.util.Arrays类中的一个sort方法,就接受一个Comparator接口,它就是一个函数式接口,sort方法的签名如下:

public static T[] sort(T[] array, Comparator<? super T> comparator)

相对于传递一个Compartor的实例给sort方法,不如传递一个Lambda表达式。

进一步,我们可以传递一个方法引用来代替Lambda表达式,一个简单的方法引用就是一个类名或是实例名后面紧跟着::符号,最后面是方法名。

为什么想用方法引用?主要有两个原因:

  1.方法引用比Lambda表达式有更短的语义,因为方法引用不像Lambda表达式那样包含定义,方法引用的主体已经在别的地方定义了。

  2.实现代码复用。

你可以使用引用给静态方法,实例方法甚至构造方法,在java8 中使用心得标识符“::”,使类名/实例引用和方法名/构造方法名分开,类封装了引用实例但并没有函数式接口的实现。

方法引用的语法有下面几种定义:

ClassName::staticMethodName
ContainingType::instanceMethod
objectReference::methodName
ClassName::new

静态方法引用

我们可以传递一个静态方法引用给一个函数接口的抽象方法,如果该抽象方法的参数和返回类型能够兼容静态方法引用。

请看下面的例子:

import java.util.Arrays;
import java.util.List; public class NoMethodRef { @FunctionalInterface
interface StringListFormatter {
String format(String delimiter, List<String> list);
} public static void formatAndPrint(StringListFormatter formatter, String delimiter, List<String> list) {
String formatted = formatter.format(delimiter, list);
System.out.println(formatted);
} public static void main(String[] args) {
List<String> names = Arrays.asList("Don", "King", "Kong"); StringListFormatter formatter = (delimiter, list) -> {
StringBuilder sb = new StringBuilder(100);
int size = list.size();
for (int i = 0; i < size; i++) {
sb.append(list.get(i));
if (i < size - 1) {
sb.append(delimiter);
}
}
return sb.toString();
};
formatAndPrint(formatter, ", ", names);
}
}

NoMethodRef类定义了一个接口StringListFormatter用来传入一个字符串类型list ,根据分隔符格式化字符串。

运行结果:Don, King, Kong

花了20分钟编写这代码,其实在JDK 1.8 以后,String类中添加了join方法做个刚才我们编码的工作,join方法签名如下:

public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)

比较我们自己写的format方法的签名:

public String format(java.langString delimiterjava.util.List<String> list);

因为List继承了Iterable类,String实现了CharSequence接口,所以join兼容format方法。

静态引用方法允许你复用已经实现好的方法,下面的代码使用了join方法:

import java.util.Arrays;
import java.util.List; public class MethodReferenceDemo1 { @FunctionalInterface
interface StringListFormatter {
String format(String delimiter, List<String> list);
} public static void formatAndPrint(StringListFormatter formatter, String delimiter, List<String> list) {
String formatted = formatter.format(delimiter, list);
System.out.println(formatted);
} public static void main(String[] args) {
List<String> names = Arrays.asList("Don", "King", "Kong");
formatAndPrint(String::join, ", ", names);
}
}

还有一点, StringListFormatter 接口的抽象方法有两个参数并有一个返回值,BiFunction这是它绝佳的替代者,下面的例子用BiFunction来改进。

import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction; public class WithBiFunction { public static void formatAndPrint(BiFunction<String, List<String>, String> formatter, String delimiter,
List<String> list) {
String formatted = formatter.apply(delimiter, list);
System.out.println(formatted);
} public static void main(String[] args) {
List<String> names = Arrays.asList("Don", "King", "Kong");
formatAndPrint(String::join, ", ", names);
}
}

在对象可用地方的实例方法引用
实例方法引用的兼容规则与静态方法引用的规则是一样的。

举例,在 JDK1.8的java.lang.Iterable 接口中有个默认方法 forEach 接受Consumer函数接口。

default void forEach(java.util.function.Consumer<? super T> action)

foreach执行元素的遍历。这个方法已经被List继承。

import java.util.Arrays;
import java.util.List; public class MethodReferenceDemo2 {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana");
// with lambda expression
fruits.forEach((name) -> System.out.println(name)); // with method reference
fruits.forEach(System.out::println);
}
}

MethodReferencDemo2 类有一个水果的list需要打印。可以执行forEach方法传递一个Comsumer函数接口使用Lambda表达式。

fruits.forEach((name) -> System.out.println(name));

另一种方法,因为System.out 是个已经存在的对象,系统已经为你创建好了,你可以使用System.out类的println方法的对象引用。

fruits.forEach(System.out::println);

在没有对象引用的地方使用实例方法的引用

你可以传递一个实例方法的应用作为方法的参数去代替一个函数接口。这种情况下,你不必明确的去创建包含类的实例。实例方法的引用的语法与前两种的引用不同。在前两种引用中,参数的个数必须与期望的函数接口的抽象方法要相同,但当使用没有对象引用的实例方法引用,它要比期望的接口函数的抽象方法少一个参数。因此,当一个函数接口的抽象方法有四个参数,那么没有对象引用的实例方法引用只能有三个参数,它必须兼容抽象方法的后三个参数的类型。此外,抽象方法的第一个参数的类型必须兼容包含实例方法的类。

这类引用的兼容规则如下描述,第一行是一个函数接口的抽象方法的签名,第二行是没有实例引用的方法引用签名:

returnType abstractMethod(type-1, type-2, type-3, type-4)
returnType instanceMethod(type-2, type-3, type-4)

在这,type-1必须兼容包含实例方法的类因为等类被初始化和实例话后要与其他的参数一起传递给抽象方法。

举个例子,就能清楚这个引用的使用了。

import java.util.Arrays;

public class MethodReferenceDemo3 {

    public static void main(String[] args) {
String[] names = { "Alexis", "anna", "Kyleen" }; Arrays.sort(names, String::compareToIgnoreCase);
for (String name : names) {
System.out.println(name);
}
}
}

这个例子展示了如何传递一个实例方法引用给Arrays.sort方法来代替Comparator接口。这个类包含一个String类型的数组,它里面有三个不区分大小写的字符串的元素,如果你只使用包含一个参数的Arrays.sort方法给数组排序,结果是这样的:

Alexis, Kyleen, anna

这不是我们想要的结果,所以我们需要使用有Comparator参数的Arrays.sort方法。

public static <T> void sort(T[] array, Comparator<? super T> c)

你可以使用String.compareIgnoreCase的实例方法的引用去代替Compator接口。下面是String.compareIgnoreCase 的签名:

public int compareToIgnoreCase(String str)

它比Comparator.compare少了一个参数:

int compare(String str1, String str2)

这就是极好的第二种的方法引用。

代码运行结构:

Alexis
anna
Kyleen

构造方法引用

第四种方法引用使用构造方法。他的语法格式如下:

ClassName::new

也许你有个方法实现将一个Integer类型数组转换成Collection,但你需要决定做回返回值的Collection是一个List还是Set,为了这个目的,你可以在下面的例子中创建arrayToCollection方法。

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.function.Supplier; public class MethodReferenceDemo4 { public static Collection<Integer> arrayToCollection(Supplier<Collection<Integer>> supplier, Integer[] numbers) {
Collection<Integer> collection = supplier.get();
for (int i : numbers) {
collection.add(i);
}
return collection;
} public static void main(String[] args) {
Integer[] array = { 1, 8, 5 };
Collection<Integer> col1 = arrayToCollection(ArrayList<Integer>::new, array);
System.out.println("Natural order");
col1.forEach(System.out::println);
System.out.println("=======================");
System.out.println("Ascending order");
Collection<Integer> col2 = arrayToCollection(HashSet<Integer>::new, array);
col2.forEach(System.out::println);
}
}

代替传递Lambda表达式给方法的第一个参数:() -> new ArrayList<Integer>()

你可以简单地传递ArrayList 构造方法的引用:ArrayList<Integer>::new

使用HashSet<Integer>::new 来代替() -> new HashSet<Integer>()

代码执行的结果:

Natural order
1
8
5
=======================
Ascending order
1
5
8