栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Lambda表达式的一些有趣玩法

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Lambda表达式的一些有趣玩法

java17都出了,很多小伙伴对java8中的一些新写法都还不熟悉,最知名的当然莫过于Stream和lambda表达式了(很多人把这两个混为一谈,实际上,stream只是一些新的api,离开lambda的语法糖也可以存在,两者完全是两回事)。stream是个大话题改天有空了单开一篇,lambda也是个大话题,不过今天只讲一些有趣的使用方法。

一、lambda本质

java8中新增了很多接口


@FunctionalInterface
public interface Function {

    
    R apply(T t);

这是其中最典型也是最常用的一个接口,里面只定义了一个方法,需要子类实现。实际上这个接口没有常规的实现类,而都是通过lambda表达式使用的。众所周知,java中接口抽象类不能直接实例化,只能通过继承或者匿名内部类的方式使用。这里答案也就呼之欲出了,没错,lambda在编译成class文件后就是个匿名内部类。

注意这个接口上有一个注解,@FunctionalInterface,没错和你想的一样,这个注解没啥用。不影响你通过lambda表达式的方式来替换内部类的写法。但能通过lambda表达式方式写的接口有一个共同特点,那就是只有一个实例方法。对于这个接口中就是apply方法。这点需要特别注意,其实这个注解的作用也就是告诉使用者,这个接口可以用lambda写法使用。

二、lambda常见写法

如果我们有个方法其入参是一个FunctionalInterface,那么我们可以将lambda直接简单的写入这个方法的入参。

public void testLambda() throws Exception {
        Issue iss = testFunc(i -> i.getId());
        Issue iss2 = testFunc((i) -> i.getId());
        testFunc(Issue::getId);
    }

    private  T testFunc(Function func) {
        //do something
        return null;
    }

可以看到,常见的3中写法(实际还有其他写法,这里不一一例举)。前两种很好理解,和js中的箭头函数一样。第三种似乎有点抽象,这种写法叫“方法引用” (method reference),并不是一种虽然里面没有变量但并不是一种静态方法,效果和前两个是一致的。

以上这些相信大家都听烂了,只是作为一种背景介绍。

三、lambda还能序列化? 1.继承序列化接口

java的实体类是可以序列化的,只要实现Serializable接口就可以通过java自带序列化方式序列化成而二进制,并可以反序列化。那如果我有一个自定义的functionalInterface,并且继承了Serializable接口,我来实例化一下会怎样?反序列化后会是啥?

@FunctionalInterface
public interface SerializableFunction extends Function, Serializable {

}

答案是下面这个:

public final class SerializedLambda implements Serializable {
    private static final long serialVersionUID = 8025925345765570181L;
    private final Class capturingClass;
    private final String functionalInterfaceClass;
    private final String functionalInterfaceMethodName;
    private final String functionalInterfaceMethodSignature;
    private final String implClass;
    private final String implMethodName;
    private final String implMethodSignature;
    private final int implMethodKind;
    private final String instantiatedMethodType;
    private final Object[] capturedArgs;

聪明的小伙伴发现了,这个和我的接口类八竿子打不着啊。而且这又有什么用呢?

2.为什么会是它

每个类中序列化时都会找到本身的一个“writeReplace”方法,他会在序列化之前将当前类转换为其他类进行序列化,反序列化会调用readObject方法。

如果A类writeReplace方法替换成了B,反序列化时会调用B的readObject方法,这时一正一反,回来A类消失了,取而代之的是B类。

明白这个重头戏来了,我们可以通过反射的方式获取到这个不可见的writeReplace方法,将lambda表达式替换为上述的SerializedLambda。

举个例子:

@Test
    public void testSerializeLambda() throws Exception {
        SimpleChain simpleChain = SimpleChain.of(Issue::getId);
        List> funcList = simpleChain.getFuncList();
        SerializableFunction sf = funcList.get(0);
        Method writeReplace = sf.getClass().getDeclaredMethod("writeReplace");
        boolean accessible = writeReplace.isAccessible();
        System.out.println("方法:" + writeReplace);
        writeReplace.setAccessible(true);

        SerializedLambda slambda = (SerializedLambda) writeReplace.invoke(sf);
        System.out.println(om.writevalueAsString(slambda));
        System.out.println("序列化的" + slambda);
        String implMethodSignature = slambda.getImplMethodSignature();
        String implMethodName = slambda.getImplMethodName();
        String implClass = slambda.getImplClass();
        System.out.println("lambda类型:" + MethodHandleInfo.referenceKindToString(slambda.getImplMethodKind()));
        System.out.println("实现实例:" + implClass);
        System.out.println("实现方法:" + implMethodName);
        System.out.println("实现方法签名" + implMethodSignature);
        String instantiatedMethodType = slambda.getInstantiatedMethodType();
        System.out.println("实例方法:" + instantiatedMethodType);
        Type methodType = Type.getMethodType(instantiatedMethodType);
        Type returnType = methodType.getReturnType();
        Type[] argumentTypes = methodType.getArgumentTypes();
        System.out.println("返回值:" + returnType);
        System.out.println("入参:" + Arrays.toString(argumentTypes));

    }

上图中就是一个简单的可序列化的lambda表达式function的简单解析,可以看到,通过writeReplace方法转换后的lambda表达式变成了SerializedLambda。这里从中取出一些属性做看下

序列化的SerializedLambda[capturingClass=class com.cowork.lambda.LambdaTests, functionalInterfaceMethod=com/cowork/lambda/SerializableFunction.apply:(Ljava/lang/Object;)Ljava/lang/Object;, implementation=invokeVirtual com/cowork/issue/domain/entity/baseEntity.getId:()Ljava/lang/Long;, instantiatedMethodType=(Lcom/cowork/issue/domain/entity/Issue;)Ljava/lang/Object;, numCaptured=0]
lambda类型:invokeVirtual
实现实例:com/cowork/issue/domain/entity/baseEntity
实现方法:getId
实现方法签名()Ljava/lang/Long;
实例方法:(Lcom/cowork/issue/domain/entity/Issue;)Ljava/lang/Object;
返回值:Ljava/lang/Object;
入参:[Lcom/cowork/issue/domain/entity/Issue;]

可以看到可以获取到方法的名字和前面,还能获取到这个方法是那个类的。这里可能有同学有疑惑了,为什么明明是issue的getId方法,这里的实现实例为什么是baseEntity?原因是getId是baseEntity的方法,Issue继承baseEntity并没有重写这个方法。所以真实调用类其实就是父类。那如果我就要获取当前调用类呢?没关系,我们看下面有个实例方法,这个方法其实就是Function的apply方法,入参即是Issue。所以可以通过这个方法的入参来获取真实的类型。

3.方法类型

这个SerializedLambda中会记录很多lambda表达式的信息,包括在那个类里实例化的lambda方法签名是什么,方法类型是什么等等,这个有空大家可以自行翻阅手册。这里简单说下方法类型, 也就是上述代码中的int implMethodKind;他又集中类型。

也就是我们写lambda表达式的一些写法,REF_invokeVirtual就是ISSUE::getId这种写法的类型。

上面我们之所以能获取到Issue类型和方法调用原因就是因为我们通过这种methodReference的写法才能获取到。如果不是这样写呢?

还是上面那个lambda,我把写法换成i -> i.getId()

序列化的SerializedLambda[capturingClass=class com.cowork.lambda.LambdaTests, functionalInterfaceMethod=com/cowork/lambda/SerializableFunction.apply:(Ljava/lang/Object;)Ljava/lang/Object;, implementation=invokeStatic com/cowork/lambda/LambdaTests.lambda$testSerializeLambda$ce4f7f85$1:(Lcom/cowork/issue/domain/entity/Issue;)Ljava/lang/Object;, instantiatedMethodType=(Lcom/cowork/issue/domain/entity/Issue;)Ljava/lang/Object;, numCaptured=0]
lambda类型:invokeStatic
实现实例:com/cowork/lambda/LambdaTests
实现方法:lambda$testSerializeLambda$ce4f7f85$1
实现方法签名(Lcom/cowork/issue/domain/entity/Issue;)Ljava/lang/Object;
实例方法:(Lcom/cowork/issue/domain/entity/Issue;)Ljava/lang/Object;
返回值:Ljava/lang/Object;
入参:[Lcom/cowork/issue/domain/entity/Issue;]

可以看到我们丢失了具体的getId的方法名和签名。lambda类型也油inokeVirtual变成了invokeStatic。

四、抛砖引玉

最常见的如上所看到的,我们可以通过lambda来标记方法,那我们自然能通过内省来获取到对应的java bean的属性名。并且,可以看到全程我并没有实例化一个Issue对象。想想我们一般反射的方式获取一个实例对象的方法是怎样的。首先获取到对象的class然后获取所有的申明方法。当然像一些框架会有内省缓存处理。但方法可能会重载,那我们还得指定方法的入参也即我们需要知道方法名和方法签名才能获取到一个Method对象。可名字是一个字符串要是名字稍微长一点或者有相似方法,要明确一个方法可太费劲了。

有了序列化lambda我们可以通过IDE辅助lambda的方式来限定我们的方法。我们通过序列化lambda获得了准确的方法名和方法签名,那获取准确的Method反射对象自然手到擒来。

其实我们熟悉的mybatis-plus框架就是这么来标记类属性的,而属性又是与表字段对应,所以,我们可以很优雅的写java的方式写sql。

Wrappers.lambdaQuery()
                .eq(Issue::getId,1)
                .ge(Issue::getShortId,2);

我们还可以做到简单的将方法作为参数传递,并且如何使用这个Method可以自由发挥。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/644403.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号