【Java基础14】函数式接口、lamba表达式、方法引用一网打尽(下)
方法引用是 Lambda 表达式的“语法糖”。理解了它,你写的 Stream API 代码会立刻变得非常专业和简洁。
什么是方法引用?
核心思想:当你写的 Lambda 表达式的唯一工作就是去调用一个已经存在的方法时,你就可以使用“方法引用”(::)来代替这个 Lambda 表达式。
为什么需要它?
我们来看一个简单的 forEach 例子:
Lambda 写法
List<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::staticMethodLambda 对比:
(args) -> ClassName.staticMethod(args)场景:你调用的方法是一个静态方法。
示例:把一个 String 列表转换为 Integer 列表。
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::instanceMethodLambda 对比:
(args) -> objectInstance.instanceMethod(args)场景:你调用的方法是一个已经存在的、特定的实例对象的方法。
示例:就是我们最开始的打印例子。
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::instanceMethodLambda 对比:
(obj, args) -> obj.instanceMethod(args)或者(obj) -> obj.instanceMethod()场景:当 Lambda 的第一个参数成为了方法调用者时,就可以使用这种形式。
示例 1:获取所有 User 对象的名字。
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:将字符串列表转为大写。
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::newLambda 对比:
(args) -> new ClassName(args)场景:当你需要“生产”一个新的对象时(常用于
Supplier接口或 Stream 的collect)。
示例:把一个名字列表转换成 User 对象列表。
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 列表来举例:
// 原材料仓库
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基础语法,敬请期待!
