Java 8 之方法引用 - Method References
什么是方法引用
简单地说,就是一个 Lambda 表达式。在 Java 8 中,我们会使用 Lambda 表达式创建匿名方法,但是有时候,我们的 Lambda 表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8 的方法引用允许我们这样做。方法引用是一个更加紧凑,易读的 Lambda 表达式,注意方法引用是一个 Lambda 表达式,其中方法引用的操作符是双冒号 "::"。
方法引用例子
首先定义一个 Person 类,如下:
public class Person {
String name;
LocalDate birthday;
public Person(String name, LocalDate birthday) {
this.name = name;
this.birthday = birthday;
}
public LocalDate getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
@Override
public String toString() {
return this.name;
}
}
假设我们有一个 Person 数组,并且想对它进行排序,这时候,我们可能会这样写:
原始写法
public class Main {
static class PersonAgeComparator implements Comparator<Person> {
public int compare(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
public static void main(String[] args) {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016, 9, 1)),
new Person("001", LocalDate.of(2016, 2, 1)),
new Person("002", LocalDate.of(2016, 3, 1)),
new Person("004", LocalDate.of(2016, 12, 1))};
Arrays.sort(pArr, new PersonAgeComparator());
System.out.println(Arrays.asList(pArr));
}
}
其中,Arrays类的sort方法定义如下:
public static <T> void sort(T[] a, Comparator<? super T> c)
Comparator
接口是一个函数式接口,因此可以使用 Lambda 表达式,而不需要定义一个实现Comparator
接口的类,并创建它的实例对象,传给 sort 方法。
使用 Lambda 表达式,我们可以这样写:
改进一,使用 Lambda 表达式,未调用已存在的方法
public class Main {
public static void main(String[] args) {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016, 9, 1)),
new Person("001", LocalDate.of(2016, 2, 1)),
new Person("002", LocalDate.of(2016, 3, 1)),
new Person("004", LocalDate.of(2016, 12, 1))};
Arrays.sort(pArr, (Person a, Person b) -> {
return a.getBirthday().compareTo(b.getBirthday());
});
System.out.println(Arrays.asList(pArr));
}
}
然而,在以上代码中,关于两个人生日的比较方法在 Person 类中已经定义了,因此,我们可以直接使用已存在的 Person.compareByAge 方法。
改进二,使用 Lambda 表达式,调用已存在的方法
public class Main {
public static void main(String[] args) {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016, 9, 1)),
new Person("001", LocalDate.of(2016, 2, 1)),
new Person("002", LocalDate.of(2016, 3, 1)),
new Person("004", LocalDate.of(2016, 12, 1))};
Arrays.sort(pArr, (a, b) -> Person.compareByAge(a, b));
System.out.println(Arrays.asList(pArr));
}
}
因为这个 Lambda 表达式调用了一个已存在的方法,因此,我们可以直接使用方法引用来替代这个 Lambda 表达式
改进三,使用方法引用
public class Main {
public static void main(String[] args) {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016, 9, 1)),
new Person("001", LocalDate.of(2016, 2, 1)),
new Person("002", LocalDate.of(2016, 3, 1)),
new Person("004", LocalDate.of(2016, 12, 1))};
Arrays.sort(pArr, Person::compareByAge);
System.out.println(Arrays.asList(pArr));
}
}
在以上代码中,方法引用 Person::compareByAge 在语义上与 Lambda 表达式 (a, b) -> Person.compareByAge(a, b) 是等同的
四种方法引用类型
静态方法引用
ContainingClass::staticMethodName
比较容易理解,和静态方法调用相比,只是把 . 换为 ::
例子:
- String::valueOf,等价于 Lambda:s -> String.valueOf(s)
- Math::pow 等价于lambda表达式 (x, y) -> Math.pow(x, y);
- 前面举的例子 Person::compareByAge 就是一个静态方法引用
- 从一个数字列表中找出最大的一个数字,方法引用方式:
Function<List<Integer>, Integer> maxFn = Collections::max;
// 等价于 Lambda 表达式:
// Function<List<Integer>, Integer> maxFn = (numbers) -> Collections.max(numbers);
maxFn.apply(Arrays.asList(1, 10, 3, 5))。
- 字符串反转
// 函数式接口
interface StringFunc {
String func(String n);
}
class MyStringOps {
// 静态方法:反转字符串
public static String strReverse(String str) {
String result = "";
for (int i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
}
}
class MethodRefDemo {
public static String stringOp(StringFunc sf, String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr = "lambda add power to Java";
// MyStringOps::strReverse 相当于实现了接口方法 func() ,并在接口方法 func() 中作了 MyStringOps.strReverse() 操作
String outStr = stringOp(MyStringOps::strReverse, inStr);
System.out.println("Original string: " + inStr);
System.out.println("String reserved: " + outStr);
}
}
引用特定对象的实例方法
实例上的实例方法引用
instanceReference::instanceMethodName
例子:x::toString,对应的 Lambda:() -> this.toString()
与引用静态方法相比,都换为实例对象而已
如下示例,引用的方法是 myComparisonProvider 对象的 compareByName 方法:
class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
超类上的实例方法引用
super::methodName
通过使用 super,可以引用方法的超类版本。除此以外,还可以捕获 this 指针
- this::equals 等价于 Lambda 表达式 x -> this.equals(x)
引用特定类型的任意对象的实例方法 (较少用)
ClassName::methodName
若类型的实例方法是泛型的,就需要在::分隔符前提供类型参数,或者(多数情况下)利用目标类型推导出其类型。
静态方法引用和引用特定类型的任意对象的实例方法拥有一样的语法。编译器会根据实际情况做出决定。
一般我们不需要指定方法引用中的参数类型,因为编译器往往可以推导出结果,但如果需要我们也可以显式在::分隔符之前提供参数类型信息。
例子:
- String::toString,对应的 Lambda:(s) -> s.toString()
这里不太容易理解,实例方法要通过对象来调用,方法引用对应 Lambda,Lambda 的第一个参数会成为调用实例方法的对象。 - 字符串数组中任意一个对象的 compareToIgnoreCase 方法:
String[] stringArray = { "Barbara", "James", "Mary" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
- 在泛型类或泛型方法中,也可以使用方法引用
interface MyFunc<T> {
int func(T[] als, T v);
}
class MyArrayOps {
public static <T> int countMatching(T[] vals, T v) {
int count = 0;
for (int i = 0; i < vals.length; i++) {
if (vals[i] == v) count++;
}
return count;
}
}
class GenericMethodRefDemo {
public static <T> int myOp(MyFunc<T> f, T[] vals, T v) {
return f.func(vals, v);
}
public static void main(String[] args){
Integer[] vals = {1, 2, 3, 4, 2, 3, 4, 4, 5};
String[] strs = {"One", "Two", "Three", "Two"};
int count;
count = myOp(MyArrayOps::<Integer>countMatching, vals, 4);
System.out.println("vals contains " + count + " 4s");
count = myOp(MyArrayOps::<String>countMatching, strs, "Two");
System.out.println("strs contains " + count + " Twos");
}
}
当把泛型方法指定为方法引用时,类型参数出现在 :: 之后、方法名之前。在这种情况下,并非必须显示指定类型参数,因为类型参数会被自动推断得出。对于指定泛型类的情况,类型参数位于类名的后面::的前面。
构造方法引用
构造方法引用又分构造方法引用和数组构造方法引用
构造方法引用 (也可以称作构造器引用)
ClassName::new
构造函数本质上是静态方法,只是方法名字比较特殊,使用前提是该类必须有无参构造函数
例子:
- String::new,对应的 Lambda:() -> new String()
- Supplier
class PersonFactory {
private Supplier<Person> supplier;
public PersonFactory(Supplier<Person> supplier) {
this.supplier = supplier;
}
public Person getPerson() {
return supplier.get();
}
}
PersonFactory factory = new PersonFactory(Person::new);
Person p1 = factory.getPerson();
- Stream
List<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
Stream<Button> stream = strings.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());
数组构造方法引用
TypeName[]::new
int[]::new 是一个含有一个参数的构造器引用,这个参数就是数组的长度。等价于 lambda 表达式 x -> new int[x]
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 创建数组 int[10]
引用特定对象的实例方法 与 引用特定类型的任意对象的实例方法 的区别
class Person {
private String name; // 省略 getter、setter
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
public int compareTo(Person p) {
return this.getName().compareTo(p.getName());
}
}
// 用特定对象的实例方法
Arrays.sort(persons, p1::compare);
// 引用特定类型的任意对象的实例方法
Arrays.sort(persons, Person::compareTo);
// 相当于 (p1, p2) -> p1.compareTo(p2)
什么场景适合使用方法引用
当一个 Lambda 表达式调用了一个已存在的方法
什么场景不适合使用方法引用
需要往引用的方法传参数的时候不适合:
IsReferable demo = () -> ReferenceDemo.commonMethod("Argument in method.");