最近在思考lambda相关的问题,简单记录下做的相关反射替代和函数映射的尝试。
原理分析
lambda是jdk8才提供的,原理其实就是动态生成内部类来执行函数映射的方法。也就是说一段lambda表达式会对应特定的类方法,之后调用。底层是通过LambdaMetaFactory实现的函数映射,利用了jdk7给出的MethodHandler之类的函数式编程相关类来实现对函数的映射,MethodType用于函数签名。
反射的话基本接触过java的都听说过,底层实现是利用了native的invoke方法,因此说大量使用反射会影响性能,毕竟调用native方法开销会更大一点。
那么,只要利用LambdaMetaFactory或者函数式编程语法糖来获取对应的方法,并在类初始化的情况下进行构建,那么之后利用该对象调用对应方法,就会和直接调用原始方法一样快,因为本质上是在调用生成的内部类的方法。
简单尝试
首先是直接使用反射的方法,对一个User类对象获取其中的Method,invoke直接调用。
反射大家都会,直接给出实现。
用户类,包括函数编程接口User和实现类
@FunctionalInterface
public interface User {
String getId();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SimpleUser implements User{
String id;
String name;
}
反射类实现
// 简单反射调用user中方法
public class UserReflectAdaptor {
public static String reflectUserId(SimpleUser user) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<? extends SimpleUser> c = user.getClass();
Method method = c.getMethod("getId");
return (String) method.invoke(user);
}
public static void main(String[] args){
SimpleUser user = new SimpleUser("123", "mage");
try {
System.out.println(reflectUserId(user));
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
简单获取Method对象,并使用invoke方法实现代理调用。
lambda表达式实现
lambda表达式进行函数映射基本有数种方法,比较简单的是直接利用::
获取函数对象
static User user;
@Benchmark
public static String lambdaUserId() {
Supplier<String> idGetter = user::getId;
return idGetter.get();
}
@Benchmark是用来做jmh test的,Supplier
是Function
的一种实现类,用来表示只有输出没有输入的方法对象。直接使用get()
即可调用对应的方法获取结果。
已有对象,需要对特定对象进行bind的情况
需要调用的是public方法,同时有已存在的对象的情况下可使用该方式。
使用返回方法对象的方式:
@Benchmark
public static String lambdaFactoryInstanceUserId(){
User sampleUser;
MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
Method method = User.class.getMethod("getId");
// lookup对应的函数,非反射直接映射
MethodHandle methodHandle = lookup.unreflect(method);
// 构建函数映射,invokeType为callSite的期望输出,必须为接口类型
// 增加参数用来输入实现类
MethodType invokedType = MethodType.methodType(User.class, User.class);
// 方法对应Type,参数为输出和输入
MethodType returnType = MethodType.methodType(String.class);
// 句柄指向真正的方法
CallSite callSite = LambdaMetafactory.metafactory(lookup, "getId",
invokedType,
returnType,
methodHandle,
returnType);
// 参数表示实现类对象
sampleUser = (User) callSite.getTarget().invokeExact(user);
return sampleUser.getId() + " lambda user interface";
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
需要注意的是,示例里名为user
的对象为已有的对象,要对其进行绑定并调用getId方法获取对应的结果。
首先和反射相似,获取对应的method对象。lookup.unreflect
方法将对应method设为不可反射,以此获取方法签名和控制MethodHandler。之后使用LambdaMetafactory.metafactory
工厂方法来构建对应的lambda对象生成类。方法参数可解释为:
- lookup,即为获取调用method对象的lookup类,在之前已初始化。
- 方法名,即为之后要调用的方法的名称,在这个示例生成的是User类,所以直接写对应的要调用方法的名称即可。
- invokeType,这个参数要注意的是,这个Type对应的是factory生成的对象类型,比如这个示例里生成User接口对应的对象。这里第二个参数指的是
callSite.getTarget().invokeExact(user);
中绑定的User对象参数。 - returnType这个Type对应的是字节码bytecode层面的调用方法的对应,也就是
getId
方法对应的MethodType。因为是字节码层面的对应,这里也可写成MethodType.methodType(Object.class)
。 - MethodHandle前面已经初始化,也是用来确定要调用的方法。
- 最后的returnType为真正调用时需要的,所以是String.class,也就是
getId
方法,只有返回值String而没有参数。
最后,在invokeType中我们增加了User.class参数,因此在callSite.getTarget().invokeExact(user);
中可以进行对象的绑定,实现lambda优化的方法调用。
不需要与特定对象进行bind的情况
比较灵活的方法是生成Function函数接口相关的对象,如BiCustom,supplier之类的,这个示例中直接生成Function。
设定一个简单工具类:
public class SimpleUserGetter implements UserGetter{
@Override
public String getId(User user) {
return user.getId();
}
private String privateMethodGetter(){
return "this is private";
}
public String publicMethodGetter(){
return "this is public";
}
}
对public方法publicMethodGetter
方法进行lambda优化:
public static String lambdaFactoryFunction(){
MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
Method method = SimpleUserGetter.class.getMethod("publicMethodGetter");
// lookup对应的函数,非反射直接映射
MethodHandle methodHandle = lookup.unreflect(method);
// 构建函数映射,invokeType为callSite的期望输出,必须为接口类型
MethodType invokedType = MethodType.methodType(Function.class);
// 方法对应Type,参数为输出和输入
MethodType returnType = MethodType.methodType(String.class, SimpleUserGetter.class);
// 这边的apply对应的是Function的方法apply
CallSite callSite = LambdaMetafactory.metafactory(lookup, "apply",
invokedType,
// byteCode level
MethodType.methodType(Object.class, Object.class),
methodHandle,
returnType);
Function sp = (Function) callSite.getTarget().invokeExact();
return (String)sp.apply(simpleUserGetter);
} catch (Throwable e) {
e.printStackTrace();
}
return "error";
}
这边需要注意的变化是invokedType
变化了,参数为Function.class,并只有一个,因为并不需要绑定特定的对象即可生成Function接口实现类对象。
另一点是方法名,这边是apply
,因为这个方法名参数对应的是最终调用的方法,也就是Function接口中apply
方法,而不是getId
,这是非常容易错的。
因此最后的returnType
也进行了变化,需要贴合apply
方法进行设置。
获取private方法的情况
调用privateMethodGetter
方法:
public static String lambdaFactoryPrivateFunction(){
try {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(SimpleUserGetter.class, MethodHandles.lookup());
Method method = SimpleUserGetter.class.getDeclaredMethod("privateMethodGetter");
method.setAccessible(true);
// lookup对应的函数,非反射直接映射
MethodHandle methodHandle = lookup.unreflect(method);
// 构建函数映射,invokeType为callSite的期望输出,必须为接口类型
MethodType invokedType = MethodType.methodType(Function.class);
// 方法对应Type,参数为输出和输入
MethodType returnType = MethodType.methodType(String.class, SimpleUserGetter.class);
CallSite callSite = LambdaMetafactory.metafactory(lookup, "apply",
invokedType,
MethodType.methodType(Object.class, Object.class),
methodHandle,
returnType);
Function sp = (Function) callSite.getTarget().invokeExact();
return (String)sp.apply(simpleUserGetter);
} catch (Throwable e) {
e.printStackTrace();
}
return "error";
}
最重要的是lookup的初始化,需要直接给予private方法调用的权限,使用MethodHandles.privateLookupIn
来获取对SimpleUserGetter的private lookup权限。
Method的accessible权限获取大家都懂,不赘述了。
时间测试
简单对使用lambda优化对象进行调用和每次都反射调用的方法进行测试。
对三个方法进行测试:
@Benchmark
public static String SimpleUserId() {
return user.getId();
}
// static方法,利用额外的类进行user处理
@Benchmark
public static String reflectUserId() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<? extends SimpleUser> c = (Class<? extends SimpleUser>) user.getClass();
Method method = c.getMethod("getId");
return (String) method.invoke(user);
}
@Benchmark
public static String lambdaFactoryUserId(){
return simpleUser.getId();
}
分别为直接调用,反射方法和创建lambda对象后进行调用,其中最后一个方法的simpleUser是在类的static块中进行初始化的,实现过程和上文示例中相同。
方法 | 时间 |
---|---|
直接调用 | 1 0 − 9 10^{-9} 10−9 |
利用反射 | 1 0 − 7 10^{-7} 10−7 |
利用lambda | 1 0 − 9 10^{-9} 10−9 |
最后的时间结果可以看出,lambda优化的方法代理调用不需要额外的开销。
总结
简单写了下lambda代替反射的demo,之前几个项目里面有用到使用代理类来实现项目中的SPI实现,这种大量调用的情况如果在初始化过程中使用lambdaFactory,之后之间使用函数对象调用,能快不少,有空改写下试试。