【问题标题】:How to invoke a MethodHandle with varargs如何使用可变参数调用 MethodHandle
【发布时间】:2019-02-05 04:50:47
【问题描述】:

我正在尝试用 MethodHandle 替换反射调用,但 varargs 似乎无法处理。

我的反射调用程序当前如下所示:

public class Invoker {

    private final Method delegate;

    public Invoker(Method delegate) {
        this.delegate = delegate;
    }

    public Object execute(Object target, Object[] args) {
        return delegate.invoke(target, args);
    }
}

我目前重写它的尝试看起来像这样(Invoker 暴露的接口必须保持不变):

public class Invoker {

    private final Method delegate;
    private final MethodHandle handle;

    public Invoker(Method delegate) {
        this.delegate = delegate;
        this.handle = MethodHandles.lookup().unreflect(delegate);
    }

    public Object execute(Object target, Object[] args) throws InvocationTargetException, IllegalAccessException {
        Object[] allArgs = Stream.concat(Stream.of(target), Stream.of(args)).toArray(Object[]::new);
        return handle.invokeWithArguments(allArgs);
    }
}

这在大多数情况下都可以正常工作。但是可变参数打破了一切。 例如。有这样的方法:

public String test(int i, String... args) {
    return ...;
}

还有如下参数:

Object[] args = new Object[] {10, new String[] {"aaa", "bbb"}};

上面实现的execute 将失败。我尝试了asSpreader()MethodHandles.explicitCastArguments()invoke 而不是invokeWithArguments 等的各种组合,但没有成功。

我可以调用可变参数方法的唯一方法是提供内联参数而不是数组。例如

handle.invokeWithArguments(10, "aaa", "bbb")

但我不能这样做并保持它当前拥有的Invoker 的通用性质。

这真的不可能按照我尝试的方式进行吗?

更新: 在对各种场景进行基准测试后,我决定坚持使用反射,因为invokeWithArguments所有测试用例中的表现要差得多。

【问题讨论】:

  • 你试过用数组代替可变参数的invokeExact吗?
  • @GotoFinal 废话,我没有,它有效!但是......我确实需要自动装箱才能继续工作...... Aaargh。
  • 请注意,您可以简单地使用return handle.bindTo(target).invokeWithArguments(args);,而不是Object[] allArgs = Stream.concat(Stream.of(target), Stream.of(args)).toArray(Object[]::new); return handle.invokeWithArguments(allArgs);

标签: java reflection java-7 variadic-functions methodhandle


【解决方案1】:

似乎您只需要一次调用.asFixedArity,因为默认情况下java将使用asVarargsCollector创建方法句柄

public class Main {
    public static String test(int i, String... args) { return "works!"; }

    public static void main(String[] args) throws Throwable {
        Method method = Main.class.getMethod("test", int.class, String[].class);
        System.out.println(new Invoker(method).execute(null, new Object[]{1, new String[] {"foo", "bar"} }));
    }

    public static class Invoker {
        private final MethodHandle handle;

        public Invoker(final Method delegate) throws Exception {
            MethodHandle handle = MethodHandles.lookup().unreflect(delegate);
            if (Modifier.isStatic(delegate.getModifiers())) { // for easy static methods support
                handle = MethodHandles.dropArguments(handle, 0, Object.class);
            }
            this.handle = handle.asFixedArity();
        }

        public Object execute(Object target, Object[] args) throws Throwable {
            Object[] allArgs = new Object[args.length + 1];
            allArgs[0] = target;
            System.arraycopy(args, 0, allArgs, 1, args.length);
            return handle.invokeWithArguments(allArgs);
        }
    }
}

还有许多其他可能的解决方案,例如您可以向 Invoker 构造函数添加更多逻辑(静态工厂可能是个好主意)并使用 asType 方法准备您想要的签名,然后您应该可以使用.invokeExact 获得小的性能提升。

你也可以继续使用Method ;)

【讨论】:

  • @kaqqao 为什么你在执行中添加了bindTo?这将大大降低性能,因为您将使该句柄完全动态化,因此 JIT 将永远无法优化此站点调用。
  • 这不是使用方法句柄的有效方式,它们应该为将来的所有使用创建一次,否则它们会慢得多,就像这里的 x15 左右。
  • 没有意识到其中的含义。再次感谢!
  • 这里奇怪的是为什么bindTo 不只是在前面做同样的参数而不是慢15倍?
  • @kaqqao MethodHandle 上需要修改的每个操作都会构造新的 MethodHandle,在这种情况下差异可能会更小,因为 arraycopy 可能也会吃一些时间,但肯定还是会像慢 10 倍。 (我只对基础版本做了基准测试,没有数组复制调用)
【解决方案2】:

我完成了您的代码以重现您的问题,但它可以使用 invokeWithArguments。也许我错过了什么?

public class Main {

    public String test(int i, String... args) {
        return i + Stream.of(args).collect(Collectors.joining());
    }


    public static void main(String[] args) throws Throwable {
        Main main = new Main();
        Method method = Main.class.getMethod(
            "test",
            int.class,
            String[].class)
        Invoker invoker = new Invoker(method);
        assertEquals("1foobar", invoker.execute(main, new Object[]{1, "foo", "bar"})); // Success
    }


    public static class Invoker {

        private final MethodHandle handle;

        public Invoker(final Method delegate) throws Exception {
            this.handle = MethodHandles.lookup().unreflect(delegate);
        }

        public Object execute(Object target, Object[] args) throws Throwable {
            // Add the target and all arguments in a new array
            Object[] allArgs = Stream.concat(Stream.of(new Object[]{target}), Stream.of(args))
                .toArray(Object[]::new);
            return handle.invokeWithArguments(allArgs);
        }
    }
}

【讨论】:

  • OP 可能有 String[] 作为参数,但我想他可以检查方法是否为可变参数然后解压。
  • 正如@GotoFinal 所说,当我收到参数数组时,varargs 不会被传播,而是自己的数组。好像你有new Object[]{1, new String[] {"foo", "bar"}}。与您用于常规反射调用的形式完全相同。这就是我尝试使用asSpreader 的原因。
猜你喜欢
  • 2019-03-28
  • 1970-01-01
  • 1970-01-01
  • 2021-07-01
  • 1970-01-01
  • 2013-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多