理解Consumer<T>
为什么要创造Consumer,创造出它是为了什么
Java 8 之前若想把“一段代码”当参数传,只能传 对象;而对象必须先 写类、写方法、new 实例,步骤又重又死板。
于是官方总结:80 % 的回调场景其实只符合“给我一个 T,我拿去干点事,什么都不用还” 这一句话。
干脆把这句话 固化成接口,让编译器 直接把 Lambda 当成实例——这就是 Consumer 被创造出来的唯一目的:
让“行为”像数据一样,一键传递、组合、复用,而不再被类/对象样板代码绑架。
下面把 Consumer<T>
从“接口定义 → 使用动机 → 实现方式 → 在 JDK 中的落地 → 自己手写一套类似机制”完整拆开讲一遍,力求“一页纸看懂”。
一、接口定义(java.util.function.Consumer)
@FunctionalInterface // 只能有一个抽象方法
public interface Consumer<T> {/*** 对给定值做“副作用”操作,无返回。* 所谓副作用:打印、写库、改外部变量、发网络请求……*/void accept(T t);/*** 默认组合方法:先执行当前 Consumer,再执行 after。* 返回一个“复合 Consumer”,便于链式拼装。*/default Consumer<T> andThen(Consumer<? super T> after) {Objects.requireNonNull(after);return (T t) -> { accept(t); after.accept(t); };}
}
二、设计动机(为什么多此一举搞个接口?)
-
把“行为”变成数据
以前写代码只能把“算法”写死在方法体内;现在可以把算法当成参数传来传去——行为参数化。 -
支持高阶函数
方法签名一旦写成
void xxx(Consumer<T> c)
就意味着“xxx 内部会在某个时机帮你执行 c”,从而完成控制反转(框架调用业务代码)。 -
配合 Lambda 实现函数式编程
单方法接口 +@FunctionalInterface
让 Lambda 表达式可以自动匹配成接口实例,语法糖极致简洁。
三、实现方式(三种写法)
List<String> list = List.of("a", "b", "c");// 1. 匿名内部类(最原始)
list.forEach(new Consumer<String>() {@Overridepublic void accept(String s) {System.out.println(s);}
});// 2. Lambda(最常用)
list.forEach(s -> System.out.println(s));// 3. 方法引用(再简一步)
list.forEach(System.out::println);
编译后三种方式都会生成一个实现了 Consumer 接口的对象,运行时 JVM 会调用它的 accept
方法。
四、在 JDK 中的“落地”位置(部分)
所在类/接口 | 使用 Consumer 的方法 | 说明 |
---|---|---|
Iterable<T> | forEach(Consumer<? super T>) | 遍历元素 |
Stream<T> | forEach(Consumer<T>) / peek(Consumer<T>) | 终端/中间操作 |
Optional<T> | ifPresent(Consumer<? super T>) | 值存在才消费 |
Map<K,V> | forEach(BiConsumer<K,V>) | BiConsumer 是 Consumer 的“两参”变体 |
CompletableFuture<T> | thenAccept(Consumer<T>) | 异步回调 |
ArrayList (重载) | forEach(Consumer<? super E>) | 默认方法实现 |
五、自己动手写一套“类似机制”
假设没有 Consumer
,我们照样可以模拟“行为参数化”:
// 1. 自定义函数式接口
@FunctionalInterface
interface MyHandler<T> {void handle(T t);
}// 2. 工具类里提供“遍历 + 回调”
class Lists {public static <T> void each(Iterable<T> src, MyHandler<T> h) {for (T t : src) {h.handle(t); // 框架调用业务代码}}
}// 3. 使用
Lists.each(Arrays.asList(1,2,3), x -> System.out.println(x * 10));
输出:
10
20
30
这段代码跟 list.forEach(x -> System.out.println(x * 10))
本质一模一样,区别只是接口名不同。
六、常见踩坑点
-
在 Lambda 里修改外部局部变量必须声明为
final
或“事实 final”。
原因:Lambda 实例可能延迟执行,Java 要保证捕获的变量安全。 -
Consumer
只负责“副作用”,不要在里面有返回逻辑;
如果想“把 T 转换成 R”请用Function<T,R>
。 -
链式
andThen
的顺序:
c1.andThen(c2)
先c1
后c2
,异常会在最近的一次accept
中抛出。
七、一张图总结(文字版)
外部代码 ──Lambda──▶ Consumer 实例│ │▼ ▼框架方法 accept(t) 被执行
(forEach/peek)
→ 把“要做什么”交给 Consumer
→ 框架决定“什么时候做、做多少次”
→ 于是算法与流程彻底解耦。
记住一句话:
Consumer<T>
就是 Java 8 对“单参数 + 无返回 + 副作用”这种最常见回调语义的官方封装,让“行为”能够像数据一样被传递、组合、复用。