以编程方式创建 Java8 函数引用
Create Java8 function reference programmatically
只是一个理论问题,我目前没有实际用例。
假设我的一些 API 接受函数引用作为参数,我想通过'::'语法直接从代码中提供它或通过反射收集匹配函数,存储在一些 Map 中并有条件地调用.
可以编程方式将 method
转换为 Consumer<String>
?
Map<String, Consumer<String>> consumers = new HashMap<>();
consumers.put("println", System.out::println);
Method method = PrintStream.class.getMethod("println", String.class);
consumers.put("println", makeFunctionReference(method));
...
myapi.feedInto(consumers.get(someInput.getConsumerId()));
更新:
虽然对当前提供的答案中的解决方案不满意,但在得到有关 LambdaMetaFactory
的提示后,我尝试编译此代码
public class TestImpl {
public static void FnForString(String arg) {}
}
public class Test {
void test() {
List<String> strings = new ArrayList<>();
Consumer<String> stringConsumer = TestImpl::FnForString;
strings.stream().forEach(stringConsumer);
strings.stream().forEach(TestImpl::FnForString);
stringConsumer.accept("test");
}
}
并且在仅将测试 class 提供给 CFR 反编译器之后,我得到了回复:
public class Test {
void test() {
ArrayList strings = new ArrayList();
Consumer<String> stringConsumer =
(Consumer<String>)LambdaMetafactory.metafactory(
null, null, null,
(Ljava/lang/Object;)V,
FnForString(java.lang.String),
(Ljava/lang/String;)V)();
strings.stream().forEach(stringConsumer);
strings.stream().forEach(
(Consumer<String>)LambdaMetafactory.metafactory(
null, null, null,
(Ljava/lang/Object;)V,
FnForString(java.lang.String ),
(Ljava/lang/String;)V)());
stringConsumer.accept("test");
}
}
从中我看到:
- 这在某种程度上可以以“1-liner”的方式进行
- 不需要异常处理
- 我不知道反编译器输出的
(Ljava/lang/Object;)V
(和其他)是什么。它应该与 metafactory() 参数中的 MethodType
匹配。此外 - 反编译器 'eats/hides' 某些东西,但现在似乎在获取函数引用期间调用了方法。
- (offtop) 即使在编译代码中获取函数引用也至少是一个函数调用 - 通常这在性能关键代码中可能不是不明显的廉价操作。
并且...在提供了 Test 和 TestImpl classes 的情况下,CFR 重构了与我编译的完全相同的代码。
你可以像这样用反射来做到这一点:
consumers.put("println", s -> {
try {
method.invoke(System.out, s);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
});
但是如果您希望您的代码使用方法引用(即使用 invokedynamic
指令)编译为相同的代码,您可以使用 MethodHandle
。这没有反射的开销,因此性能会好得多。
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(void.class, String.class);
MethodHandle handle = lookup.findVirtual(PrintStream.class, "println", methodType);
consumers.put("println", s -> {
try {
handle.invokeExact(System.out, s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
consumers.get("println").accept("foo");
在这段代码中,首先创建一个 MethodHandles.Lookup
object is retrieved. This class is reponsible for creating MethodHandle
objects. Then a MethodType
对象,它表示接受的参数和 return 类型并由方法句柄 return 编辑:在本例中,它是return 无效(因此 void.class
)并采用字符串(因此 String.class
)的方法。最后在PrintStream
class.
上找到println
方法得到句柄
您可以参考 this question (and this one) 了解更多关于 MethodHandle
是什么的信息。
最简单但不一定最高效的方法就是将方法包装到消费者中。
final Method m = ...
final T target = ...
Consumer<String> c = (arg1) => m.invoke(t, arg1);
使用 LambdaMetaFactory 可能会产生更优化的代码,但考虑到您正在通过 Map
进行调度,这可能不值得。
This is somehow possible to do in '1-liner' manner
如果您真的想模拟字节码的作用,那仅适用于单行代码的充分折磨定义。你的反编译器在某种程度上欺骗了你。
No exception handling is required
那是因为字节码层面不存在checked exceptions的概念。这可以通过为您执行 sneaky rethrow 的静态辅助方法来模拟。
I have no idea what is (Ljava/lang/Object;)V (and others) in decompiler's output. It should match to MethodType in metafactory() arguments. Additionally - either decompiler 'eats/hides' something, but there seems to be now invocations of methods during getting of function reference.
它看起来像是 invokedynamic 调用的伪代码。 JVM真正做的事情比较复杂,不能用java来简明表达,因为它涉及惰性初始化。最好阅读 java.lang.invoke package description 以了解实际情况。
相当于链接阶段的 java 级将把 CalleSite 的 dynamicInvoker
MH 放入 static final MethodHandle
字段并调用其 invokeExact
方法。
(offtop) Obtaining function reference even in compiled code is at least one function call - in general this may be not unnoticeably cheap operation in performance critical code.
如上所述,链接阶段相当于将方法句柄放在静态字段中一次,然后在将来调用它,而不是尝试第二次解析该方法。
反编译器在您的代码上严重失败,但是,除了重新创建原始的 Java8 方法参考之外,无论如何都没有正确的反编译,这不是您感兴趣的内容。
lambda 表达式和方法引用是使用 invokedynamic
字节码指令编译的,该指令在 Java 编程语言中没有等效项。等效代码类似于:
public static void main(String... arg) {
Consumer<String> consumer=getConsumer();
consumer.accept("hello world");
}
static Consumer<String> getConsumer() {
try {
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodType consumeString = MethodType.methodType(void.class, String.class);
return (Consumer<String>)LambdaMetafactory.metafactory(lookup, "accept",
MethodType.methodType(Consumer.class, PrintStream.class),
consumeString.changeParameterType(0, Object.class),
lookup.findVirtual(PrintStream.class, "println", consumeString), consumeString)
.getTarget().invokeExact(System.out);
}
catch(RuntimeException | Error e) { throw e; }
catch(Throwable t) { throw new BootstrapMethodError(t); }
}
除此之外,在 getConsumer()
中完成的所有事情最初都是由单个 invokedynamic
指令处理的,它将所有涉及的 MethodHandle
和 MethodType
实例视为常量并且其第一次评估的结果获得了内在的缓存设施。您无法使用普通 Java 源代码对其进行建模。
不过,上述 getConsumer()
方法返回的 Consumer<String>
与具有相同行为的表达式 System.out::println
(当分配给 Consumer<String>
时)完全等价和性能特征。
你可以研究一下“Translation of Lambda Expressions” by Brian Goetz for getting a deeper understanding of how it works. Also, the API documentation of LambdaMetafactory
很详尽
只是一个理论问题,我目前没有实际用例。
假设我的一些 API 接受函数引用作为参数,我想通过'::'语法直接从代码中提供它或通过反射收集匹配函数,存储在一些 Map 中并有条件地调用.
可以编程方式将 method
转换为 Consumer<String>
?
Map<String, Consumer<String>> consumers = new HashMap<>();
consumers.put("println", System.out::println);
Method method = PrintStream.class.getMethod("println", String.class);
consumers.put("println", makeFunctionReference(method));
...
myapi.feedInto(consumers.get(someInput.getConsumerId()));
更新:
虽然对当前提供的答案中的解决方案不满意,但在得到有关 LambdaMetaFactory
的提示后,我尝试编译此代码
public class TestImpl {
public static void FnForString(String arg) {}
}
public class Test {
void test() {
List<String> strings = new ArrayList<>();
Consumer<String> stringConsumer = TestImpl::FnForString;
strings.stream().forEach(stringConsumer);
strings.stream().forEach(TestImpl::FnForString);
stringConsumer.accept("test");
}
}
并且在仅将测试 class 提供给 CFR 反编译器之后,我得到了回复:
public class Test {
void test() {
ArrayList strings = new ArrayList();
Consumer<String> stringConsumer =
(Consumer<String>)LambdaMetafactory.metafactory(
null, null, null,
(Ljava/lang/Object;)V,
FnForString(java.lang.String),
(Ljava/lang/String;)V)();
strings.stream().forEach(stringConsumer);
strings.stream().forEach(
(Consumer<String>)LambdaMetafactory.metafactory(
null, null, null,
(Ljava/lang/Object;)V,
FnForString(java.lang.String ),
(Ljava/lang/String;)V)());
stringConsumer.accept("test");
}
}
从中我看到:
- 这在某种程度上可以以“1-liner”的方式进行
- 不需要异常处理
- 我不知道反编译器输出的
(Ljava/lang/Object;)V
(和其他)是什么。它应该与 metafactory() 参数中的MethodType
匹配。此外 - 反编译器 'eats/hides' 某些东西,但现在似乎在获取函数引用期间调用了方法。 - (offtop) 即使在编译代码中获取函数引用也至少是一个函数调用 - 通常这在性能关键代码中可能不是不明显的廉价操作。
并且...在提供了 Test 和 TestImpl classes 的情况下,CFR 重构了与我编译的完全相同的代码。
你可以像这样用反射来做到这一点:
consumers.put("println", s -> {
try {
method.invoke(System.out, s);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
});
但是如果您希望您的代码使用方法引用(即使用 invokedynamic
指令)编译为相同的代码,您可以使用 MethodHandle
。这没有反射的开销,因此性能会好得多。
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(void.class, String.class);
MethodHandle handle = lookup.findVirtual(PrintStream.class, "println", methodType);
consumers.put("println", s -> {
try {
handle.invokeExact(System.out, s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
consumers.get("println").accept("foo");
在这段代码中,首先创建一个 MethodHandles.Lookup
object is retrieved. This class is reponsible for creating MethodHandle
objects. Then a MethodType
对象,它表示接受的参数和 return 类型并由方法句柄 return 编辑:在本例中,它是return 无效(因此 void.class
)并采用字符串(因此 String.class
)的方法。最后在PrintStream
class.
println
方法得到句柄
您可以参考 this question (and this one) 了解更多关于 MethodHandle
是什么的信息。
最简单但不一定最高效的方法就是将方法包装到消费者中。
final Method m = ...
final T target = ...
Consumer<String> c = (arg1) => m.invoke(t, arg1);
使用 LambdaMetaFactory 可能会产生更优化的代码,但考虑到您正在通过 Map
进行调度,这可能不值得。
This is somehow possible to do in '1-liner' manner
如果您真的想模拟字节码的作用,那仅适用于单行代码的充分折磨定义。你的反编译器在某种程度上欺骗了你。
No exception handling is required
那是因为字节码层面不存在checked exceptions的概念。这可以通过为您执行 sneaky rethrow 的静态辅助方法来模拟。
I have no idea what is (Ljava/lang/Object;)V (and others) in decompiler's output. It should match to MethodType in metafactory() arguments. Additionally - either decompiler 'eats/hides' something, but there seems to be now invocations of methods during getting of function reference.
它看起来像是 invokedynamic 调用的伪代码。 JVM真正做的事情比较复杂,不能用java来简明表达,因为它涉及惰性初始化。最好阅读 java.lang.invoke package description 以了解实际情况。
相当于链接阶段的 java 级将把 CalleSite 的 dynamicInvoker
MH 放入 static final MethodHandle
字段并调用其 invokeExact
方法。
(offtop) Obtaining function reference even in compiled code is at least one function call - in general this may be not unnoticeably cheap operation in performance critical code.
如上所述,链接阶段相当于将方法句柄放在静态字段中一次,然后在将来调用它,而不是尝试第二次解析该方法。
反编译器在您的代码上严重失败,但是,除了重新创建原始的 Java8 方法参考之外,无论如何都没有正确的反编译,这不是您感兴趣的内容。
lambda 表达式和方法引用是使用 invokedynamic
字节码指令编译的,该指令在 Java 编程语言中没有等效项。等效代码类似于:
public static void main(String... arg) {
Consumer<String> consumer=getConsumer();
consumer.accept("hello world");
}
static Consumer<String> getConsumer() {
try {
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodType consumeString = MethodType.methodType(void.class, String.class);
return (Consumer<String>)LambdaMetafactory.metafactory(lookup, "accept",
MethodType.methodType(Consumer.class, PrintStream.class),
consumeString.changeParameterType(0, Object.class),
lookup.findVirtual(PrintStream.class, "println", consumeString), consumeString)
.getTarget().invokeExact(System.out);
}
catch(RuntimeException | Error e) { throw e; }
catch(Throwable t) { throw new BootstrapMethodError(t); }
}
除此之外,在 getConsumer()
中完成的所有事情最初都是由单个 invokedynamic
指令处理的,它将所有涉及的 MethodHandle
和 MethodType
实例视为常量并且其第一次评估的结果获得了内在的缓存设施。您无法使用普通 Java 源代码对其进行建模。
不过,上述 getConsumer()
方法返回的 Consumer<String>
与具有相同行为的表达式 System.out::println
(当分配给 Consumer<String>
时)完全等价和性能特征。
你可以研究一下“Translation of Lambda Expressions” by Brian Goetz for getting a deeper understanding of how it works. Also, the API documentation of LambdaMetafactory
很详尽