qinghuan
← 回到知识探索

后端现场 / 可下载小项目

参数明明是 21,为什么 invokeExact 仍说方法类型不对?

固定同一个 int 到 int 的方法句柄,只改变参数声明类型和返回值接法,观察匹配失败发生在目标执行之前。

展开本文目录
  1. 下载完整练习包
  2. 第一步:方法句柄也有完整的输入输出形状
  3. 第二步:接收方式也是调用点类型的一部分
  4. 第三步:选择明确转换,或明确使用适配入口
  5. 第四步:类型完全匹配,目标仍可能拒绝输入
  6. 运行单文件,按实际输出逐项核对
  7. 记住这三点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
开始前,会这些就够了
  • 理解 int 与 Integer 的区别、装箱拆箱和静态方法。
  • 先读过反射的基本定位流程;不要求掌握字节码。
  • 先读 反射、注解与动态代理如何连起来:先理解运行时定位方法与执行方法的区别,再读调用点类型匹配。
遇到不熟悉的词,先看这 2 个解释
方法句柄类型
把接收什么参数、返回什么值写成一份完整的类型说明。
精确调用点类型
invokeExact调用处的参数声明与返回预期必须与句柄类型一致。
打开完整术语小抄 ↗

你把一个接收 int、返回 int 的静态方法变成 MethodHandle。调用参数仍然是数值 21,只是接收结果的变量改成 Object,运行时就出现 WrongMethodTypeException。它为什么不像普通方法调用那样自动把 int 装箱?

先不要把这个入口当作一块神秘的反射魔法。本实验只定位本类自己的 twice 方法,打印它的输入和输出类型,再改变调用处的类型。每次目标方法执行都会增加计数,用来辨别“没有进入方法”和“方法自己抛错”。

把例子带到自己的电脑上

下载完整练习包

需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • ExactHandleDemo.java:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

java -version
javac -version
javac -encoding UTF-8 --release 17 ExactHandleDemo.java
java -cp . ExactHandleDemo

解压到独立练习文件夹,在含 ExactHandleDemo.java 的目录打开终端运行。Windows 本类静态twice;int/Integer/Object调用点、返回与参数不匹配、显式拆箱/invoke/asType及目标异常;JDK17/21各12项断言。未测任意转换、null、模块访问、插件或性能。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

第一步:方法句柄也有完整的输入输出形状

MethodType.methodType(int.class, int.class) 的第一个参数是返回类型,后面才是参数类型。程序定位静态方法后打印 (int)int,含义是接收一个 int、返回一个 int。

findStatic 使用类、方法名和这个类型找到可访问的目标。这里没有 receiver 参数,因为目标是静态方法;如果以后换成实例方法,不能照抄本例类型列表而不检查接收者。

第一次调用写为 int exact = (int) mh.invokeExact(21),参数类型和返回预期都为 int,得到 42;目标计数变成一。此时我们建立了一个确定能工作的基线。

第二步:接收方式也是调用点类型的一部分

下一次调用写成 Object value = mh.invokeExact(21)。虽然目标理论上能返回一个可装箱的数字,但调用点此时期待 Object,而句柄实际返回 int;invokeExact 要求两边精确匹配,于是抛类型异常。

再把参数放入 Integer boxed,并仍用 (int) 接返回值,错误变成参数侧:调用点传的是 Integer,目标需要 int。数值都为 21,不能让类型差异消失。

两次异常后 calls 仍然为一,说明目标没有执行。这个观测比只打印异常类更有用:它把调用连接失败和目标业务代码失败分开了。

第三步:选择明确转换,或明确使用适配入口

程序展示三条成功路径。第一条手动调用 intValue,让 invokeExact 的参数恢复为 int;第二条使用 invoke,允许本例所需的适配;第三条用 asType 创建一个接收 Integer、返回 Object 的新句柄,再对它执行 invokeExact。

新句柄打印为 (Integer)Object,返回结果是装箱后的 42。这里精确匹配仍然存在,只是匹配的目标已从原句柄换成适配后的类型。

适配并不表示任意对象都能变成整数。类型转换、拆箱或目标执行仍可能失败;本实验没有把 String、null 或所有基本类型的转换规则全部列完。不要用宽泛的捕获掩盖是哪一步失败。

第四步:类型完全匹配,目标仍可能拒绝输入

最后传入 int 值 -1,参数和返回预期都匹配,所以真正进入 twice;calls 变成五,然后目标抛出本例自己的 IllegalArgumentException。

这一组和前面两个 WrongMethodTypeException 的差别是执行位置。排错记录应同时保留句柄类型、调用处的参数与返回预期、目标是否进入,以及实际异常。

实跑限于本类静态方法、int/Integer/Object、三条适配方式和一个目标异常。没有做速度比较、越权访问、模块边界或任意插件加载;这个实验也不能证明它比反射在你的项目里更快。

运行单文件,按实际输出逐项核对

保存为 ExactHandleDemo.java,在源码目录执行下方命令;需要完整 JDK 17 或更新版本。无需 Maven、网络或第三方依赖。

2026 年 10 月 18 日在 Windows 上分别以 Microsoft OpenJDK 17.0.20.1 与 21.0.12、--release 17 编译运行,输出一致。每项断言失败都会终止程序;下方是本次真实输出。

展开收起代码 · 小实验java · 37 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · java
import java.lang.invoke.*;
public class ExactHandleDemo {
    static int checks,calls;
    static void check(boolean ok) { if(!ok) throw new AssertionError(); checks++; }
    static int twice(int value) {
        calls++;
        if(value<0) throw new IllegalArgumentException("negative input");
        return value*2;
    }
    public static void main(String[] args) throws Throwable {
        MethodType type=MethodType.methodType(int.class,int.class);
        MethodHandle mh=MethodHandles.lookup().findStatic(ExactHandleDemo.class,"twice",type);
        check(mh.type().equals(type));
        int exact=(int)mh.invokeExact(21);
        check(exact==42); check(calls==1);
        System.out.println("handle_type="+mh.type()+"; exact_int="+exact);
        boolean wrongReturn=false;
        try { Object value=mh.invokeExact(21); } catch(WrongMethodTypeException e) { wrongReturn=true; }
        Integer boxed=Integer.valueOf(21);
        boolean wrongParameter=false;
        try { int value=(int)mh.invokeExact(boxed); } catch(WrongMethodTypeException e) { wrongParameter=true; }
        check(wrongReturn); check(wrongParameter); check(calls==1);
        System.out.println("Object_return=WrongMethodTypeException; Integer_parameter=WrongMethodTypeException; target_calls="+calls);
        int explicit=(int)mh.invokeExact(boxed.intValue());
        int flexible=(int)mh.invoke(boxed);
        MethodHandle adapted=mh.asType(MethodType.methodType(Object.class,Integer.class));
        Object adaptedResult=adapted.invokeExact(boxed);
        check(explicit==42); check(flexible==42); check(adaptedResult.equals(Integer.valueOf(42))); check(calls==4);
        System.out.println("explicit_unbox="+explicit+"; invoke_adaptation="+flexible+"; adapted_type="+adapted.type()+"; adapted_value="+adaptedResult);
        boolean targetFailure=false;
        try { int value=(int)mh.invokeExact(-1); }
        catch(IllegalArgumentException e) { targetFailure=e.getMessage().equals("negative input"); }
        check(targetFailure); check(calls==5);
        System.out.println("target_failure=IllegalArgumentException; target_calls="+calls);
        System.out.println("PASS checks="+checks);
    }
}

怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 ExactHandleDemo.java;编译成功后,再执行 java -cp . ExactHandleDemo。两条命令分别回车。2026-10-18 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。Windows 本类静态twice;int/Integer/Object调用点、返回与参数不匹配、显式拆箱/invoke/asType及目标异常;JDK17/21各12项断言。未测任意转换、null、模块访问、插件或性能。

运行后对照结果
handle_type=(int)int; exact_int=42
Object_return=WrongMethodTypeException; Integer_parameter=WrongMethodTypeException; target_calls=1
explicit_unbox=42; invoke_adaptation=42; adapted_type=(Integer)Object; adapted_value=42
target_failure=IllegalArgumentException; target_calls=5
PASS checks=12

记住这三点

  • invokeExact 比较的是参数和返回值的完整类型,数值相同还不够。
  • 调用点的返回预期也会参与类型匹配。
  • 适配失败与目标方法抛错需要分别定位。

别把例子用过头

  • 只检查参数值,忽略调用点的声明类型。
  • 把返回值改接到Object后,仍假设会自动装箱。
  • 把invoke或asType理解为任意输入都可转换。
  • 从小实验推断实际框架性能。

自己试一小步

给这个方法增加一个 long 版本后,你能用观测区分查找失败、类型匹配失败和目标失败吗?

  1. 先列出两个目标方法的完整MethodType,标出返回类型。
  2. 设计int、long、Integer、Long四种参数声明的调用矩阵,先写预测。
  3. 选择有限的适配路径,每次记录句柄类型、输出或异常以及目标进入次数。
  4. 提交矩阵、源码、运行版本和未测试的转换。

做到这些,就算完成

  • 参数和返回类型都纳入比较。
  • 错误发生在查找、调用匹配还是目标执行能够辨别。
  • 没有将结果包装成未经基准测试的性能结论。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。