美文网首页
Java Lambda 符号引用::探析与小结

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

作者: lz做过前端 | 来源:发表于2022-01-12 14:47 被阅读0次

    本文是基于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

    符号引用的场景与方式

    • 类签名实例方法引用
    • 实例签名方法引用
    • 类签名静态方法的引用
    • 实例化方法(todo):对象的new、数组的new
    • 数组方法(todo):clone
    • 泛型在符号引用下的使用(todo)

    具体示例

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

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

    类签名实例方法引用

    • Function<Main, String> getNameFunction = Main::getName;
    • BiConsumer<Main, String> setNameBiConsumer = Main::setName;

    上面两种都属于【类签名实例方法引用】,只不过由于方法的签名(参数、返回值)不同,属于不用的函数接口类型,这些函数接口由于是常用的,在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");
            }
        }
    

    实例签名方法引用

    • Supplier<String> getNameSupplier = main::getName;
    • Consumer<String> setNameConsumer = main::setName;
      除了【类签名实例方法引用】可以引用类的实例方法,还可以使用【实例签名方法引用】,这两个函数接口类型,我就不贴了,大家自己去包java.util.function下面找下

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

    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?我总结区别如下:

    • Main::getName这种方式将类的方法的纯引用,上面也说了,它更像一个静态方法。把实例和方法分开了。因此我们可以将类的实例方法作为参数传递出去,让拥有实例的方法帮我们去调用,这样有时候代码可以写的非常简洁紧凑
    • main::getName这种已经已经耦合了实例对象,作为参数调用的时候,只是一个调用,然后拿结果,更像一个成品。
    • 此外【实例签名方法引用】在实例为null时,即使你的代码没有走到调用处也会触发 NPE 异常(JDK8.0版本及之前),据说这个问题在之后的高版本中得到修复,我还没有机会进行验证。从直觉上来看,这属于一个BUG,因为代码都没有执行。以下是测试代码
        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;
        }
    

    相关文章

      网友评论

          本文标题:Java Lambda 符号引用::探析与小结

          本文链接:https://www.haomeiwen.com/subject/cjkwcrtx.html