Java中的函数式编程(四)方法引用method reference

641 阅读3分钟

写在前面

我们已经知道,lambda表达式是一个匿名函数,可以用lambda表达式来实现一个函数式接口。

很自然的,我们会想到类的方法也是函数,本质上和lambda表达式是一样的,那是否也可以用类的方法来实现一个函数式接口呢?答案是可以的。我们称之为方法引用(method reference)。

本文的示例代码可从gitee上获取:gitee.com/cnmemset/ja…

全部专栏文章获取,可关注公众号【员说】

方法引用

一个典型例子,向一个Map中写入单词以及它的长度:

public static void simpleMethodReference() {
    Map wordMap = new HashMap<>();

    // 等同于 wordMap.computeIfAbsent("hello", s -> s.length());
    wordMap.computeIfAbsent("hello", String::length);

    // 输出为 {hello=5}
    System.out.println(wordMap);
}

上述代码中,String::length 就是方法引用,它用 :: 来分割类名或对象与方法名,:: 左侧是类名或对象,:: 右侧是方法名。

一般来说,方法引用有4种情况:

  1. object::instanceMethod —— 对象 + 实例方法
  2. Class::staticMethod —— 类名 + 静态方法
  3. Class::instanceMethod —— 类名 + 实例方法
  4. Class::new —— 类名 + 关键字 new ,这种情况又称为构造器引用(constructor reference)

1. object::instanceMethod

object::instanceMethod,:: 左侧是一个对象,:: 右侧是实例方法名。

它等价于提供了 instanceMethod 方法的参数列表的 lambda表达式。

例如对于字符串 str (String str = ""):

str::compareTo 等价于  s -> str.compareTo(s)

示例代码如下:

public static void objectInstanceMethodReference() {
    String me = "me";

    // wordMap 的 key 是给定的单词,value是不区分大小写,与单词 "me" 比较后得出的值
    Map wordMap = new HashMap<>();

    // me::compareToIgnoreCase 等价于 s ->  me.compareToIgnoreCase(s)
    wordMap.computeIfAbsent("him", me::compareToIgnoreCase);
    wordMap.computeIfAbsent("you", s ->  me.compareToIgnoreCase(s));

    System.out.println(wordMap);
}

上述代码的输出是:

{him=5, you=-12}

2. Class::staticMethod

Class::staticMethod,:: 左侧是一个类,:: 右侧是静态方法名。

它等价于提供了staticMethod方法的参数列表的lambda表达式。

例如:

System.out::println 等价于 x -> System.out.print(x)

示例代码:

public static void classStaticMethodReference() {
    List list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu");

    // System.out::println 等价于 s -> System.out.println(s)
    list.forEach(System.out::println);
}

上述代码输出为:

Guangdong
Zhejiang
Jiangsu

3. Class::instanceMethod

对于Class::instanceMethod,:: 左侧是一个类,:: 右侧是实例方法名。

假设 instanceMethod 的参数列表是 (x, y),那么Class::instanceMethod 等价于lambda表达式  (obj, x, y) -> obj.instanceMethod(x, y),其中 obj 是 Class 的对象实例。

例如:

String::length 等价于 s -> s.length()

String::compareToIgnoreCase 等价于 (s1, s2) -> s1.compareToIgnoreCase(s2)

示例代码:

public static void classInstanceMethodReference() {
    Map wordMap = new HashMap<>();
    Integer wordLen = wordMap.computeIfAbsent("hello", String::length);

    System.out.println(wordMap);
}

上述代码输出为:

{hello=5}

4. Class::new

对于Class::new,new的含义是指Class的构造函数,所以又称为构造器引用(constructor reference)。

假设Class的构造函数有两个,它们的参数列表分别是(x)和(x, y),那么 Class::new 可能等价于 x -> new Class(x),也有可能等价于 (x, y) -> new Class(x, y),具体是哪个,编译器会在编译阶段通过上下文推断出来。

例如:

BigDecimal::new ,根据上下文,可能等价于 (String s) -> new BigDecimal(s)

特别的,数组类型也可以使用构造器引用。数组类型只有一个构造参数,表示数组的长度:

String[]::new 等价于 x -> new String[x]

示例代码:

public static void ctorMethodReference() {
    List list = Arrays.asList("1.1", "2.2", "3.3");

    // BigDecimal::new 根据上下文推断,等价于 s -> new BigDecimal(s)
    Stream stream = list.stream().map(BigDecimal::new);

    List decimalList = stream.collect(Collectors.toList());

    System.out.println(decimalList);

    // 构建一个新的 Stream ,之前的 Stream 已经被关闭了
    Stream stream1 = list.stream().map(BigDecimal::new);

    // BigDecimal[]::new ,数组的构造器引用,等价于 x -> new BigDecimal[x]
    BigDecimal[] decimalArray = stream1.toArray(BigDecimal[]::new);

    for (BigDecimal d : decimalArray) {
        System.out.println(d);
    }
}

上述代码的输出为:

[1.1, 2.2, 3.3]
1.1
2.2
3.3

5. this::instanceMethod和super::instanceMethod

对于this::instanceMethod,很容易理解,相当于把this关键字看做是当前类的实例对象即可。

例如:

this::equals 等价于 x -> this.equals(x)

对于super::instanceMethod,会相对复杂一些,相当于在this对象上,调用的指定方法父类版本。

示例代码:

public class SuperMethodReferenceExample {
    public static void main(String[] args) {
        ThreadWaiter waiter = new ThreadWaiter();
        waiter.run();
    }

    public static class Waiter {
        public void sayHi() {
            System.out.println("Hello, man!");
        }
    }

    public static class ThreadWaiter extends Waiter {
        @Override
        public void sayHi() {
            System.out.println("Hello, thread!");
        }

        public void run() {
            // 指定调用父类 Waiter 的 sayHi 方法
            Thread t = new Thread(super::sayHi);
            t.start();
        }
    }
}

上述代码的输出为:

Hello, man!

结语

方法引用可以视为lambda表达式的一个语法糖。