【Java基础14】函数式接口、lamba表达式、方法引用一网打尽(下)

方法引用是 Lambda 表达式的"语法糖"。理解了它,你写的 Stream API 代码会立刻变得非常专业和简洁。

什么是方法引用?

核心思想 :当你写的 Lambda 表达式的唯一 工作就是去调用一个已经存在 的方法时,你就可以使用"方法引用"(::)来代替这个 Lambda 表达式。

为什么需要它?

我们来看一个简单的 forEach 例子:

  • Lambda 写法

    java 复制代码
    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::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>。Lambda s -> 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>。Lambda s -> 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());
  • 解读

    1. map 需要一个 Function<User, String>,它的抽象方法是 String apply(User user)

    2. 我们的 Lambda 是 user -> user.getName()

    3. 注意看:Lambda 的第一个(也是唯一一个)参数 user ,变成了 getName() 方法的调用者

    4. 编译器看到这个模式 (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>。Lambda name -> new User(name) 刚好符合。编译器看到 ::new,就会自动去 User 类里寻找一个接收 String(即流中元素 name 的类型)的构造函数。

核心规则 :当你的 Lambda 表达式包含了额外的逻辑时,就不能用方法引用。、

Stream 的用法"三步曲"

使用 Stream 几乎总遵循这三个步骤,像个"公式":

  1. 创建流 :把原材料(List)放到流水线上。

  2. 中间操作 :在流水线上对原材料进行加工(可以有多道工序)。

  3. 终止操作 :把加工好的成品打包消费(启动流水线)。

我们用一个 User 列表来举例:

java 复制代码
// 原材料仓库
List<User> users = ... ; // 假设里面有很多 User 对象

// 目标:找到所有 30 岁以上的用户,获取他们的名字,并返回一个新列表

1. 创建流 (Get the Stream)

这是第一步:告诉 Stream 你的数据源是什么。最常用的就是 list.stream()

复制代码
users.stream() // 从这里开始,users 列表中的数据就被放到了流水线上

注意,这里的流不是输入输出流

对比维度 I/O Stream Java 8 Stream (我们刚讲的)
中文比喻 数据的管道🚰 数据的流水线🏭
所属包 java.io java.util.stream
处理对象 外部数据(字节或字符) 内存数据(Java 对象)
来源/去向 文件、网络套接字(Socket)、字节数组 List, Set, Map, 数组
核心目的 I/O 读写(把数据读进来/写出去) 计算和转换(筛选、排序、分组)
核心操作 read(), write(), close() filter(), map(), collect()
举例 FileInputStream, BufferedReader list.stream()

2. 中间操作 (Process the Stream)

这是最核心的部分,你可以对流水线上的数据进行一道或多道"工序"。

最常用的工序有两个:

  • filter(Predicate p):筛选

    • 作用:像一个筛子,只保留你想要的。

    • Lambdauser -> user.getAge() > 30 (只保留年龄大于30的)

    • Predicate 就是我们之前说的"断言型"接口,返回 boolean

  • map(Function f):转换

    • 作用 :把流水线上的东西变成另一个东西。

    • Lambdauser -> user.getName() (把 User 对象转换String 名字)

    • Function 就是"功能型"接口,有输入有输出)

特点:

  • 链式调用 :你可以把很多操作串起来,比如 stream().filter(...).map(...)

  • 惰性执行 :你定义这些操作时,它们并不会立即执行。它们只是在"搭建流水线"。

3. 终止操作 (End the Stream)

这是最后一步,也是真正触发流水线启动的一步。

最常用的终止操作有两个:

  • collect(Collectors.toList()):打包

    • 作用 :把流水线上所有处理完的成品,收集到一个新的 List 中。

    • 这是 90% 的情况下你想要的。

  • forEach(Consumer c):消费

    • 作用:不打包,而是对流水线上的每个成品执行一个操作(比如打印)。

    • Lambdaname -> System.out.println(name)

    • Consumer 就是"消费型"接口,只进不出)

总结

到此我们讲完了函数式接口、lamba表达式、方法引用这三部分的内容,相信你对Java的语法理解有精进了一步。不知道你会不会这样想:Java本身都这么繁琐了,你还总是搞一些其他的语法来简化这种繁琐,我怎么学的过来啊!

后面我们会以重点八股的形式继续学习Java基础语法,敬请期待!

相关推荐
spencer_tseng4 小时前
pinyin4j-2.5.0.jar
java·jar·pinyin4j
lzq6034 小时前
Python虚拟环境全指南:venv与conda对比与实践
开发语言·python·conda
ZhengEnCi4 小时前
J1B-为什么99%的人配置Java环境失败?大厂开发者5分钟搞定的JDK安装与环境配置完全指南
java
零雲4 小时前
java面试:有了解过kafka架构吗,可以详细讲一讲吗
java·面试·kafka
一行•坚书4 小时前
kafka服务端与客户端如何协作?生产者发送消息分区策略是什么?消费者组分区策略?集群与ACK机制?
java·后端·kafka
小年糕是糕手4 小时前
【数据结构】常见的排序算法 -- 插入排序
c语言·开发语言·数据结构·学习·算法·leetcode·排序算法
serve the people4 小时前
Prompt Composition with LangChain’s PipelinePromptTemplate
java·langchain·prompt
天天摸鱼的java工程师4 小时前
干掉系统卡顿!Excel异步导出完整实战方案(百万数据也不慌)
java·后端
星释5 小时前
Rust 练习册 4:Deref trait 与智能指针
开发语言·后端·rust