Java Lambda 符号引用::探析与小结

2022-01-12  本文已影响0人  lz做过前端

本文是基于JDK8.0下做的测试和研究
参考:https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-LambdaExpression
NPE问题探究:https://stackoverflow.com/questions/65101313/what-wrong-with-java-lambda-expression-why-what-diff-between-and-norma
查看字节码命令:javap -c -verbose xxx.class

符号引用的场景与方式

具体示例

这样说有点抽象,我们来看个具体例子,假设有个如下类

public class Main {

    private String name;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static String buildName(Integer param) {
        return "@@@" + param + "@@@";
    }
}

我们可以用如下方式引用Main类的方法

public static void main(String[] args) {
        Main main = new Main();

        // 类签名实例方法引用
        Function<Main, String> getNameFunction = Main::getName;
        // 实例签名方法引用
        Supplier<String> getNameSupplier = main::getName;
        // 类签名实例方法引用
        BiConsumer<Main, String> setNameBiConsumer = Main::setName;
        // 实例签名方法引用
        Consumer<String> setNameConsumer = main::setName;
        // 类签名静态方法的引用
        Function<Integer, String> buildName = Main::buildName;
    }

类签名实例方法引用

上面两种都属于【类签名实例方法引用】,只不过由于方法的签名(参数、返回值)不同,属于不用的函数接口类型,这些函数接口由于是常用的,在JDK里都有定义,在包java.util.function下面,都标注为@FunctionalInterface

我们看下这两个函数接口的定义

@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);
}

@FunctionalInterface
public interface BiConsumer<T, U> {

    /**
     * Performs this operation on the given arguments.
     *
     * @param t the first input argument
     * @param u the second input argument
     */
    void accept(T t, U u);
}

只要定义的方法的签名和它们一样,就可以申明该类型为函数接口类型
我们知道调用类的实例方法必须要有类的实例才可以调用,所以这些方法的参数必须要有一个参数为该类的类型,这样JVM才可以帮我们调用,而且必须是第一个参数

为什么必须是第一个参数?
还记得大家在学JVM的字节码章程中的方法调用栈中的局部变量表Slot吗,第一个存的就是this对象,在由java文件编译为class字节码时,JVM已经隐含的帮我们把该方法引用的对象作为第一个参数传进来了,所以【类签名实例方法引用】第一个参数必然是该类的引用。在调用时,需要我们在代码中传进来。它看起来更多的像一个静态方法,而第一个参数是固定的。

我们可以这样使用他们

    public static String useFunction(Main main, Function<Main, String> function) {
        return Objects.nonNull(main) ? function.apply(main) : null;
    }

    public static void useBiConsumer(Main main, BiConsumer<Main, String> biConsumer) {
        if (Objects.nonNull(main)) {
            biConsumer.accept(main, "setName");
        }
    }

实例签名方法引用

本质上,下面的调用效果是一样的

Main main = new Main();
// 原始调用
String name = main.getName();
// 方式1-类签名实例方法引用
Function<Main, String> getNameFunction = Main::getName;
String name = getNameFunction.apply(main);
// 方式2-实例签名方法引用
Supplier<String> getNameSupplier = main::getName;
String name = getNameSupplier.get();

那有人就会问了,他俩有啥区别,啥时候该用方式1,啥时候该用方式2?我总结区别如下:

    public static String useFunction(Main main, Function<Main, String> function) {
        return Objects.nonNull(main) ? function.apply(main) : null;
    }

    public static String useSupplier(Main main, Supplier<String> supplier) {
        return Objects.nonNull(main) ? supplier.get() : null;
    }

    public static void useBiConsumer(Main main, BiConsumer<Main, String> biConsumer) {
        if (Objects.nonNull(main)) {
            biConsumer.accept(main, "setName");
        }
    }

    public static void useConsumer(Main main, Consumer<String> consumer) {
        if (Objects.nonNull(main)) {
            consumer.accept("setName");
        }
    }

    public static void main(String[] args) {
        // NPE
        Main mainNull = null;
        String fValue = Main.useFunction(mainNull, Main::getName);
        System.out.println("fValue = " + fValue);
        String sValue = Main.useSupplier(mainNull, mainNull::getName); // will throw NPE exception
        System.out.println("sValue = " + sValue);
        String sValue1 = Main.useSupplier(mainNull, () -> mainNull.getName());
        System.out.println("sValue1 = " + sValue1);

        Main.useBiConsumer(mainNull, Main::setName);
        Main.useConsumer(mainNull, mainNull::setName); // // will throw NPE exception
        Main.useConsumer(mainNull, (x) -> mainNull.setName(x));
    }

如果一定要这样使用,最好的方式是用Main::getName这种方式重构方法参数,如果不想修改,一个折中的方式是() -> mainNull.getName() 将该方法包装成一个新的lambda表达式

类签名静态方法的引用

Function<Integer, String> buildName = Main::buildName;
这个没什么好说的,因为是静态方法,比较简单,参数与函数接口的参数是一致的

    public static String buildName(Integer param) {
        return "@@@" + param + "@@@";
    }

    public static void main(String[] args) {
        // 普通调用方式
        String name = Main.buildName("pppp");
        // 类签名静态方法的引用
        Function<Integer, String> buildName = Main::buildName;
        String name = buildName.apply("pppp");
    }

如果类的方法参数比较复杂怎么办

我们可以通过自定义函数接口来实现复杂方法的lambda参数化,假设我们的Main类有个如下方法

    // BiFunction
    public String getName(Integer param) {
        return this.name + param;
    }
    // TiFunction
    public String getName(Integer param, Object obj) {
        return this.name + param + obj;
    }

我们可以这样,定义一个函数接口

    @FunctionalInterface
    public interface TiFunction<T, U1, U2, R> {

        R apply(T t, U1 u1, U2 u2);
    }

然后就可以这样使用了

    public static void main(String[] args) {
        // 一个复杂的例子
        BiFunction<Main, Integer, String> getNameBiFunction = Main::getName;
        // 多个参数
        TiFunction<Main, Integer, Object, String> getNameTiFunction = Main::getName;
    }
上一篇下一篇

猜你喜欢

热点阅读