方法引用是 Lambda 表达式的"语法糖"。理解了它,你写的 Stream API 代码会立刻变得非常专业和简洁。
什么是方法引用?
核心思想 :当你写的 Lambda 表达式的唯一 工作就是去调用一个已经存在 的方法时,你就可以使用"方法引用"(::)来代替这个 Lambda 表达式。
为什么需要它?
我们来看一个简单的 forEach 例子:
-
Lambda 写法
javaList<String> list = Arrays.asList("a", "b", "c"); list.stream().forEach(s -> System.out.println(s)); -
分析 :这个 Lambda
s -> System.out.println(s)接收一个参数s,然后原封不动 地把它传给System.out.println方法。s这个变量显得有些多余,我们真正的意图是"对于流中的每个元素,都去执行System.out.println这个操作"。 -
方法引用写法:
list.stream().forEach(System.out::println); -
解读 :
System.out::println这段代码的意思就是:"请使用System.out这个实例的println方法 "。编译器会自动推断,流中的每个元素(s)都应该被当作参数传递给println方法。
你看,代码是不是立刻就干净了?:: 运算符用于分隔类名/对象名 和方法名。
方法引用的四种主要类型
1. 静态方法引用 (Static Method Reference)
-
语法 :
ClassName::staticMethod -
Lambda 对比 :
(args) -> ClassName.staticMethod(args) -
场景 :你调用的方法是一个静态方法。
示例 :把一个 String 列表转换为 Integer 列表。
java
List<String> strNums = Arrays.asList("1", "2", "3");
// Lambda 写法
List<Integer> nums1 = strNums.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
// 方法引用写法
List<Integer> nums2 = strNums.stream()
.map(Integer::parseInt) // parseInt 是 Integer 类的静态方法
.collect(Collectors.toList());
-
解读 :
map操作需要一个Function<String, Integer>。Lambdas -> Integer.parseInt(s)完美符合。 -
Integer::parseInt告诉map:"请用Integer类的parseInt静态方法来处理流中的每个元素(s)"。
2. 特定对象的实例方法引用 (Instance Method Reference of a Particular Object)
-
语法 :
objectInstance::instanceMethod -
Lambda 对比 :
(args) -> objectInstance.instanceMethod(args) -
场景 :你调用的方法是一个已经存在的、特定的实例对象的方法。
示例:就是我们最开始的打印例子。
java
List<String> list = Arrays.asList("a", "b", "c");
// Lambda 写法
list.stream().forEach(s -> System.out.println(s));
// 方法引用写法
// System.out 是一个已经存在的实例对象 (PrintStream 类的实例)
list.stream().forEach(System.out::println);
- 解读 :
System.out是一个具体的对象实例 。forEach需要一个Consumer<String>。Lambdas -> System.out.println(s)告诉它去调用System.out对象的println方法。System.out::println是更直接的表达。
3. 特定类型的任意对象的实例方法引用 (Instance Method Reference of an Arbitrary Object of a Particular Type)
-
(这是最常用,也是最容易混淆的一种,请重点理解)
-
语法 :
ClassName::instanceMethod -
Lambda 对比 :
(obj, args) -> obj.instanceMethod(args)或者(obj) -> obj.instanceMethod() -
场景 :当 Lambda 的第一个参数 成为了方法调用者时,就可以使用这种形式。
示例 1 :获取所有 User 对象的名字。
java
class User {
private String name;
public String getName() { return this.name; }
// ...
}
List<User> users = ... ;
// Lambda 写法
List<String> names1 = users.stream()
.map(user -> user.getName())
.collect(Collectors.toList());
// 方法引用写法
List<String> names2 = users.stream()
.map(User::getName) // getName 是 User 类的实例方法
.collect(Collectors.toList());
-
解读:
-
map需要一个Function<User, String>,它的抽象方法是String apply(User user)。 -
我们的 Lambda 是
user -> user.getName()。 -
注意看:Lambda 的第一个(也是唯一一个)参数
user,变成了getName()方法的调用者。 -
编译器看到这个模式 (
user -> user.someMethod()),就允许你简写为User::getName。它的意思是:"对于流中的任意一个User对象 ,请调用它自己 的getName方法"。
-
示例 2:将字符串列表转为大写。
java
List<String> list = Arrays.asList("a", "b", "c");
// Lambda 写法
List<String> upperList1 = list.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
// 方法引用写法
List<String> upperList2 = list.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
- 解读 :同理,
s -> s.toUpperCase()中,参数s成为了toUpperCase()的调用者。因此简写为String::toUpperCase。
4. 构造函数引用 (Constructor Reference)
-
语法 :
ClassName::new -
Lambda 对比 :
(args) -> new ClassName(args) -
场景 :当你需要"生产"一个新的对象时(常用于
Supplier接口或 Stream 的collect)。
示例 :把一个名字列表转换成 User 对象列表。
java
List<String> names = Arrays.asList("Alice", "Bob");
// Lambda 写法
List<User> users1 = names.stream()
.map(name -> new User(name)) // 假设 User 有一个 User(String name) 构造函数
.collect(Collectors.toList());
// 方法引用写法
List<User> users2 = names.stream()
.map(User::new) // 编译器会自动匹配合适的构造函数
.collect(Collectors.toList());
- 解读 :
map需要一个Function<String, User>。Lambdaname -> new User(name)刚好符合。编译器看到::new,就会自动去User类里寻找一个接收String(即流中元素name的类型)的构造函数。
核心规则 :当你的 Lambda 表达式包含了额外的逻辑时,就不能用方法引用。、
Stream 的用法"三步曲"
使用 Stream 几乎总遵循这三个步骤,像个"公式":
-
创建流 :把原材料(
List)放到流水线上。 -
中间操作 :在流水线上对原材料进行加工(可以有多道工序)。
-
终止操作 :把加工好的成品打包 或消费(启动流水线)。
我们用一个 User 列表来举例:
java
// 原材料仓库
List<User> users = ... ; // 假设里面有很多 User 对象
// 目标:找到所有 30 岁以上的用户,获取他们的名字,并返回一个新列表
1. 创建流 (Get the Stream)
这是第一步:告诉 Stream 你的数据源是什么。最常用的就是 list.stream()。
users.stream() // 从这里开始,users 列表中的数据就被放到了流水线上
注意,这里的流不是输入输出流
对比维度 I/O Stream Java 8 Stream (我们刚讲的) 中文比喻 数据的管道🚰 数据的流水线🏭 所属包 java.iojava.util.stream处理对象 外部数据(字节或字符) 内存数据(Java 对象) 来源/去向 文件、网络套接字(Socket)、字节数组 List,Set,Map, 数组核心目的 I/O 读写(把数据读进来/写出去) 计算和转换(筛选、排序、分组) 核心操作 read(),write(),close()filter(),map(),collect()举例 FileInputStream,BufferedReaderlist.stream()
2. 中间操作 (Process the Stream)
这是最核心的部分,你可以对流水线上的数据进行一道或多道"工序"。
最常用的工序有两个:
-
filter(Predicate p):筛选-
作用:像一个筛子,只保留你想要的。
-
Lambda :
user -> user.getAge() > 30(只保留年龄大于30的) -
(
Predicate就是我们之前说的"断言型"接口,返回boolean)
-
-
map(Function f):转换-
作用 :把流水线上的东西变成另一个东西。
-
Lambda :
user -> user.getName()(把User对象转换 成String名字) -
(
Function就是"功能型"接口,有输入有输出)
-
特点:
-
链式调用 :你可以把很多操作串起来,比如
stream().filter(...).map(...)。 -
惰性执行 :你定义这些操作时,它们并不会立即执行。它们只是在"搭建流水线"。
3. 终止操作 (End the Stream)
这是最后一步,也是真正触发流水线启动的一步。
最常用的终止操作有两个:
-
collect(Collectors.toList()):打包-
作用 :把流水线上所有处理完的成品,收集到一个新的
List中。 -
这是 90% 的情况下你想要的。
-
-
forEach(Consumer c):消费-
作用:不打包,而是对流水线上的每个成品执行一个操作(比如打印)。
-
Lambda :
name -> System.out.println(name) -
(
Consumer就是"消费型"接口,只进不出)
-
总结
到此我们讲完了函数式接口、lamba表达式、方法引用这三部分的内容,相信你对Java的语法理解有精进了一步。不知道你会不会这样想:Java本身都这么繁琐了,你还总是搞一些其他的语法来简化这种繁琐,我怎么学的过来啊!
后面我们会以重点八股的形式继续学习Java基础语法,敬请期待!