c260804course / homework0803函数式编程是一种编程范式,核心思想就是把计算看作是数学中的函数计算——程序由一些数学函数的组合构成,通过组合这些函数来完成计算任务;而不是像命令式那样写一条一条的语句序列来达到目的。
// 来源:course/Demo01.java(注释部分)
/*
函数式编程是一种编程范式,核心思想就是把计算看作是数学中的函数计算,
在函数式编程的过程中,程序由一些数学函数的组合构成,通过组合这些函数来完成计算任务。
而不是像命令式的语句序列一样来达到目的。
*/
| 特点 | 说明 |
|---|---|
| 不可变性 | 变量只能被赋值一次,在其生命周期内保持不变,减少程序错误,更好维护 |
| 高阶函数 | 支持把函数作为参数传递,以及把函数作为返回值,可创建更通用的函数 |
| 函数组合 | 通过组合多个小的、独立的函数构建更大、更复杂的函数(核心思想) |
| 无副作用 | 函数执行结果只取决于输入的参数,不会对函数外部环境产生影响 |
| 延迟求值 | 最终值只在必要的时候才被计算,提高程序执行效率 |
一般用在并发编程、异步编程、数据处理以及各种机器学习和数据科学等方向中。
Java 中使用 Lambda 表达式就是一种函数式编程的体现,Lambda 属于 JDK8 的一种语法糖:
// 来源:course/Demo01.java(注释部分)
(参数) -> {代码块} // 这就是 Java 语法糖
Stream 流才是真正把函数式编程引入 Java(详见第 3~9 节)。昨天(0803)已经学习了 Lambda 的标准格式、省略模式、使用前提,今天是它的应用延续。
(形式参数) -> {代码块}
三要素:形式参数、箭头 ->、代码块。
| 对比维度 | 匿名内部类 | Lambda 表达式 |
|---|---|---|
| 需要的类型 | 接口、抽象类、普通具体类均可 | 只能是接口 |
| 使用限制 | 多抽象方法也能用 | 必须只有一个抽象方法 |
| 实现原理 | 编译后产生独立 .class 文件 |
编译后不产生单独 .class 文件,运行期动态生成字节码 |
按照下面的要求完成集合的创建和遍历:
// 来源:course/Demo01.java(注释部分:传统 for-each 写法)
ArrayList<String> list = new ArrayList<>();
list.add("万震山");
list.add("万梓良");
list.add("万事如意");
list.add("王老五");
list.add("万巨龙");
list.add("张三");
// - 把集合中所有以"万"开头的元素存储到一个新的集合
ArrayList<String> list02 = new ArrayList<>();
for (String s : list) {
if (s.startsWith("万")) {
list02.add(s);
}
}
// - 把"万"开头的集合中的长度为3的元素存储到一个新的集合
ArrayList<String> list03 = new ArrayList<>();
for (String s : list) {
if (s.startsWith("万") && s.length() == 3) {
list03.add(s);
}
}
// - 遍历上一步得到的集合
System.out.println(list03);
缺点:要写好几个循环 + 好几个中间集合,代码繁琐、语义不直观。
// 来源:course/Demo01.java(注释部分:Stream 写法)
ArrayList<String> list = new ArrayList<>(List.of("万震山", "万梓良", "万事如意", "王老五", "万巨龙", "张三"));
// Stream流 - 把集合中所有以"万"开头的元素存储到一个新的集合
list.stream().filter(s -> s.startsWith("万")).forEach(System.out::println);
// Stream流 - 把"万"开头的集合中的长度为3的元素存储到一个新的集合
list.stream()
.filter(s -> s.startsWith("万")) // 过滤"万"开头
.filter(s -> s.length() == 3) // 过滤长度为 3
.forEach(System.out::println); // 遍历输出
可以直接阅读代码的字面意思,就能展示跟逻辑无关的代码语义:
list ---> 集合对象
.stream() ---> 获取流
.filter(s -> s.startsWith("万")) ---> 过滤(过滤方式是找"万"开头)
.forEach(System.out::println); ---> foreach 遍历(输出)
不需要再创建一个个中间集合,一行一步操作,链式调用非常直观。
filter 是过滤(筛选)操作:传入一个判断条件(Lambda),返回符合条件的新流。forEach 是遍历操作:对流中的每个元素执行传入的动作(如打印)。Stream 流才是真正把函数式编程引入到 Java 中。Stream 流:你可以把它理解为一条流水线、水流——数据从一头进,经过中间一道道工序(过滤、去重、跳过……),最后从另一头产出结果。
// 来源:course/Demo01.java(注释部分)
/*
Stream流的思想:
Stream流总体来说,分为三类方法:
1、获取方法 -- 获取Stream流
|- 创建一条流水线,并且把数据放在流水线上准备进行操作。
2、中间方法 -- 处理Stream流
|- 流水线上的操作,一次操作完成后,还可以继续进行其他操作
3、终结方法 -- 执行最后的操作
|- 一个Stream流水线只能有一个终结方法,是流水线上的最后一个操作。
*/
| 类别 | 作用 | 特点 |
|---|---|---|
| 获取方法 | 创建一条流水线,把数据放上去准备操作 | 一切的起点 |
| 中间方法 | 流水线上的操作(过滤、去重、跳过、截断、映射…) | 执行完还可以继续链式调用,如 .filter().filter() |
| 终结方法 | 流水线上最后一个操作(遍历、计数、收集、最值…) | 一条流水线只能有一个终结方法,调用后流结束 |
stream.filter(...).filter(...)——每调用一次仍在"流水线上"。filter/limit/skip/map/concat/distinct)详见第 5 节;方法引用详见第 6 节;终结方法 forEach/count 详见第 7 节;收集方法 collect + Collectors 详见第 8 节。中间操作方法的意思是:执行完此方法后,Stream 流依然可以继续执行其他操作——中间方法往往会返回一个新的流对象(Stream 对象),就是为了让我们可以继续进行下一步操作(链式调用)。
// 来源:course/Demo01.java(注释部分:中间操作方法定义)
/*
Stream流中间操作方法:
中间操作的意思是执行完此方法后,Stream流依然可以继续执行其他操作。
常用的方法:
Stream<T> filter(Predicate<? super T> predicate) 用于对流的数据进行过滤
Stream<T> limit(long maxSize) 返回这个流中元素组成的流,截取前指定参数个数的数据。
Stream<T> skip(long n) 跳过指定参数个数的数据,返回由该流剩余元素组成的流
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) 合并a和b两个流为一个流
Stream<T> distinct() 返回由该流的不同元素组成的流,根据Object.equals(obj)得到是否相同。
*/
| 方法 | 作用 | 说明 |
|---|---|---|
Stream<T> filter(Predicate<? super T> predicate) |
过滤 | 传入断言条件,返回满足条件的元素组成的新流 |
Stream<T> limit(long maxSize) |
截取 | 返回由该流中前 maxSize 个元素组成的流 |
Stream<T> skip(long n) |
跳过 | 跳过前 n 个元素,返回由该流剩余元素组成的流 |
Stream<R> map(Function<? super T, ? extends R> mapper) |
映射 | 把流中每个元素映射/转换成另一种类型,返回新类型组成的流 |
Stream<T> concat(Stream a, Stream b) |
合并 | 把 a 和 b 两个流合并为一个流(静态方法) |
Stream<T> distinct() |
去重 | 返回由该流不同元素组成的流,根据 Object.equals(obj) 判断是否相同 |
filter的参数类型Predicate<? super T>是 JDK 内置函数式接口(java.util.function.Predicate,有参有返回值、返回 boolean 的断言),昨天 0803 笔记中已介绍过。
// 来源:course/Demo01.java(main 方法:中间方法演示)
import java.util.function.Predicate; // 新增 import:Predicate 函数式接口
import java.util.stream.Stream;
// 承接上文生成的流(数据已换为新示例):
Stream<String> arrayStream = Arrays.stream(new String[]{"郑伊健", "陈浩南", "齐夏", "谢文东"});
Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
// ① filter / limit / skip 链式调用(Predicate 匿名内部类 vs Lambda 对比在注释中演示,此处先注释,思路已明确)
// Stream<String> newStream = stringStream.filter(s -> s.startsWith("万"));
// Stream<String> newNewStream = newStream.limit(2);
// newNewStream.skip(1).forEach(System.out::println);
// ② concat:合并 a 和 b 两个流为一个流(Stream 接口的静态方法)
Stream<String> concat = Stream.concat(arrayStream, stringStream);
concat.forEach(System.out::println);
// ③ distinct:去重(依据 Object.equals),重复元素只保留一个
List<String> stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能",
"郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏");
stringList.stream().distinct().forEach(System.out::println);
map(映射)是流中非常常用的中间方法:把流中的每个元素转换成另一种类型/形式,返回新类型的流。典型场景——学生集合中提取所有学生的姓名生成一个新集合。
// 来源:course/Demo02.java(main 方法)
// 需求:把学生集合中每个学生的 name 提取出来生成一个新的集合
// 传统写法:for 循环 + 中间集合
ArrayList<String> nameList = new ArrayList<>();
for (Student s : list) {
nameList.add(s.getName());
}
// Stream 写法:map 把"学生"映射成"姓名",再收集成 List
List<String> nameList2 = list // 集合对象
.stream() // 获取流
.map(student -> student.getName()) // 映射:获取每个学生对象的姓名
.collect(Collectors.toList()); // 收集成新集合
map 的参数是 JDK 内置函数式接口
Function<T, R>(有参有返回值)——student -> student.getName()就是Function<Student, String>的简化写法。
Stream.of("万震山", "万梓良", "万事如意", "王老五")
.filter(姓"万") // 中间方法 → 剩 "万震山"、"万梓良"、"万事如意"
.limit(2) // 中间方法 → 截取前 2 个 → "万震山"、"万梓良"
.skip(1) // 中间方法 → 跳过前 1 个 → "万梓良"
.forEach(...) // 终结方法 → 只输出 "万梓良"
Stream 对象,所以可以 .filter().limit().skip() 一直写下去。filter 的 Lambda 本质:s -> s.startsWith("万") 就是 Predicate<String> 匿名内部类 test() 方法的简化写法(s 是元素,返回 boolean)。limit 与 skip 常配合分页:limit(n) 截取前 n 个;skip(m) 跳过前 m 个——两者组合可实现"跳过 m 条、取 n 条"的分页效果。concat 是静态方法:它属于 Stream 接口的静态方法(不是流对象方法),用法是 Stream.concat(流a, 流b),把两个流合并成一个。distinct 依赖 equals:去重依据是 Object.equals(obj)——自定义对象要正确去重,必须重写 equals/hashCode(正好呼应前几天的 Object 类知识点)。map 是"转换"而不是"筛选":filter 是"留下符合条件的",map 是"把每个元素变成另一种东西"(如把学生变成姓名、把 Integer 变成 String)。方法引用是 Lambda 表达式的进一步简化:当 Lambda 的方法体只有一行代码,且这行代码只是调用某个已有的方法时,就可以直接用 ::(双冒号)引用那个方法,把参数按顺序传入。
// 来源:course/Demo02.java(注释部分:方法引用四种形式)
/*
方法引用:
引用类的静态方法: 类名::静态方法名 String::valueOf
引用对象的实例方法: 对象名::方法名 sb::append
引用类的实例方法: 类名::方法名 String::substring
引用类的构造方法: 类名::new String::new
使用前提(四种通用):在重写方法的时候,方法体里面【只有一行代码】,并且这行代码是
调用【某个类的静态方法 / 某个对象的成员方法 / 某个类的成员方法 / 某个类的构造方法】,
并且我们要把重写的【抽象方法中的所有参数都按照顺序传入】到这个方法中。
*/
| 形式 | 写法 | 示例 | 说明 |
|---|---|---|---|
| 引用类的静态方法 | 类名::静态方法名 |
String::valueOf |
调用某个类的静态方法 |
| 引用对象的实例方法 | 对象名::方法名 |
sb::append |
调用某个对象的成员方法(非 static) |
| 引用类的实例方法 | 类名::方法名 |
String::substring |
调用某个类的成员方法 |
| 引用类的构造方法 | 类名::new |
String::new |
调用某个类的构造方法 |
四种形式的共同前提:方法体只有一行代码,且抽象方法的所有参数按顺序传入被引用的方法中。
// 来源:course/Demo02.java(main 方法:map 后把 Integer 转成 String)
// 先 map 出每个学生的 id(Integer),再 map 用 String::valueOf 把整数转成字符串
List<String> stringIdList = list.stream()
.map(s -> s.getId()) // 映射出学号(Integer 流)
.map(String::valueOf) // 静态方法引用:Integer → String(相当于 new String(valueOf(元素)))
.collect(Collectors.toList());
System.out.println(stringIdList);
// 输出:[1001, 1002, 1003, ...]
// 来源:course/Demo02.java(main 方法:把每个学生姓名追加到 StringBuilder)
StringBuilder sb = new StringBuilder();
list.stream()
.map(student -> student.getName()) // 取出姓名流
// 简化前——匿名内部类 Consumer
// .forEach(new Consumer<String>() {
// public void accept(String s) { sb.append(s); }
// })
// 简化后——Lambda 表达式
// .forEach(s -> sb.append(s));
.forEach(sb::append); // 对象方法引用:对每个元素调用 sb.append(元素)
System.out.println(sb);
// 来源:course/MethodDemo.java
// 自定义函数式接口(内部接口):截取字符串
public interface UseString {
String use(String str, int start, int length);
}
public static String subName(String name, UseString us) {
return us.use(name, 0, 1); // 截取第 0 位开始的 1 个字符(取姓氏)
}
public static void main(String[] args) {
// 方式 1:传统实现类
UseStringImpl usi = new UseStringImpl();
String lastName = subName("张三", usi); // 张
// 方式 2:匿名内部类
String lastName02 = subName("李四", new UseString() {
public String use(String str, int start, int length) {
return str.substring(start, length);
}
});
// 方式 3:Lambda 表达式
String lastName03 = subName("李四", (str, start, length) -> str.substring(start, length));
// 方式 4:引用类的实例方法 —— 类名::方法名(str.substring(start,length) 完全匹配)
String lastName04 = subName("王五", String::substring);
}
方式 4 为什么能成立:重写的抽象方法 use(String str, int start, int length) 方法体只有一行 str.substring(start, length)——它调用的是某个类(String)的成员方法,且抽象方法的三个参数 str, start, length 按顺序正好对应 substring 的参数,所以可以直接写成 String::substring。
// 来源:course/Demo02.java(main 方法:把姓名创建成 String 对象)
list.stream()
.map(name -> name.getName()) // 取出姓名流
.map(String::new) // 构造方法引用:等价于 new String(name)
.forEach(System.out::println);
String::valueOf ≈ s -> String.valueOf(s);sb::append ≈ s -> sb.append(s);String::new ≈ s -> new String(s)。String::substring 实现的是 UseString 接口的 use() 方法。对象名::方法名(如 sb::append,针对某个具体对象)与 类名::方法名(如 String::substring,第一个参数隐式作为调用对象)要分清——类名::方法名 形式下,抽象方法的第一个参数就是调用成员方法的那个对象。Comparator.comparing(Employee::getName) 就是"引用类的实例方法"的一种应用(getName 是 Employee 的成员方法)。终结操作方法的意思:执行完这个方法后,Stream 流就不能再执行其他操作了——一条流水线只能有一个终结方法,它是流水线上的最后一个操作。
// 来源:course/Demo01.java(注释部分:终结操作方法定义)
/*
Stream流终结操作方法:
终结操作的意思就是执行完这个方法后,Stream流就不能再执行其他操作了。
void forEach(Consumer<? super T> action); 对此流的每个元素执行操作(不一定是输出)
long count(); 返回此流中的元素数
*/
| 方法 | 作用 | 说明 |
|---|---|---|
void forEach(Consumer<? super T> action) |
遍历 | 对流的每个元素执行操作(不一定是输出,也可以做其他事) |
long count() |
计数 | 返回此流中的元素个数(注意返回值是 long) |
forEach的参数Consumer<? super T>是 JDK 内置函数式接口(java.util.function.Consumer,有参无返回值)——昨天 0803 笔记中介绍过。
forEach/count 后,这条流就结束了,不能再继续 .filter() 等操作。forEach 不一定只是输出:它的 Consumer 参数可以执行任意动作(打印、计数、写入集合等),System.out::println 只是最常见的一种(方法引用)。count 返回 long:元素个数用 long 表示,配合中间方法可统计"过滤后还剩多少个"。收集操作方法:当我们使用流对一些数据操作完毕后,可以把流中的数据收集到集合(List / Set / Map / 数组)中——collect 也是终结操作的一种,但它把结果"存起来"而非直接输出。
// 来源:course/Demo01.java(注释部分:收集操作方法定义)
/*
Stream流收集操作方法:
当我们使用流对一些数据操作完毕后,可以把流中的数据收集到集合中。
<R, A> R collect(Collector<? super T, A, R> collector); 把结果收集到集合中
Object[] toArray(); 把结果收集到数组,生成一个新的结果集数组
default List<T> toList() 把结果收集到List集合,生成一个新的List集合,这里返回的List集合是不可变的集合。
工具类Collectors提供了具体的收集方式:
toMap(Function keyMapper, Function valueMapper) 把元素收集到map集合
toList() 把元素收集到List集合
toSet() 把元素收集到Set集合
*/
| 方法 | 收集目标 | 说明 |
|---|---|---|
collect(Collector collector) |
集合 | 配合 Collectors 工具类的具体收集器使用 |
toArray() |
数组 | 把结果收集成新数组 |
toList() |
List | 把结果收集成新 List——返回的 List 是不可变集合(不能增删改) |
Collectors.toList() |
List | 工具类 Collectors 提供的收集方式 |
Collectors.toSet() |
Set | 工具类 Collectors 提供的收集方式(Set 天然去重) |
Collectors.toMap(keyMapper, valueMapper) |
Map | 工具类 Collectors 提供的收集方式,两个参数都是 Function 函数式接口 |
// 来源:course/Demo01.java(main 方法:收集操作演示)
import java.util.stream.Collectors; // 新增 import:Collectors 收集工具类
List<String> stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能",
"郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏");
// ① 去重后收集成 List 集合
List<String> list1 = stringList.stream() // 集合对象
.distinct() // 去重
.collect(Collectors.toList()); // 收集成 List
System.out.println(list1);
// ② 过滤"伊"后收集成 Set 集合(Set 天然去重,即使不过滤 distinct 也只有不重复的)
Set<String> set = stringList.stream() // 集合对象
.filter(s -> s.contains("伊")) // 过滤包含"伊"的
.collect(Collectors.toSet()); // 收集成 Set
System.out.println(set);
新增两个文件:Student 实体类 + ToMapDemo 演示类。
// 来源:course/Student.java(实体类,作为 toMap 的值对象)
public class Student {
private Integer id; // 学号
private String name; // 姓名
// 无参构造 / 有参构造(id, name)
// getter / setter
// equals / hashCode:基于 id + name(Objects.equals / Objects.hash)
// toString()
}
// 来源:course/ToMapDemo.java(main 方法)
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
// 创建 7 名学生放入 ArrayList
Student s1 = new Student(1001, "项羽");
Student s2 = new Student(1002, "张良");
// ... s3~s7 略(项庄/刘邦/萧何/韩信/樊哙)
ArrayList<Student> list = new ArrayList<>(List.of(s1, s2, s3, s4, s5, s6, s7));
/*
Collectors.toMap 的完整签名:
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
Function<? super T, ? extends K> keyMapper, // 取"键"的函数
Function<? super T, ? extends U> valueMapper // 取"值"的函数
)
*/
// 方式一:Lambda 写法 —— toMap(键, 值)
Map<Integer, Student> map = list // 集合对象
.stream() // 生成流
.collect(Collectors.toMap(
student -> student.getId(), // 键:学号 id
student -> student // 值:学生对象本身
));
System.out.println(map);
// 方式二:匿名内部类写法(Function 函数式接口,与 Lambda 等价)
Map<Integer, Student> map1 = list.stream()
.collect(Collectors.toMap(
new Function<Student, Integer>() { // keyMapper
@Override
public Integer apply(Student student) {
return student.getId();
}
},
new Function<Student, Student>() { // valueMapper
@Override
public Student apply(Student student) {
return student;
}
}
));
collect 是终结操作:调用完流就结束,但结果不是直接输出,而是收集到集合/数组,方便后续继续使用。Collectors 是收集工具类:类比 Arrays(数组工具类)、Collections(集合工具类),Collectors 专门为流提供收集器——toList() / toSet() / toMap()。toMap 的两个参数都是 Function:Function<T, R> 是 JDK 内置函数式接口(有参有返回值)——第一个函数从元素中取键(keyMapper),第二个函数从元素中取值(valueMapper)。student -> student.getId() 取学号当键,student -> student 取学生对象当值。student -> student.getId() 就是 new Function<Student, Integer>(){...} 的简化写法,两种方式等价,Lambda 更简洁(对应 0803 笔记的省略规则)。toList() 与 Stream.toList() 返回不可变集合:收集出的 List 不能 add/remove(呼应 List.of() 不可变集合的坑)。toSet() 天然去重:收集成 Set 本身就保证元素不重复。toMap 键冲突会抛异常:如果多个元素映射出相同的键,Collectors.toMap 默认抛 IllegalStateException——所以键必须是唯一的(如学号 id)。| 数据来源 | 生成方式 | 说明 |
|---|---|---|
| Collection 体系集合 | 集合对象.stream() |
使用默认方法 default Stream<E> stream() |
| Map 体系集合 | map.keySet().stream() / map.values().stream() |
把 Map 转换成 Set 集合,间接生成流 |
| 数组 | Arrays.stream(数组) |
Arrays 静态方法 public static IntStream stream(int[] array) |
| 相同类型的多个数据 | Stream.of(T... value) |
Stream 接口的静态方法 of(T...value) |
// 来源:course/Demo01.java
package course;
import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Demo01 {
public static void main(String[] args) {
// 1. Collection体系集合:使用默认方法 stream() 生成流
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
// 2. Map体系集合:把 Map 转换成 Set,间接的生成流
Map<String, Object> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream(); // key 就是 Set 集合
Stream<Object> valueStream = map.values().stream(); // value 就是 List 集合
// 3. 数组:通过 Arrays 中的静态方法 stream 生成流
String[] array = {"郑伊健", "陈浩南", "齐夏", "谢文东"};
Stream<String> arrayStream = Arrays.stream(array);
// 4. 相同类型的多个数据:通过 Stream 接口的静态方法 of(T...value) 生成流
Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
Stream<Integer> integerStream = Stream.of(10, 15, 20, 35, 46);
}
}
stream() 是 Collection 接口的默认方法:List / Set / Queue 这些 Collection 体系集合都能直接调用。keySet()(Set)或 values()(Collection)取出来再 .stream()。Arrays.stream() 专门处理数组:数组不是集合,不能调 .stream(),要用 Arrays 工具类的静态方法。Stream.of(T... values) 处理零散数据:注意 of 的参数是可变参数(T...)——正好用上了昨天学的 Varargs!Stream.of(10, 15, 20, 35, 46) 本质就是 Stream.of(new Integer[]{10,15,20,35,46})。调用某个方法的时候,需要做哪些事情?核心思路:
private 修饰符 → 肯定调用不了static 修饰 → 类名.方法名() 直接调用;否则创建对象用对象调用看方法的参数:只要不是那 8 个基本数据类型,其他的都是引用数据类型变量,都需要传入对象
传入的对象必须是该参数类型的,或者是该参数类型的子类、实现类的对象——这就形成了多态
// 来源:homework0803/p4_lambda/LambdaDemo.java(注释部分)
// 只要不是那8个基本数据类型,其他的都是引用数据类型变量。都需要在调用方法的时候传入 【对象】
// 传入的对象必须是该参数类型的,或者是该参数类型的子类、实现类的对象。 这就形成了多态
// 来源:homework0803/p4_lambda/Addable.java
// 有参有返回值的函数式接口
package homework0803.p4_lambda;
public interface Addable {
int add(int x, int y);
}
// 来源:homework0803/p4_lambda/Flyable.java
// 有参无返回值的函数式接口
package homework0803.p4_lambda;
public interface Flyable {
void fly(String s);
}
// 来源:homework0803/p4_lambda/LambdaDemo.java
package homework0803.p4_lambda;
public class LambdaDemo {
public static void useAdd(Addable a) {
System.out.println(a.add(10, 20));
}
public static void useFlyable(Flyable f) {
f.fly("今天天气不错...是风和日丽的...");
}
public static void main(String[] args) {
// 调用 useAdd 方法,传入参数有两种选择:
// 1、自己新建一个实现类,实现接口,重写方法。
// 2、创建该接口的匿名内部类对象。
// 现在还有第 3 种:直接传入 Lambda 表达式(本质是简化后的匿名内部类)
useAdd((x, y) -> x + y); // 有参有返回值:单语句 → 省略大括号、分号、return
}
}
| 知识点 | 说明 |
|---|---|
| 调用方法三步看 | ①所属类 ②修饰符(private 不能调 / static 类名调 / 否则对象调)③参数类型 |
| 引用类型参数传对象 | 参数不是 8 大基本类型时,就要传该类型或其子类/实现类的对象(多态) |
| Lambda 是第三种传参方式 | 方法参数是函数式接口时,可传实现类对象、匿名内部类对象,或直接传 Lambda |
useAdd((x, y) -> x + y) |
两个参数(小括号不能省),方法体只有一条表达式语句(省大括号/分号/return) |
创建
TreeSet<Employee>(自然排序),添加 5 名员工(其中两名工号相同,测试去重):按薪资降序、薪资相同再按工号;再用迭代器遍历输出。最后通讯录按姓名排序:重新创建 TreeSet 传入按姓名排序的 Comparator。
// 来源:homework0803/p5_employee/Employee.java
package homework0803.p5_employee;
import java.util.Objects;
public class Employee implements Comparable<Employee> {
private String id; // 工号
private String name; // 姓名
private double salary; // 薪资
private String dept; // 部门
public Employee(String id, String name, double salary, String dept) {
this.id = id;
this.name = name;
this.salary = salary;
this.dept = dept;
}
// getter / setter ...(略)
// 自然排序:按薪资降序,薪资相同再按工号
@Override
public int compareTo(Employee o) {
int result = Double.compare(o.getSalary(), this.salary); // 降序:交换参数
return result == 0 ? this.id.compareTo(o.getId()) : result; // 薪资相同按工号
}
// equals / hashCode:以工号判定(与 compareTo 的"工号"判定口径一致)
@Override
public boolean equals(Object obj) {
if (obj instanceof Employee e) {
return Objects.equals(id, e.id);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
// 来源:homework0803/p5_employee/EmployeeSystem.java(部分)
package homework0803.p5_employee;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
public class EmployeeSystem {
public static void main(String[] args) {
// 1. 创建 TreeSet<Employee>(自然排序:按薪资降序,薪资相同按工号;工号重复自动去重)
TreeSet<Employee> treeSet = new TreeSet<>();
treeSet.add(new Employee("E001", "张三", 12000, "技术部"));
treeSet.add(new Employee("E002", "李四", 15000, "市场部"));
treeSet.add(new Employee("E003", "王五", 15000, "技术部"));
treeSet.add(new Employee("E004", "赵六", 9000, "财务部"));
treeSet.add(new Employee("E005", "刘七", 12000, "市场部"));
treeSet.add(new Employee("E002", "李四", 20000, "市场部")); // 工号重复 → 被去重
// 2. 迭代器遍历输出 → 观察:按薪资降序,薪资相同按工号;工号重复的自动去重
Iterator<Employee> iterator = treeSet.iterator();
while (iterator.hasNext()) {
Employee e = iterator.next();
System.out.println(e.getId() + "-" + e.getName() + "-" + e.getSalary() + "-" + e.getDept());
}
// 3. 通讯录按姓名排序:重新创建 TreeSet,传入按姓名排序的 Comparator(此处用方法引用)
treeSet = new TreeSet<>(Comparator.comparing(Employee::getName));
// 再添加 6 名员工(同上),迭代器遍历输出 → 按姓名排序
}
}
方法引用的四种形式(静态方法 / 对象实例方法 / 类实例方法 / 构造方法)详见第 6 节,本节是它在 Comparator 中的一次具体应用。
Comparator.comparing(Employee::getName) 里的 Employee::getName 就是方法引用——它是 Lambda 的一种更简化的写法:
| 写法 | 代码 | 说明 |
|---|---|---|
| 匿名内部类 | new Comparator<Employee>() { compare(...) { return o1.getName().compareTo(o2.getName()); } } |
最原始,最啰嗦 |
| Lambda 表达式 | (o1, o2) -> o1.getName().compareTo(o2.getName()) |
省略类型、大括号、return |
| 方法引用 | Comparator.comparing(Employee::getName) |
用 类名::方法名 直接引用现有方法,最简洁 |
// 来源:homework0803/p5_employee/EmployeeSystem.java(注释提示)
// 可用匿名内部类,也可尝试用 Lambda:Comparator.comparing(Employee::getName) 或
// (o1, o2) -> o1.getName().compareTo(o2.getName())
| 知识点 | 说明 |
|---|---|
| 自然排序 + 二次比较 | compareTo 先按薪资(Double.compare),薪资相同返回 0 时再按工号比较,保证唯一 |
| 降序技巧 | 交换 compare 参数位置:Double.compare(o.getSalary(), this.salary) = 薪资降序 |
| compareTo / equals / hashCode 一致性 | 都以工号判定口径 → TreeSet 去重(E002 重复被丢弃)与排序行为一致,符合官方推荐 |
instanceof 模式匹配 |
if (obj instanceof Employee e) 直接得到强转后的变量 e(JDK16+ 特性) |
Comparator.comparing(类::getter) |
方法引用写法,TreeSet<>(Comparator.comparing(Employee::getName)) = 按姓名排序 |
| 方法引用 vs Lambda | 方法引用是 Lambda 的进一步简化:Employee::getName ≈ (e) -> e.getName() |
核心步骤:
ArrayList<String>,存入"万震山、万梓良、万事如意、王老五、万巨龙、张三"list.stream().filter(s -> s.startsWith("万")).forEach(...) 筛选并遍历"万"开头.filter(s -> s.length() == 3) 叠加筛选长度为 3 的元素思考:
filter 链式过滤,数据在流水线上流动,不需要中间容器.filter() 属于三类方法中的哪一类?—— 中间方法(可连续调用多个);.forEach() 呢?—— 终结方法(只能有一个,流水线终点)核心步骤:
list.stream()map.keySet().stream() / map.values().stream()Arrays.stream(array)Stream.of(10, 15, 20, 35, 46)思考:
.stream()?—— Map 不是 Collection 体系,要先 keySet() 或 values() 取出 Set/CollectionStream.of(T... values) 用了昨天学的什么语法?—— 可变参数 Varargs(T...),所以可以传任意个数数据核心步骤:
Addable(int add(int x, int y))Flyable(void fly(String s))useAdd(Addable a) / useFlyable(Flyable f) 方法,调用时直接传 LambdauseAdd((x, y) -> x + y) 体会单语句省略大括号/分号/return思考:
核心步骤:
Employee 实现 Comparable<Employee>,compareTo 按薪资降序、薪资相同按工号equals / hashCode 以工号判定(与 compareTo 口径一致)TreeSet<Employee> 存入 6 名员工(含工号重复的 E002),迭代器遍历观察排序 + 去重new TreeSet<>(Comparator.comparing(Employee::getName)) 按姓名排序,迭代器遍历思考:
Comparator.comparing(Employee::getName) 中的 Employee::getName 是什么写法?—— 方法引用,等价于 Lambda (e) -> e.getName()类名::方法名 引用现有方法(当前作业已用上)核心步骤:
Stream.of("万震山", "万梓良", "万事如意", "王老五") 生成流.filter(s -> s.startsWith("万")) 过滤出"万"开头 → 再 .limit(2) 截取前 2 个 → 再 .skip(1) 跳过前 1 个 → .forEach(System.out::println) 输出Stream.concat(流a, 流b) 合并两个流;给包含重复元素的数据加 .distinct() 去重思考:
filter 的 Lambda s -> s.startsWith("万") 本质是什么?—— 是 Predicate<String> 函数式接口 test() 方法的简化(有参、返回 boolean 的断言)limit 和 skip 组合能实现什么?—— 分页:skip(m) 跳过前 m 条、limit(n) 取 n 条distinct 去重的依据是什么?—— Object.equals(obj),自定义对象要重写 equals/hashCode 才能正确去重核心步骤:
list.stream().forEach(System.out::println) 遍历输出每个元素long count = list.stream().filter(s -> s.contains("伊")).count(); 统计过滤后元素个数forEach 之后继续调用 .filter(...),观察编译/运行结果思考:
forEach 之后不能再调用其他方法?—— forEach 是终结操作,调用后流就结束了,不能继续链式操作forEach 的参数 Consumer 一定只能打印吗?—— 不一定,Consumer<T> 是有参无返回值的函数式接口,可以做任意动作(打印、计数、写入等)count() 的返回值类型是什么?—— long,不是 int核心步骤:
list.stream().distinct().collect(Collectors.toList()) 去重后收集成 List,打印观察list.stream().filter(...).collect(Collectors.toSet()) 过滤后收集成 Set,观察 Set 天然去重Student 实体类(id、name、getter/setter、equals/hashCode、toString),创建 7 名学生list.stream().collect(Collectors.toMap(s -> s.getId(), s -> s)) 收集成 Map<Integer, Student>,键是学号、值是学生对象s -> s.getId() 改写成匿名内部类 new Function<Student, Integer>() {...},对比两种写法思考:
collect 是中间操作还是终结操作?—— 终结操作(执行完流结束),但结果不是直接输出,而是收集到集合/数组里Collectors.toMap 的两个参数分别是什么?—— 第一个 keyMapper 从元素中取键,第二个 valueMapper 从元素中取值,都是 Function 函数式接口toMap 默认抛 IllegalStateExceptioncollect(Collectors.toList()) 得到的 List 能 add 吗?—— 不能,Collectors.toList() / Stream.toList() 返回的是不可变集合,直接增删改抛 UnsupportedOperationExceptions -> s.getId() 与匿名内部类 new Function<Student, Integer>(){...} 是什么关系?—— 等价,Lambda 是后者的简化写法对应今日随堂练习的参考解答已写入授课代码:
| 练习 | 参考代码路径 |
|---|---|
练习 3 分页:limit/skip 分页 + Stream.toList() |
exericse/exericse03/PageTest.java |
PageTest 参考解答要点:
// 来源:exericse/exericse03/PageTest.java
// 1. limit(5):截取前 5 个
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).limit(5).forEach(i -> System.out.print(i + " "));
// 2. skip(3):跳过前 3 个
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).skip(3).forEach(i -> System.out.print(i + " "));
// 3. 用 Stream.toList() 把流收集成 List(注意:返回不可变集合)
List<Integer> list = Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).toList();
// 分页方法:skip((pageNo-1) * pageSize).limit(pageSize)
public static void showPage(List<Integer> list, int pageNo, int pageSize) {
list.stream()
.skip((pageNo - 1) * pageSize) // 跳过前面的页
.limit(pageSize) // 截取本页数量
.forEach(i -> System.out.print(i + " "));
}
showPage(list, 1, 3); // 0 1 2
showPage(list, 2, 3); // 3 4 5
showPage(list, 3, 3); // 6 7 8
showPage(list, 4, 3); // 9
| 前置知识 | 当前知识 | 后续知识 |
|---|---|---|
| 匿名内部类(0727) | 函数式编程思想 | 方法引用、Stream 高阶函数 |
| Lambda 表达式(0803) | Lambda 应用(Addable/Flyable) | Stream API 的 filter/map/collect |
| Comparable/Comparator(0803) | TreeSet 员工排序 + 方法引用 | Stream 流的 sorted 排序、Collectors |
| 可变参数 Varargs(0803) | Stream.of(T... values) 可变参数应用 |
方法引用 & 收集器 |
| Collection 体系集合(0729) | 生成 Stream 流的四种方式 | 中间方法(filter/limit/skip/distinct/map) |
| 函数式接口 Predicate(0803) | 中间操作方法(filter/limit/skip/concat/distinct) | Stream 映射 map、终结方法 collect/count、Collectors 收集器 |
| 函数式接口 Consumer/Function(0803) | 终结操作 forEach/count、收集操作 collect/Collectors(toList/toSet/toMap) | Collectors 高级用法(groupingBy 分组、joining 拼接)、Stream 与集合互转 |
| Lambda 表达式(0803) | 方法引用四种形式(静态/对象实例/类实例/构造) | 方法引用的高级组合、Stream 中方法引用大量应用 |