3.方法引用
3.1体验方法引用【理解】
-
方法引用的出现原因
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿参数做操作
那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑呢?答案肯定是没有必要
那我们又是如何使用已经存在的方案的呢?
这就是我们要讲解的方法引用,我们是通过方法引用来使用已经存在的方案
-
代码演示
public interface Printable { void printString(String s); } public class PrintableDemo { public static void main(String[] args) { //在主方法中调用usePrintable方法 // usePrintable((String s) -> { // System.out.println(s); // }); //Lambda简化写法 usePrintable(s -> System.out.println(s)); //方法引用 usePrintable(System.out::println); } private static void usePrintable(Printable p) { p.printString("爱生活爱Java"); } }
面试题 1:什么是方法引用?它和 Lambda 表达式有什么关系?
参考答案:
方法引用是 Java 8 提供的一种新特性,它是对 Lambda 表达式的进一步简化。当 Lambda 中仅仅是调用已有的方法时,方法引用允许我们直接使用“类名::方法名”或“对象::方法名”的形式来代替 Lambda,从而让代码更简洁。
二者的关系可以这样理解:
-
Lambda 表达式是一种匿名函数,用于实现函数式接口;
-
方法引用则是 Lambda 表达式的“快捷方式”,本质上也是实现函数式接口。
比如:
// Lambda
list.forEach(s -> System.out.println(s));
// 方法引用
list.forEach(System.out::println);
方法引用必须满足:Lambda 表达式中调用的方法和引用的方法在参数列表和返回值类型上是完全匹配的。
大厂面试官追问(比如字节跳动):
-
方法引用的四种形式具体都有哪些?
-
你能说一下它们各自的应用场景吗?
场景题 1:项目中 Stream 排序如何使用方法引用实现多字段排序?
题目描述:
你在一个电商系统中处理订单列表,Order
对象包含 price
(价格)和 createTime
(下单时间)两个字段。现在你要用 Stream 实现以下排序:
1️⃣ 先按价格升序排序;
2️⃣ 如果价格相同,再按下单时间降序排序。
要求使用方法引用实现。
参考答案:
首先,方法引用可以用在 Comparator.comparing()
里,用于简化代码。示例实现:
List<Order> orderList = ...;
orderList.stream()
.sorted(Comparator.comparing(Order::getPrice)
.thenComparing(Comparator.comparing(Order::getCreateTime).reversed()))
.forEach(System.out::println);
解读:
-
Order::getPrice
是引用对象的实例方法,获取价格; -
Order::getCreateTime
同样是实例方法,获取下单时间; -
方法引用让代码更清晰,省去 Lambda 表达式的样板代码;
-
thenComparing
链式调用实现二次排序,.reversed()
实现倒序。
3.2方法引用符【理解】
-
方法引用符
:: 该符号为引用运算符,而它所在的表达式被称为方法引用
-
推导与省略
-
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式,它们都将被自动推导
-
如果使用方法引用,也是同样可以根据上下文进行推导
-
方法引用是Lambda的孪生兄弟
-
3.3引用类方法【应用】
引用类方法,其实就是引用类的静态方法
-
格式
类名::静态方法
-
范例
Integer::parseInt
Integer类的方法:public static int parseInt(String s) 将此String转换为int类型数据
-
练习描述
-
定义一个接口(Converter),里面定义一个抽象方法 int convert(String s);
-
定义一个测试类(ConverterDemo),在测试类中提供两个方法
-
一个方法是:useConverter(Converter c)
-
一个方法是主方法,在主方法中调用useConverter方法
-
-
-
代码演示
public interface Converter { int convert(String s); } public class ConverterDemo { public static void main(String[] args) { //Lambda写法 useConverter(s -> Integer.parseInt(s)); //引用类方法 useConverter(Integer::parseInt); } private static void useConverter(Converter c) { int number = c.convert("666"); System.out.println(number); } }
-
使用说明
Lambda表达式被类方法替代的时候,它的形式参数全部传递给静态方法作为参数
面试题 2:方法引用的四种常见类型及其使用场景?
参考答案:
方法引用有四种常见类型:
类型 | 格式 | 适用场景 |
---|---|---|
引用类的静态方法 | 类名::静态方法 | 当 Lambda 中调用的是某个类的静态方法 |
引用对象的实例方法 | 对象::实例方法 | 当 Lambda 中调用的是某个已知对象的实例方法 |
引用类的实例方法 | 类名::实例方法 | 当 Lambda 中通过参数调用实例方法,第一个参数作为调用者 |
引用构造器 | 类名::new | 当 Lambda 中是创建对象时 |
实际例子:
1️⃣ Integer::parseInt
—— 转换字符串为整数;
2️⃣ System.out::println
—— 打印控制台输出;
3️⃣ String::toUpperCase
—— 转换字符串为大写;
4️⃣ Student::new
—— 构造新对象。
面试官提醒点:
比如在 Stream 中,我们经常看到 map(String::toUpperCase)
这样的用法,这就是引用类的实例方法的应用。
场景题 2:JDK 源码中有哪些场景是典型使用了方法引用的?结合源码谈谈它的好处。
参考答案:
方法引用在 JDK 8 及之后的源码中广泛使用,典型场景包括:
1️⃣ Stream API
如 Collectors.toMap
源码中经常看到方法引用:
map.merge(
Objects.requireNonNull(keyMapper.apply(element)),
Objects.requireNonNull(valueMapper.apply(element)),
mergeFunction
);
很多 Lambda 参数传入的地方,外部调用中都是用 Function.identity()
或 Map.Entry::getKey
这种形式。
2️⃣ Comparator 源码
Comparator<Person> byName = Comparator.comparing(Person::getName);
这在 Collections.sort()
和 List.sort()
中非常典型,简化了传统的匿名内部类写法。
好处:
-
减少重复代码:无需重复书写 Lambda 内部逻辑;
-
提高可读性:方法引用表达“做什么”,Lambda 更像是“怎么做”;
-
与函数式接口无缝对接:方法引用与 Predicate、Consumer、Function、Supplier 等天然契合;
-
提高维护性:一旦方法修改,所有引用点自动更新逻辑。
3.4引用对象的实例方法【应用】
引用对象的实例方法,其实就引用类中的成员方法
-
格式
对象::成员方法
-
范例
"HelloWorld"::toUpperCase
String类中的方法:public String toUpperCase() 将此String所有字符转换为大写
-
练习描述
-
定义一个类(PrintString),里面定义一个方法
public void printUpper(String s):把字符串参数变成大写的数据,然后在控制台输出
-
定义一个接口(Printer),里面定义一个抽象方法
void printUpperCase(String s)
-
定义一个测试类(PrinterDemo),在测试类中提供两个方法
-
一个方法是:usePrinter(Printer p)
-
一个方法是主方法,在主方法中调用usePrinter方法
-
-
-
代码演示
public class PrintString { //把字符串参数变成大写的数据,然后在控制台输出 public void printUpper(String s) { String result = s.toUpperCase(); System.out.println(result); } } public interface Printer { void printUpperCase(String s); } public class PrinterDemo { public static void main(String[] args) { //Lambda简化写法 usePrinter(s -> System.out.println(s.toUpperCase())); //引用对象的实例方法 PrintString ps = new PrintString(); usePrinter(ps::printUpper); } private static void usePrinter(Printer p) { p.printUpperCase("HelloWorld"); } }
-
使用说明
Lambda表达式被对象的实例方法替代的时候,它的形式参数全部传递给该方法作为参数
场景题 3:团队项目中你们使用方法引用遇到过哪些“踩坑”问题?如何解决?
参考答案:
🚩 踩坑场景 1:误用对象方法 vs. 类方法
曾有一次我们这样写:
list.stream().map(String::toUpperCase());
结果编译失败,因为多写了 ()
。方法引用是引用方法本身,而不是调用方法,应该写成:
list.stream().map(String::toUpperCase);
✅ 解决:严格区分“引用方法” vs “调用方法”。
🚩 踩坑场景 2:参数不匹配
在实现 BiFunction<String, Integer, String>
时,想引用 substring
:
BiFunction<String, Integer, String> func = String::substring;
发现报错,因为 substring
有两个参数,但我们只传了一个。
✅ 解决:看清接口定义和方法签名,确保参数个数与顺序匹配。
🚩 踩坑场景 3:涉及上下文状态时无法用方法引用
比如一个 Lambda 里既要调用方法,又要做日志处理:
list.stream().map(s -> {
System.out.println("处理:" + s);
return s.toUpperCase();
});
这种 Lambda 不能被方法引用替代,因为它包含了额外逻辑。
✅ 解决:方法引用仅适用于“单纯调用”的场景,否则保留 Lambda。
面试题 3:方法引用的参数匹配规则是什么?有没有什么坑?
参考答案:
参数匹配规则:
-
Lambda 表达式的参数必须能“完美对应”到被引用方法的参数;
-
引用类的实例方法时,第一个参数作为调用者,后续参数作为实际入参;
-
返回值类型必须与函数式接口定义的返回值一致。
易错点/坑:
-
引用类的实例方法这一类最容易出错。例如:
BiFunction<String, Integer, String> func = String::substring;
这里 String
是调用者(第一个参数),Integer
是入参(第二个参数)。
-
如果 Lambda 中有额外逻辑,而不是单纯调用方法,不能用方法引用。
面试官补充问题(如阿里):
-
方法引用能否用于有重载的方法?如果可以,怎么确保匹配对?
3.5引用类的实例方法【应用】
引用类的实例方法,其实就是引用类中的成员方法
-
格式
类名::成员方法
-
范例
String::substring
public String substring(int beginIndex,int endIndex)
从beginIndex开始到endIndex结束,截取字符串。返回一个子串,子串的长度为endIndex-beginIndex
-
练习描述
-
定义一个接口(MyString),里面定义一个抽象方法:
String mySubString(String s,int x,int y);
-
定义一个测试类(MyStringDemo),在测试类中提供两个方法
-
一个方法是:useMyString(MyString my)
-
一个方法是主方法,在主方法中调用useMyString方法
-
-
-
代码演示
public interface MyString { String mySubString(String s,int x,int y); } public class MyStringDemo { public static void main(String[] args) { //Lambda简化写法 useMyString((s,x,y) -> s.substring(x,y)); //引用类的实例方法 useMyString(String::substring); } private static void useMyString(MyString my) { String s = my.mySubString("HelloWorld", 2, 5); System.out.println(s); } }
-
使用说明
Lambda表达式被类的实例方法替代的时候 第一个参数作为调用者 后面的参数全部传递给该方法作为参数
面试题 4:方法引用在源码或框架中有哪些典型应用?
参考答案:
方法引用在 Java 8 Stream API 中非常普遍,比如:
-
list.forEach(System.out::println);
-
list.stream().map(String::toUpperCase).collect(Collectors.toList());
在框架中,比如:
-
Spring Boot:在 Java Config 中常用
MyClass::myMethod
来注册 Bean 定义; -
Junit5:断言中常见
Assertions::assertEquals
; -
MyBatis Lambda Query:
LambdaQueryWrapper<User>::eq
这种链式条件也有类似思路(虽然实现机制更复杂)。
此外,Java 内置的 Comparator 比较器,也大量使用方法引用,例如:
list.sort(Comparator.comparing(String::length));
这既简化了代码,也让函数式编程风格无缝衔接到 Java 世界。
场景题 4:你如何说服团队从传统 Lambda 升级为方法引用?
参考答案:
我在团队内部培训时给出了几个理由:
1️⃣ 代码更简洁:方法引用是 Lambda 的简写版,少写重复逻辑。
2️⃣ 提高可读性:方法引用“指向”已有方法,很清楚表明意图。
3️⃣ 减少错误:手写 Lambda 时容易写错逻辑,方法引用可以复用测试过的方法。
4️⃣ 提升维护性:假设被引用方法改名或重构,IDE 会自动提示修改所有引用点。
5️⃣ 一致性:行业内已经将方法引用作为 Java 8 函数式编程的标准写法,和 JDK 源码保持一致。
最后结合实际项目,展示了 Stream 流中用方法引用替代 Lambda 的前后对比图,让大家看到明显的代码简化效果。
3.6引用构造器【应用】
引用构造器,其实就是引用构造方法
-
l格式
类名::new
-
范例
Student::new
-
练习描述
-
定义一个类(Student),里面有两个成员变量(name,age)
并提供无参构造方法和带参构造方法,以及成员变量对应的get和set方法
-
定义一个接口(StudentBuilder),里面定义一个抽象方法
Student build(String name,int age);
-
定义一个测试类(StudentDemo),在测试类中提供两个方法
-
一个方法是:useStudentBuilder(StudentBuilder s)
-
一个方法是主方法,在主方法中调用useStudentBuilder方法
-
-
-
代码演示
public class Student { private String name; private int age; public Student() { } public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } public interface StudentBuilder { Student build(String name,int age); } public class StudentDemo { public static void main(String[] args) { //Lambda简化写法 useStudentBuilder((name,age) -> new Student(name,age)); //引用构造器 useStudentBuilder(Student::new); } private static void useStudentBuilder(StudentBuilder sb) { Student s = sb.build("林青霞", 30); System.out.println(s.getName() + "," + s.getAge()); } }
-
使用说明
Lambda表达式被构造器替代的时候,它的形式参数全部传递给构造器作为参数
面试题 5:项目中有哪些场景必须用 Lambda 而不能用方法引用?
参考答案:
方法引用虽然简洁,但它只能引用已有的“单一方法”,如果 Lambda 表达式中包含:
-
多步操作(如多行业务逻辑);
-
涉及判断或分支(if/else);
-
参数和返回值与已有方法不匹配;
那么就不能使用方法引用。
举例:
list.stream().filter(s -> {
System.out.println(s);
return s.startsWith("张");
});
这里既有打印又有判断,方法引用就无法胜任,必须用 Lambda。