Lambda表达式的一些有趣玩法

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

一、lambda本质

java8中新增了很多接口


/**
 * Represents a function that accepts one argument and produces a result.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object)}.
 *
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    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, R> T testFunc(Function<T, R> func) {
        //do something
        return null;
    }

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

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

三、lambda还能序列化?

1.继承序列化接口

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

@FunctionalInterface
public interface SerializableFunction<T, R> extends Function<T, R>, Serializable {

}

答案是下面这个:

/**
 * Serialized form of a lambda expression.  The properties of this class
 * represent the information that is present at the lambda factory site, including
 * static metafactory arguments such as the identity of the primary functional
 * interface method and the identity of the implementation method, as well as
 * dynamic metafactory arguments such as values captured from the lexical scope
 * at the time of lambda capture.
 *
 * <p>Implementors of serializable lambdas, such as compilers or language
 * runtime libraries, are expected to ensure that instances deserialize properly.
 * One means to do so is to ensure that the {@code writeReplace} method returns
 * an instance of {@code SerializedLambda}, rather than allowing default
 * serialization to proceed.
 *
 * <p>{@code SerializedLambda} has a {@code readResolve} method that looks for
 * a (possibly private) static method called
 * {@code $deserializeLambda$(SerializedLambda)} in the capturing class, invokes
 * that with itself as the first argument, and returns the result.  Lambda classes
 * implementing {@code $deserializeLambda$} are responsible for validating
 * that the properties of the {@code SerializedLambda} are consistent with a
 * lambda actually captured by that class.
 *
 * @see LambdaMetafactory
 */
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<Issue> simpleChain = SimpleChain.of(Issue::getId);
        List<SerializableFunction<Issue, ?>> funcList = simpleChain.getFuncList();
        SerializableFunction<Issue, ?> 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。这比jpa或者其他的一些example来配置查询条件的写法要灵活且严格的多,不需要再用手写字符串字段名的方式配置条件了。

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

除此之外我们还可以做到简单的将方法作为参数传递,让一个方法标识作为像普通参数一样传递,并在合适的时候使用。

例如我们有些方法是私有方法,只在本类调用,但本类调用时可能是stream或一些其他的api需要Function入参怎么办?

方法1:私有方法返回Function

private Function<String,Long> stringToInt(){
    return str -> {
        int i = Integer.parseInt(str);
        return i+2;
    };
}

调用时:

Stream.of("1").map(stringToInt()).collect(Collectors.toList());

方法2:方法传递

private int stringToLong(String str){
    int i = Integer.parseInt(str);
    return i+2;
}

调用时:

Stream.of("1").map(this::stringToInt).collect(Collectors.toList());

 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340