方法引用 | JavaSE

方法引用

一、学习目标

完成本章后,你应该能够:

  • 能够解释方法引用(Method Reference)是什么,以及它与 Lambda 表达式的关系。
  • 能够判断一个 Lambda 是否适合改写为方法引用。
  • 能够掌握 :: 方法引用运算符的基本形式。
  • 能够使用静态方法引用。
  • 能够使用特定对象的实例方法引用。
  • 能够理解特定类型实例方法引用中“第一个参数作为调用者”的特殊规则。
  • 能够使用构造器引用创建对象。
  • 能够在 Comparator、集合遍历等场景中合理选择 Lambda 或方法引用。

二、核心知识

2.1 什么是方法引用

上一章我们已经能够写:

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

观察 Lambda 方法体:

name -> System.out.println(name)

会发现它并没有真正实现新的业务逻辑。

它只是:

接收 name
   ↓
原封不动交给 println
   ↓
调用已有方法

既然真正要执行的行为已经存在于:

System.out.println(...)

中,那么再写一层:

name -> ...

就显得有些重复。

Java 因此允许写成:

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

其中:

System.out::println

就是方法引用(Method Reference)

可以先用一句最通俗的话理解:

当 Lambda 只是“帮忙转调一个已经存在的方法”时,可以考虑直接引用那个方法。


2.2 方法引用不是在调用方法

这一点非常重要。

普通方法调用:

System.out.println("Java");

意味着:

现在立刻执行 println()

而:

System.out::println

不是立即调用。

它表达的是:

println 这个已有行为提供给某个函数式接口使用。

因此:

Consumer<String> consumer = System.out::println;

执行这一行时,并不会输出任何内容。

真正执行:

consumer.accept("Java");

时,才相当于调用:

System.out.println("Java");

所以:

System.out.println("Java")
          ↓
立即调用方法


System.out::println
          ↓
引用这个行为
          ↓
以后通过函数式接口调用

不要把这两个概念混在一起。


2.3 方法引用仍然依赖函数式接口

方法引用和 Lambda 一样,不能完全脱离上下文独立确定最终含义。

例如:

Consumer<String> consumer = System.out::println;

左边的:

Consumer<String>

规定函数方法:

void accept(String value);

而:

System.out::println

能够找到与这个行为规格兼容的方法:

println(String value)

于是可以建立对应关系:

Consumer<String>

void accept(String value)
          ↓
System.out::println
          ↓
void println(String value)

也就是说:

方法引用的参数、返回值必须能够与目标函数式接口的函数方法匹配。

这与 Lambda 的“目标类型”思想完全一致。


2.4 方法引用与 Lambda 的关系

例如:

Consumer<String> c1 =
        value -> System.out.println(value);

可以改写:

Consumer<String> c2 =
        System.out::println;

它们表达的核心行为一致:

收到 String
   ↓
调用 println(String)

再例如:

Comparator<Student> comparator =
        (s1, s2) -> Student.compareByAge(s1, s2);

如果存在:

public static int compareByAge(
        Student s1,
        Student s2
) {
    return Integer.compare(
            s1.getAge(),
            s2.getAge()
    );
}

那么可以写:

Comparator<Student> comparator =
        Student::compareByAge;

所以可以把方法引用理解成:

某些 Lambda 表达式的进一步简写。

但注意关键词是:

某些。

不是所有 Lambda 都可以变成方法引用。


2.5 核心判断原则

假设有:

x -> existingMethod(x)

如果 Lambda:

  1. 只是调用一个已经存在的方法;
  2. Lambda 参数能够对应到目标方法参数;
  3. 返回值也与函数式接口要求兼容;
  4. 没有在中间增加额外业务逻辑;

通常就可以考虑改成方法引用。

例如:

name -> System.out.println(name)

适合:

System.out::println

但:

name -> System.out.println("姓名:" + name)

就不能直接简单改成:

System.out::println

因为 Lambda 中增加了:

"姓名:" + name

这一层额外处理。

方法引用不是:

“看到方法调用就把 . 换成 ::”。

而是需要满足行为签名与参数传递关系。


三、使用方法

Java 学习中最重要的四类方法引用,可以整理为:

| 类型 | 基本语法 | | -------------------- | -------------------- | | 静态方法引用 | 类名::静态方法名 | | 特定对象实例方法引用 | 对象名::实例方法名 | | 特定类型实例方法引用 | 类名::实例方法名 | | 构造器引用 | 类名::new |

下面逐个学习。


3.1 静态方法引用

语法:

类名::静态方法名

假设:

public class Student {

    private String name;
    private int age;

    // getter、setter、构造器省略

    public static int compareByAge(
            Student s1,
            Student s2
    ) {
        return Integer.compare(
                s1.getAge(),
                s2.getAge()
        );
    }
}

Lambda:

Arrays.sort(
        students,
        (s1, s2) ->
                Student.compareByAge(s1, s2)
);

这里 Lambda:

(s1, s2) ->
        Student.compareByAge(s1, s2)

只是把两个参数交给:

Student.compareByAge(...)

因此可以写:

Arrays.sort(
        students,
        Student::compareByAge
);

参数对应关系:

Comparator<Student>

compare(Student s1, Student s2)
        ↓              ↓
Student.compareByAge(s1, s2)

所以:

Student::compareByAge

能够作为 Comparator<Student> 的实现。


3.2 静态方法引用完整案例

import java.util.Arrays;

public class MethodReferenceDemo {

    public static void main(String[] args) {

        Student[] students = {
                new Student("张三", 21),
                new Student("李四", 18),
                new Student("王五", 23)
        };

        Arrays.sort(
                students,
                Student::compareByAge
        );

        System.out.println(
                Arrays.toString(students)
        );
    }
}

class Student {

    private String name;
    private int age;

    public Student(
            String name,
            int age
    ) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public static int compareByAge(
            Student s1,
            Student s2
    ) {
        return Integer.compare(
                s1.getAge(),
                s2.getAge()
        );
    }

    @Override
    public String toString() {
        return name + ":" + age;
    }
}

这里真正需要记住的不是:

Student::compareByAge

这个字符串。

而是理解:

Lambda 原来在做什么?

(s1, s2)
   ↓
Student.compareByAge(s1, s2)

既然参数只是原样交给已有静态方法
   ↓
可以引用已有静态方法

3.3 特定对象的实例方法引用

第二种:

对象名::实例方法名

最经典例子:

System.out::println

其中:

System.out

是已经存在的对象。

println() 是这个对象的实例方法。

Lambda:

value -> System.out.println(value)

可以改成:

System.out::println

完整示例:

import java.util.List;

public class Demo {

    public static void main(String[] args) {

        List<String> names =
                List.of(
                        "Java",
                        "MySQL",
                        "Spring"
                );

        names.forEach(
                System.out::println
        );
    }
}

这里:

forEach(...)

需要的是一个:

Consumer<String>

而:

System.out.println(String)

正好可以完成:

String
  ↓
打印
  ↓
void

因此可以匹配。


3.4 自定义对象实例方法引用

例如:

class Printer {

    public void print(String text) {
        System.out.println(
                "内容:" + text
        );
    }
}

创建对象:

Printer printer = new Printer();

Lambda:

Consumer<String> consumer =
        text -> printer.print(text);

可以改成:

Consumer<String> consumer =
        printer::print;

调用:

consumer.accept("Java");

执行的就是:

printer.print("Java");

结构:

已有对象 printer
      ↓
已有实例方法 print
      ↓
printer::print

3.5 特定类型的实例方法引用

第三种看起来与前一种非常像:

类名::实例方法名

例如:

String::toUpperCase

注意:

toUpperCase() 明明是实例方法。

为什么前面却写:

String::

而不是某个具体字符串对象?

因为这里使用的是一种特殊的参数映射规则。

例如:

Function<String, String> function =
        text -> text.toUpperCase();

Lambda 的参数:

text

既是数据,又是调用实例方法的对象:

text.toUpperCase()

因此可以改成:

Function<String, String> function =
        String::toUpperCase;

它的含义不是:

调用 String 类的静态方法 toUpperCase()

而是:

将传入的第一个 String 对象作为实例方法调用者。

可以近似理解:

text -> text.toUpperCase()

          ↓

String::toUpperCase

3.6 两参数情况更能说明这个规则

例如:

BiPredicate<String, String> predicate =
        (s1, s2) -> s1.equals(s2);

这里:

第一个参数 s1
     ↓
成为实例方法调用者

第二个参数 s2
     ↓
成为 equals 的参数

即:

s1.equals(s2)

因此可以写成:

BiPredicate<String, String> predicate =
        String::equals;

对应关系:

BiPredicate<String, String>

test(s1, s2)
      │    │
      │    └────────→ equals(s2)
      │
      └─────────────→ s1.equals(...)

所以:

String::equals

可以理解成:

(s1, s2) -> s1.equals(s2)

这是方法引用中最容易混淆的一种。


3.7 特定对象 vs 特定类型

对比:

特定对象

System.out::println

已经明确知道调用者:

System.out

对应:

value ->
        System.out.println(value)

特定类型

String::toUpperCase

调用者需要从 Lambda 参数中获得:

text ->
        text.toUpperCase()

因此可以记忆:

对象::实例方法
        ↓
对象已经提前确定


类名::实例方法
        ↓
调用对象从 Lambda 参数中获得

3.8 构造器引用

如果 Lambda 的目的只是:

根据参数创建一个对象。

也可以进一步简化。

例如:

Supplier<Student> supplier =
        () -> new Student();

如果存在无参构造器,可以写:

Supplier<Student> supplier =
        Student::new;

调用:

Student student =
        supplier.get();

3.9 有参数构造器引用

假设:

class Student {

    private String name;

    public Student(String name) {
        this.name = name;
    }
}

Lambda:

Function<String, Student> function =
        name -> new Student(name);

可以改成:

Function<String, Student> function =
        Student::new;

调用:

Student student =
        function.apply("张三");

函数式接口决定:

输入:String
输出:Student

编译器就会寻找能够匹配的构造器:

Student(String name)

因此:

Student::new

并不是固定表示某一个构造器。

具体选择哪个重载构造器,与目标函数式接口参数列表有关。


四、原理与进阶

4.1 方法引用仍然是目标类型表达式

例如:

String::length

单独看它并不能完全说明:

最终到底要适配哪个函数式接口。

例如:

Function<String, Integer> function =
        String::length;

因为:

Function<String, Integer>

规定:

String → Integer

而:

String.length()

恰好:

String 对象
    ↓
length()
    ↓
int

所以能够建立匹配。

与 Lambda 一样:

方法引用的最终意义依赖目标函数式接口。


4.2 方法引用不会复制方法代码

例如:

Student::compareByAge

并不是 Java 把:

compareByAge(...)

方法体复制到当前位置。

它只是建立:

函数式接口行为与已有方法之间的适配关系。

因此从设计角度看:

已有业务方法
     ↓
可以复用
     ↓
方法引用直接指向它

这也是代码复用的一种体现。


4.3 方法重载与方法引用

假设:

System.out.println(int value)
System.out.println(String value)
System.out.println(double value)
...

println 本身存在大量重载。

但是:

Consumer<String> consumer =
        System.out::println;

目标函数式接口已经规定:

void accept(String value)

因此编译器会根据目标类型寻找兼容的重载。

这再次说明:

不要脱离函数式接口去理解方法引用。


五、实践应用

5.1 集合遍历

Lambda:

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

方法引用:

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

第二种通常更直接。


5.2 Comparator

上一章:

Comparator<Student> comparator =
        (s1, s2) ->
                Integer.compare(
                        s1.getAge(),
                        s2.getAge()
                );

如果我们已经封装:

public static int compareByAge(
        Student s1,
        Student s2
) {
    return Integer.compare(
            s1.getAge(),
            s2.getAge()
    );
}

可以:

Comparator<Student> comparator =
        Student::compareByAge;

5.3 comparingInt 与方法引用

上一章见过:

Comparator<Student> comparator =
        Comparator.comparingInt(
                student -> student.getAge()
        );

这里:

student -> student.getAge()

只是调用:

Student.getAge()

因此可以继续:

Comparator<Student> comparator =
        Comparator.comparingInt(
                Student::getAge
        );

这是一种非常典型的工程写法:

Lambda
student -> student.getAge()

        ↓

方法引用
Student::getAge

5.4 创建对象

Lambda:

Function<String, Student> creator =
        name -> new Student(name);

方法引用:

Function<String, Student> creator =
        Student::new;

当“创建对象”本身就是你要传递的行为时,构造器引用会非常简洁。


六、常见问题

6.1 方法引用一定比 Lambda 好吗?

不一定。

例如:

student ->
        student.getAge() >= 18
        && student.getScore() >= 60

这是一个真实的组合业务条件。

强行寻找方法引用反而没有意义。

如果已经存在:

Student::isQualified

并且语义完全一致,再考虑方法引用。

原则应该是:

可读性优先,而不是 :: 越多越高级。


6.2 所有 Lambda 都能改成方法引用吗?

不能。

例如:

x -> x * x

这里进行了:

x * x

计算,没有直接调用现成方法。

不能凭空写:

???::???

除非已经存在一个语义完全对应的方法。


6.3 ClassName::method 一定引用静态方法吗?

不是。

可能是:

Student::compareByAge

引用静态方法。

也可能是:

String::length

引用实例方法。

判断关键不是左边是不是类名。

而要分析:

被引用的方法本身是静态方法还是实例方法,以及参数如何与函数式接口匹配。


6.4 System.out::println 为什么不是静态方法引用?

因为:

System.out

是对象。

println() 是该对象的实例方法。

所以属于:

特定对象::实例方法

6.5 String::equals 中谁调用 equals?

函数式接口传入的第一个参数。

例如:

(s1, s2) -> s1.equals(s2)

可以写:

String::equals

因此:

s1 → 调用者
s2 → equals 参数

6.6 方法引用会立即执行吗?

不会。

Consumer<String> consumer =
        System.out::println;

这里只是建立行为对象。

调用:

consumer.accept("Java");

时才执行。


七、练习与验收

7.1 知识问答

  1. 什么是方法引用?
  2. 方法引用和 Lambda 是什么关系?
  3. :: 的作用是什么?
  4. 方法引用会立即执行方法吗?
  5. 为什么方法引用仍然需要函数式接口目标类型?
  6. 静态方法引用的语法是什么?
  7. 特定对象实例方法引用的语法是什么?
  8. 特定类型实例方法引用有什么特殊参数映射规则?
  9. 构造器引用的语法是什么?
  10. 所有 Lambda 都适合改成方法引用吗?
  11. System.out::println 属于哪一种?
  12. String::length 属于哪一种?
  13. Student::new 表示什么?

7.2 代码阅读

判断:

Consumer<String> c =
        System.out::println;

c.accept("Java");

回答:

  1. System.out::println 创建时是否输出?
  2. 真正什么时候执行 println()
  3. 为什么 println(String) 可以适配 Consumer<String>

阅读:

Function<String, Integer> f =
        String::length;

System.out.println(
        f.apply("Java")
);

回答:

  1. String::length 引用的是静态方法还是实例方法?
  2. 谁是 length() 的调用对象?
  3. 最终结果是多少?

分析:

BiPredicate<String, String> p =
        String::equals;

将其还原成完整 Lambda。

7.3 手写代码

将下面代码逐个改成方法引用。

题一

Consumer<String> consumer =
        text -> System.out.println(text);

题二

Function<String, Integer> function =
        text -> text.length();

题三

Function<String, String> function =
        text -> text.toUpperCase();

题四

Supplier<Student> supplier =
        () -> new Student();

题五

Function<String, Student> function =
        name -> new Student(name);

7.4 Debug

下面是否能够改成:

System.out::println

name ->
        System.out.println(
                "姓名:" + name
        )

说明原因。


判断下面方法引用表示什么:

String::equals

为什么不能简单说成:

“调用 String 类的 equals 静态方法”?


下面代码:

Function<String, Student> creator =
        Student::new;

如果 Student 没有:

Student(String name)

这样的兼容构造器,会发生什么?

7.5 综合训练

定义:

class Student {

    private String name;
    private int age;

    // 构造器、getter 等

    public static int compareByAge(
            Student a,
            Student b
    ) {
        return Integer.compare(
                a.getAge(),
                b.getAge()
        );
    }
}

完成以下任务:

  1. 使用 Lambda 按年龄排序。
  2. 使用 Student::compareByAge 按年龄排序。
  3. 使用 Lambda 打印每个学生。
  4. 将打印 Lambda 改成方法引用。
  5. 使用 Comparator.comparingInt(...) + Lambda 排序。
  6. 将其中的 Lambda 改成 Student::getAge

最后说明:

哪些 Lambda 可以直接方法引用化,哪些不能,判断依据是什么?

7.6 本章验收

不查看资料,能够写出:

类名::静态方法

对象名::实例方法

类名::实例方法

类名::new

并分别解释一个实际例子。

同时能够闭卷完成:

name -> System.out.println(name)

到:

System.out::println

以及:

student -> student.getAge()

到:

Student::getAge

的转换,并解释两种方法引用的区别。

如果只会机械把:

->

改成:

::

而不能分析函数式接口参数与目标方法参数的对应关系,则本章还没有真正掌握。