本文整理 Java 8 中的 Lambda 表达式、方法引用、函数式接口、闭包、柯里化、高阶函数和 Stream API。
1. 纯函数
纯函数通常具备以下特点:
- 相同的输入始终得到相同的输出。
- 不修改函数外部的状态。
- 如果读取外部数据,外部数据在执行期间必须保持不变。
- 不产生文件写入、数据库修改、网络请求等副作用。
示例:
public int add(int a, int b) {
return a + b;
}
上面的 add 方法只依赖参数,不修改外部状态,因此可以视为纯函数。
2. 函数对象化的好处
Java 8 可以通过函数式接口,把一段行为作为对象进行传递。
2.1 行为参数化
方法中的某个行为可以由调用方传入。
public static <T> List<T> filter(
List<T> list,
Predicate<T> predicate) {
List<T> result = new ArrayList<T>();
for (T item : list) {
if (predicate.test(item)) {
result.add(item);
}
}
return result;
}
调用时传入不同的判断逻辑:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = filter(
numbers,
number -> number % 2 == 0
);
2.2 延迟执行
函数对象本身可以先被传递,等满足条件时再执行。
logger.debug("{}", expensive());
上面的写法会先执行 expensive(),即使当前日志级别不是 DEBUG,也可能产生不必要的开销。
如果日志框架支持 Supplier 风格的延迟参数,可以写成:
logger.debug("{}", () -> expensive());
注意:是否支持这种写法取决于具体日志框架和版本,并不是所有
logger.debug都支持 Lambda 参数。
3. Lambda 表达式
3.1 基本格式
(参数列表) -> 表达式
或者:
(参数列表) -> {
多行代码;
return 返回值;
}
3.2 常见写法
明确声明参数类型:
(int a, int b) -> a + b
由编译器推断参数类型:
(a, b) -> a + b
只有一个参数时,可以省略括号:
a -> a + 1
无参数时,括号不能省略:
() -> System.out.println("执行任务")
多行代码必须使用大括号:
(int a, int b) -> {
int result = a + b;
return result;
}
3.3 常见错误
错误写法:
a -> return a;
正确写法:
a -> a
或者:
a -> {
return a;
}
4. 方法引用
方法引用是 Lambda 表达式的简化写法。
基本格式:
类名或对象名::方法名
4.1 静态方法引用
Lambda:
(String value) -> Integer.parseInt(value)
方法引用:
Integer::parseInt
完整示例:
Function<String, Integer> parser = Integer::parseInt;
Integer number = parser.apply("123");
4.2 指定对象的实例方法引用
String prefix = "Java";
Supplier<Integer> lengthSupplier = prefix::length;
等价于:
Supplier<Integer> lengthSupplier = () -> prefix.length();
4.3 任意对象的实例方法引用
Lambda:
(Student student) -> student.getName()
方法引用:
Student::getName
这里的 Student 不是静态调用。函数式接口传入的第一个参数会作为方法调用者。
Function<Student, String> getName = Student::getName;
4.4 构造方法引用
无参构造:
Supplier<Student> supplier = Student::new;
等价于:
Supplier<Student> supplier = () -> new Student();
有参构造:
Function<String, Student> creator = Student::new;
等价于:
Function<String, Student> creator =
name -> new Student(name);
构造方法引用具体匹配哪个构造器,由函数式接口的参数列表决定。
4.5 this 和 super 方法引用
当前对象的方法:
this::handle
父类对象的方法:
super::handle
this::方法名 和 super::方法名 只能在相应的实例上下文中使用。
4.6 返回值可以被忽略
Consumer<T> 的抽象方法没有返回值,但它可以引用一个有返回值的方法,返回值会被直接忽略。
public class MethodReferenceDemo {
public static void main(String[] args) {
Consumer<Object> consumer1 =
MethodReferenceDemo::print1;
Function<Object, Integer> function =
MethodReferenceDemo::print2;
Consumer<Object> consumer2 =
MethodReferenceDemo::print2;
consumer1.accept("Java");
function.apply("Spring Boot");
consumer2.accept("Docker");
}
private static void print1(Object value) {
System.out.println(value);
}
private static int print2(Object value) {
System.out.println(value);
return 1;
}
}
4.7 方法引用的使用条件
方法引用需要满足:
- 目标类型必须是函数式接口。
- 参数数量和参数类型能够匹配。
- 返回值类型能够兼容。
- 构造方法引用需要存在与函数式接口参数匹配的构造器。
方法引用本身不要求类必须有无参构造方法。只有使用
Supplier<Student> supplier = Student::new时,才要求存在无参构造器。
5. 自定义函数式接口
函数式接口有且只有一个抽象方法。
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
}
使用:
Calculator calculator = (a, b) -> a + b;
int result = calculator.calculate(10, 20);
@FunctionalInterface 不是必须的,但推荐添加。它能让编译器帮助检查接口是否满足函数式接口要求。
6. Java 8 常用函数式接口
这些接口都属于 java.util.function,是 Java 8 自带的,不需要引入第三方依赖。
| 接口 | 参数 | 返回值 | 常见用途 |
|---|---|---|---|
Runnable | 无 | 无 | 执行任务 |
Callable<V> | 无 | V | 执行并返回结果,可抛异常 |
Comparator<T> | 两个 T | int | 排序和比较 |
Consumer<T> | T | 无 | 消费数据 |
Supplier<T> | 无 | T | 提供数据 |
Function<T, R> | T | R | 类型转换 |
Predicate<T> | T | boolean | 条件判断 |
UnaryOperator<T> | T | T | 同类型转换 |
BinaryOperator<T> | 两个 T | T | 同类型计算 |
6.1 Predicate
Predicate<Integer> positive =
number -> number > 0;
boolean result = positive.test(10);
6.2 Consumer
Consumer<String> printer =
value -> System.out.println(value);
printer.accept("Hello Java");
6.3 Supplier
Supplier<String> supplier =
() -> "default value";
String value = supplier.get();
6.4 Function
Function<String, Integer> length =
String::length;
Integer result = length.apply("Java");
7. 泛型与函数式接口
7.1 固定类型场景
参数类型明确时,直接使用具体泛型类型。
public boolean judge(
Integer value,
Predicate<Integer> predicate) {
return predicate.test(value);
}
调用:
boolean result = judge(
10,
number -> number > 5
);
7.2 多类型复用场景
方法确实需要支持多种类型时,在方法上声明泛型:
public <T> boolean judge(
T value,
Predicate<T> predicate) {
return predicate.test(value);
}
调用:
boolean numberResult =
judge(10, number -> number > 5);
boolean stringResult =
judge("Java", value -> value.length() > 3);
7.3 不推荐的写法
public <T> boolean judge(
Integer value,
Predicate<T> predicate) {
// value 是 Integer,但 predicate 接收 T,
// 两者没有建立类型约束,设计不合理。
return false;
}
参数已经固定为 Integer,就不应该再使用无关的 <T>。
推荐:
public boolean judge(
Integer value,
Predicate<Integer> predicate) {
return predicate.test(value);
}
7.4 类型推断错误示例
judge(1, value -> value.length() > 0);
这里第一个参数是 Integer,所以 Lambda 参数也会被推断为 Integer。Integer 没有 length() 方法,因此编译失败。
8. 闭包
闭包可以理解为:
函数对象与它引用的外部变量绑定在一起形成的整体。
int x = 10;
Function<Integer, Integer> add =
y -> x + y;
这里的 Lambda 捕获了外部变量 x。
Java 要求被 Lambda 捕获的局部变量必须是:
final;- 或者事实上的
final,即effectively final。
下面的写法不能编译:
int x = 10;
Function<Integer, Integer> add =
y -> x + y;
x = 20;
因为 x 后续被修改,不再是 effectively final。
注意:对象引用不能重新赋值,不代表对象内部状态不能修改。但在函数式编程中,仍然建议尽量避免修改外部状态。
9. 柯里化
柯里化是把接收多个参数的函数,拆分成多个每次只接收一个参数的函数。
Function<Integer, Function<Integer, Integer>> add =
x -> y -> x + y;
调用:
int result = add.apply(2).apply(3);
执行过程:
Function<Integer, Integer> addTwo =
add.apply(2);
int result = addTwo.apply(3);
可以理解为:
x -> y -> x + y
等价于:
x -> (y -> x + y)
其中:
Function<Integer, Function<Integer, Integer>>
表示:
- 第一个
Integer是输入参数x; - 返回值是另一个
Function<Integer, Integer>; - 第二个函数接收
y,返回x + y。
10. 高阶函数
高阶函数通常满足至少一个条件:
- 接收函数对象作为参数;
- 返回一个函数对象。
接收函数对象:
public static int calculate(
int a,
int b,
BinaryOperator<Integer> operator) {
return operator.apply(a, b);
}
调用:
int sum = calculate(10, 20, (a, b) -> a + b);
int product = calculate(10, 20, (a, b) -> a * b);
返回函数对象:
public static Function<Integer, Integer> multiplyBy(
int multiplier) {
return value -> value * multiplier;
}
11. Stream API
Stream API 用于对集合数据进行声明式处理。
List<Integer> result = numbers.stream()
.filter(number -> number > 0)
.map(number -> number * 2)
.collect(Collectors.toList());
11.1 Stream 的特点
- Stream 本身不存储数据。
- Stream 通常不会直接修改原集合。
- 中间操作具有延迟执行特性。
- 终止操作执行后,Stream 不能再次使用。
12. 常用 Stream 操作
12.1 filter
根据条件过滤元素,可以连续使用多次。
List<Integer> result = numbers.stream()
.filter(number -> number > 0)
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());
12.2 map
把每个元素转换成另一个元素。
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.toList());
12.3 flatMap
flatMap 先把每个元素映射为一个流,再把多个流合并成一个流。
适用场景:
List<List<T>>;- 二维数组;
- 对象内部包含集合属性;
- 字符串拆分后合并。
List<List<String>> nestedList = Arrays.asList(
Arrays.asList("Java", "Spring"),
Arrays.asList("Docker", "MySQL")
);
List<String> result = nestedList.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
结果:
Java
Spring
Docker
MySQL
12.4 查找元素
Optional<Integer> first =
numbers.stream().findFirst();
Optional<Integer> any =
numbers.stream().findAny();
区别:
findFirst():返回遇到的第一个元素;findAny():允许返回任意元素,并行流中通常更容易优化。
不要直接调用 Optional.get(),推荐:
Integer value = first.orElse(0);
12.5 匹配判断
boolean anyPositive = numbers.stream()
.anyMatch(number -> number > 0);
boolean allPositive = numbers.stream()
.allMatch(number -> number > 0);
boolean noneNegative = numbers.stream()
.noneMatch(number -> number < 0);
含义:
anyMatch:至少有一个满足条件;allMatch:全部满足条件;noneMatch:全部不满足条件。
13. 创建 Stream
13.1 从集合创建
Stream<String> stream = list.stream();
13.2 从数组创建
Stream<String> stream = Arrays.stream(array);
13.3 使用 Stream.of
Stream<String> stream =
Stream.of("Java", "Spring", "Docker");
13.4 基本类型流
IntStream stream = IntStream.range(0, 5);
结果范围:
0、1、2、3、4
range 包含起始值,不包含结束值。
IntStream stream = IntStream.rangeClosed(0, 5);
rangeClosed 同时包含起始值和结束值。
14. Stream 合并与截取
14.1 合并流
正确方法名是 concat,不是 cancat。
Stream<String> stream1 =
Stream.of("Java", "Spring");
Stream<String> stream2 =
Stream.of("Docker", "MySQL");
Stream<String> result =
Stream.concat(stream1, stream2);
14.2 跳过元素
Stream<Integer> result =
numbers.stream().skip(2);
跳过前两个元素。
14.3 限制数量
Stream<Integer> result =
numbers.stream().limit(5);
最多保留前五个元素。
14.4 takeWhile 和 dropWhile
takeWhile(predicate)
dropWhile(predicate)
这两个方法是 Java 9 才加入的,Java 8 不能直接使用。
含义:
takeWhile:从开头连续获取满足条件的元素,一旦遇到不满足的元素就停止;dropWhile:从开头连续丢弃满足条件的元素,一旦遇到不满足的元素,就保留后续元素。
Java 8 项目不要直接使用这两个方法,否则会编译失败。
15. 生成 Stream
15.1 iterate
根据上一个元素生成下一个元素。
Stream<Integer> stream = Stream.iterate(
0,
number -> number + 2
);
List<Integer> result = stream
.limit(5)
.collect(Collectors.toList());
结果:
0、2、4、6、8
iterate 默认会生成无限流,所以通常要搭配 limit。
15.2 generate
通过 Supplier 持续生成元素。
Stream<Double> stream =
Stream.generate(Math::random);
List<Double> result = stream
.limit(5)
.collect(Collectors.toList());
generate 同样会创建无限流,需要使用 limit 限制数量。
15.3 ThreadLocalRandom
int[] numbers = ThreadLocalRandom.current()
.ints(5, 0, 100)
.toArray();
含义:
- 生成 5 个随机整数;
- 最小值包含
0; - 最大值不包含
100。
ThreadLocalRandom 是 Java 7 引入的,属于 JDK 自带类:
java.util.concurrent.ThreadLocalRandom
它适合多线程环境,每个线程使用相对独立的随机数状态,可以减少多个线程争用同一个随机数生成器的问题。
补充修正:
java.util.Random的核心种子更新主要使用原子 CAS 操作,并不是简单地通过synchronized锁完成。但多个线程共享同一个Random时,仍可能竞争同一个原子种子,导致性能下降。
16. 排序与比较
16.1 Comparator
List<Student> students = new ArrayList<Student>();
students.sort(
Comparator.comparing(Student::getAge)
);
倒序:
students.sort(
Comparator.comparing(Student::getAge)
.reversed()
);
多字段排序:
students.sort(
Comparator.comparing(Student::getAge)
.thenComparing(Student::getName)
);
Comparator.compare(a, b) 通常遵循:
- 返回负数:
a排在b前面; - 返回
0:两者在排序意义上相等; - 返回正数:
a排在b后面。
17. Java 8 使用注意事项
17.1 Stream 不能重复使用
错误示例:
Stream<Integer> stream = numbers.stream();
stream.filter(number -> number > 0)
.count();
stream.filter(number -> number < 0)
.count();
第二次操作会抛出异常,因为 Stream 已经被消费。
正确做法:
long positiveCount = numbers.stream()
.filter(number -> number > 0)
.count();
long negativeCount = numbers.stream()
.filter(number -> number < 0)
.count();
17.2 不要在 Stream 中修改外部集合
不推荐:
List<Integer> result = new ArrayList<Integer>();
numbers.stream()
.filter(number -> number > 0)
.forEach(result::add);
推荐:
List<Integer> result = numbers.stream()
.filter(number -> number > 0)
.collect(Collectors.toList());
17.3 不要为了使用 Stream 而使用 Stream
简单循环往往更容易理解:
for (Student student : students) {
student.setEnabled(true);
}
没有必要强行改成复杂的 Stream 链式调用。
17.4 注意装箱和拆箱
Stream<Integer>
会涉及 int 与 Integer 的装箱和拆箱。
大量数字计算优先考虑:
IntStream
LongStream
DoubleStream
18. 总结
Lambda 表达式
(参数) -> 表达式
Lambda 用于创建函数式接口对象。
方法引用
类名::静态方法
对象名::实例方法
类名::实例方法
类名::new
方法引用是满足签名匹配条件时,对 Lambda 的进一步简化。
函数式接口
函数式接口只有一个抽象方法,Java 8 常用接口包括:
Predicate
Consumer
Supplier
Function
UnaryOperator
BinaryOperator
Comparator
Runnable
Callable
Stream API
常见处理流程:
数据源
↓
创建 Stream
↓
filter / map / flatMap / sorted
↓
collect / count / findFirst / reduce
核心原则
- 固定类型场景不要滥用泛型。
- Lambda 捕获的局部变量必须是
final或effectively final。 - Stream 中间操作延迟执行,终止操作触发计算。
- Stream 使用一次后不能重复消费。
- Java 8 不支持
takeWhile和dropWhile。