我第一次把 MethodHandle 用在“统一清洗入参”时,最容易犯的错是只看方法名,不看过滤器到底消费几个参数。一个简单判断是:每个入参独立变换,用 filterArguments;一个过滤器需要同时吃几项输入,用 collectArguments;还要把组合结果插回目标参数序列的指定位置,再考虑 foldArguments。三者都在组装新的句柄,但生成的 MethodType 不一样。
官方文档:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/invoke/MethodHandles.html
先写出目标句柄的签名,再写出过滤器的输入和返回类型。过滤器返回值必须能接到目标参数上;调用点最后还要与组合后的句柄类型匹配。
我会先把参数组合画成类型契约
MethodHandle 是带类型的可执行引用,type() 返回的 MethodType 同时描述参数列表和返回值。组合器选错时,通常不是“过滤逻辑没执行”,而是组装阶段的类型关系就不成立。下面的判断比背 API 名称更可靠:
- 一个过滤器只接收一个参数,并且每个参数各有一个过滤器:选
filterArguments。 - 一个过滤器要接收连续的一段输入,再用一个返回值替换目标参数:选
collectArguments。 - 过滤器结果要插入目标签名的指定位置,并保留被组合的输入:选带
pos的foldArguments。

filterArguments 适合一元逐参数预处理
filterArguments(target, pos, filters) 的心智模型很简单:过滤器数组的第一个元素对应 pos,后续元素依次对应后面的目标参数。每个非空过滤器必须只有一个入参,且返回类型必须与目标对应参数类型完全相同;过滤器的入参类型会成为新句柄的入参类型。
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Locale;
static String normalize(String value) {
// 统一空白和大小写,返回值仍然保持 String,才能接回 join 的参数。
return value.trim().toLowerCase(Locale.ROOT);
}
static String join(String left, String right) {
// 目标句柄只负责组合,不把清洗逻辑混进来。
return left + ":" + right;
}
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle target = lookup.findStatic(
Demo.class, "join",
MethodType.methodType(String.class, String.class, String.class));
MethodHandle clean = lookup.findStatic(
Demo.class, "normalize",
MethodType.methodType(String.class, String.class));
// 两个过滤器分别作用于参数 0 和参数 1。
MethodHandle adapted = MethodHandles.filterArguments(target, 0, clean, clean);
String value = (String) adapted.invokeExact(" Java", "MethodHandle ");
这里生成的句柄仍然返回 String,但两个调用参数在进入 join 前先分别经过 normalize。官方 API 还允许过滤器数组里出现 null,它代表对应位置保持不变;但数组不能越过目标句柄的参数数量。
需要跨参数组合时不要硬套 filterArguments
如果过滤器要同时读取“前缀”和“原始值”,它就不再是一元过滤器。此时可以把过滤器的多项入参整体接到目标参数位置,用 collectArguments。它会用过滤器的返回值替换目标位置原来的一个参数,调用者看到的句柄参数数量可能增加,也可能减少。
static String makeLabel(String prefix, int number) {
// 一个过滤器同时消费两个输入,返回目标所需的 String。
return prefix + "-" + number;
}
static String wrap(String label) {
// 目标只接收已经组合好的标签。
return "[" + label + "]";
}
MethodHandle target = lookup.findStatic(
Demo.class, "wrap", MethodType.methodType(String.class, String.class));
MethodHandle make = lookup.findStatic(
Demo.class, "makeLabel",
MethodType.methodType(String.class, String.class, int.class));
// (String, int)String -> 先 makeLabel,再把结果交给 wrap。
MethodHandle collected = MethodHandles.collectArguments(target, 0, make);
我会把 collectArguments 理解为“用一个过滤器收集一段输入”。若只是一个输入变换成目标参数,它和 filterArguments 的效果可能相近;一旦过滤器要消费多项参数,就不该继续堆叠一元过滤器。

foldArguments 用来控制结果插回哪里
foldArguments 与 collectArguments 都能消费一段输入,但它更强调“折叠结果插入目标参数列表的位置”。带 pos 的重载允许从目标签名的指定位置开始组合:组合器返回值非 void 时,目标在该位置需要一个相同类型的参数,组合器消费的那段参数仍按规则保留在适配后的调用序列里。
这适合把派生上下文、规范化键或跟踪信息插入已有目标签名中。例如目标原本是 (String, String, String)Result,组合器根据后两项生成一个 String,就可以把它插入第 0 或第 1 个位置,再决定哪些原始参数继续传给目标。这里不要只凭“返回值类型相同”判断成功,pos、组合器参数序列和目标参数序列必须一起对齐。
最后用 MethodType 做选择,而不是靠试错
调用前我会记录三件事:目标句柄的 target.type()、每个过滤器的 filter.type()、组合后句柄的 adapted.type()。尤其是使用 invokeExact 时,调用点的参数和返回类型必须与句柄类型完全一致;不一致会得到 WrongMethodTypeException。如果确实需要更宽松的调用转换,应该显式构造适配句柄或谨慎使用普通 invoke,不要把类型差异藏在业务代码里。
| 你的输入关系 | 优先选择 | 关键契约 |
|---|---|---|
| 每个目标参数各自做一元转换 | filterArguments | 过滤器返回类型等于目标参数类型 |
| 一个过滤器同时消费多项输入 | collectArguments | 过滤器返回值替换目标位置的参数 |
| 需要指定结果插入位置并保留输入 | foldArguments | pos、组合器参数和目标签名同时匹配 |
常见问题
filterArguments 能不能让一个过滤器接收两个参数?
不能把它当作普通的二元过滤器使用。它要求每个非空过滤器只有一个入参;需要跨参数消费时,优先考虑 collectArguments 或 foldArguments。
为什么组合器创建成功,invokeExact 仍然失败?
组合器只保证新句柄自身的类型关系成立,不能替调用点完成隐式转换。先打印或记录 MethodType,再检查调用表达式的参数类型和返回值强制转换是否完全一致。