方法引用 | 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:
- 只是调用一个已经存在的方法;
- Lambda 参数能够对应到目标方法参数;
- 返回值也与函数式接口要求兼容;
- 没有在中间增加额外业务逻辑;
通常就可以考虑改成方法引用。
例如:
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 知识问答
- 什么是方法引用?
- 方法引用和 Lambda 是什么关系?
::的作用是什么?- 方法引用会立即执行方法吗?
- 为什么方法引用仍然需要函数式接口目标类型?
- 静态方法引用的语法是什么?
- 特定对象实例方法引用的语法是什么?
- 特定类型实例方法引用有什么特殊参数映射规则?
- 构造器引用的语法是什么?
- 所有 Lambda 都适合改成方法引用吗?
System.out::println属于哪一种?String::length属于哪一种?Student::new表示什么?
7.2 代码阅读
判断:
Consumer<String> c =
System.out::println;
c.accept("Java");
回答:
System.out::println创建时是否输出?- 真正什么时候执行
println()? - 为什么
println(String)可以适配Consumer<String>?
阅读:
Function<String, Integer> f =
String::length;
System.out.println(
f.apply("Java")
);
回答:
String::length引用的是静态方法还是实例方法?- 谁是
length()的调用对象? - 最终结果是多少?
分析:
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()
);
}
}
完成以下任务:
- 使用 Lambda 按年龄排序。
- 使用
Student::compareByAge按年龄排序。 - 使用 Lambda 打印每个学生。
- 将打印 Lambda 改成方法引用。
- 使用
Comparator.comparingInt(...)+ Lambda 排序。 - 将其中的 Lambda 改成
Student::getAge。
最后说明:
哪些 Lambda 可以直接方法引用化,哪些不能,判断依据是什么?
7.6 本章验收
不查看资料,能够写出:
类名::静态方法
对象名::实例方法
类名::实例方法
类名::new
并分别解释一个实际例子。
同时能够闭卷完成:
name -> System.out.println(name)
到:
System.out::println
以及:
student -> student.getAge()
到:
Student::getAge
的转换,并解释两种方法引用的区别。
如果只会机械把:
->
改成:
::
而不能分析函数式接口参数与目标方法参数的对应关系,则本章还没有真正掌握。