专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
开发者全社区  ·  年会上被迫营业的小姐姐 ·  昨天  
开发者全社区  ·  天王回乡的瓜 ·  2 天前  
开发者全社区  ·  东北复旦投行女自述血泪史 ·  3 天前  
开发者全社区  ·  清华马翔宇再次举报 ·  3 天前  
开发者全社区  ·  大专生的PDF ·  3 天前  
51好读  ›  专栏  ›  郭霖

深入理解Java的Lambda原理

郭霖  · 公众号  · android  · 2021-05-07 22:29

正文



/   今日科技快讯   /


近日,特斯拉的中国竞争对手蔚来正进军挪威市场,计划在9月份向这个北欧国家出口其智能电动汽车。蔚来打算今年在挪威销售电动ES8 SUV,明年销售续航里程接近1000公里的ET7豪华电动汽车。在此之前,特斯拉的另一个中国竞争对手小鹏,已经在2020年12月底向挪威个人客户交付了100多辆G3智能SUV。显然,挪威正迅速成为中国电动汽车初创企业的关键出口市场。


/   作者简介   /


大家周六好,明天就短暂休息一下吧,我们周日再见!

本篇文章来自 Omnipotent_7 同学的投稿,和大家分享了他理解的Lambda原理,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!


Omnipotent_7 的博客地址:
https://blog.csdn.net/weixin_43687181


/   Lambda表达式的背景   /


Lambda函数的概念其实有很久远的历史了,在Lisa,C#中早有实现。且近年来,开发者对语言的表现力有了更高的要求,Java也在JDK 1.8 中引入了Lambda函数这一概念。虽然截止到写下这段文字的一刻已经过去七年之久,但其底层的设计思想仍值得我们参考一番,以便我们更好地使用。


Lambda表达式的核心思想,就是将一段函数作为参数进行传递。那要怎么实现这个目的呢?我们慢慢来看一下。


传递一段代码


传递一个函数作为参数,这在C里面很好实现,函数指针嘛,一两个*号&号能解决的问题,C的语言特性具有先天的优势。


但是Java呢?我们都知道,Java的函数接收的参数都是对象(这个时候可以认为基本类型也是对象),可从来没听说过有函数对象这个说法啊。那要怎么整?


设想有以下场景,函数executeFunc,它接收一个函数,用这个接受到的函数打印另外一个传入的变量word。


static void executeFunc(方法??,String word) {
    // todo 用传入的方法打印word变量
}


匿名内部类写法


先介绍一种曲线救国的方式,勤劳勇敢的程序员们想出来,直接传函数不行是吧,那我就用一个接口包裹住这个方法,传接口对象进去就可以啦!好方法,我们下面来看看代码。


package main;

interface Wrapper {
    void myPrint(String w);
}

class Solution {

    static void executeFunc(Wrapper w, String word) {
        w.myPrint(word);
    }


}


确实,问题解决了。那怎么在主函数调用呢?


public static void main(String[] args) {
        executeFunc(new Wrapper() {
            @Override
            public void  myPrint(String w) {
                // 个性化拓展,例如在打印之前记录时间什么的
                System.out.println(w);
            }
        }, "Hello Lambda!");
    }


看起来有一点麻烦,如果需要用executeFunc的次数多起来的时候,显然就会造成很多“不太必要”的代码了。这里说的不太必要是因为,我们必须满足编译器的需求来进行规范的语法编写,但实际上我们关心的逻辑仅仅是里面那一小段打印的语句:


// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);


这就是lambda派上用场的地方了。


Lambda写法


其实如果你在编译器编写以上代码时,你会收到一个灰色的智能提示,让你用lambda重写以上语句。



我们应用一下这个修改试试看。


public static void main(String[] args) {
    executeFunc(w -> {
        // 个性化拓展,例如在打印之前记录时间什么的
        System.out.println(w);
    }, "Hello Lambda!");
}


我们看到,一大串的new操作,@Override重写被替换成一个传入参数,一对大括号,代码量大大减少。(实际上可以通过方法引用来进一步化简,但是在这里暂不作讨论了)


/  两种写法的实际操作   /


终于介绍完两种常见的写法了。看了上面的写法,可能有人就会说,这看起来不是差不多嘛,lambda就是帮我们完成了那些override啊之类的东西的重写嘛,语法糖,甜甜的,就这?


非也非也,Lambda是Java 8的一个重要升级特性,为了自动生成这个语句,编译器装个插件啥的,写个宏定义啥的不就好了吗?为什么要费这么大功夫来折腾?下面就来细细分析两者的区别。下面会涉及到一些字节码的理解,不过不用担心,要用到的字节码指令我会讲清楚的(我也曾经深受其苦)。


完整代码如下,我们基于以下代码进行分析。


package main;

interface Wrapper {
    void myPrint(String w);
}

class Solution {

    static void executeFunc(Wrapper w, String word) {
        w.myPrint(word);
    }

    public static void main(String[] args) {
        // 匿名内部类写法
        executeFunc(new Wrapper() {
            @Override
            public void myPrint(String w) {
                // 个性化拓展,例如在打印之前记录时间什么的
                System.out.println(w);
            }
        }, "Hello Lambda!");

        // lambda写法
        executeFunc(w -> {
            // 个性化拓展,例如在打印之前记录时间什么的
            System.out.println(w);
        }, "Hello Lambda!");

    }
}


匿名内部类的实际操作


我们先把lambda部分注释掉,仅保留匿名内部类写法的语句。利用IDEA的ShowByteCode功能进行查看。字节码(ByteCode)是JVM运行时读取执行的,一切的语法特性,都会在这里暴露无遗。下面先贴一段字节码。


// class version 55.0 (55)
// access flags 0x20
class main/Solution {

  // compiled from: Solution.java
  NESTMEMBER main/Solution$1
  // access flags 0x0
  INNERCLASS main/Solution$1 null null

  // access flags 0x0
  ()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object. ()V
    RETURN
   L1
    LOCALVARIABLE this  Lmain/Solution; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static executeFunc(Lmain/Wrapper;Ljava/lang/String;)V
   L0
    LINENUMBER 10 L0
    ALOAD 0
    ALOAD 1
    INVOKEINTERFACE main/Wrapper.myPrint (Ljava/lang/String;)V (itf)
   L1
    LINENUMBER 11 L1
    RETURN
   L2
    LOCALVARIABLE w Lmain/Wrapper
; L0 L2 0
    LOCALVARIABLE word Ljava/lang/String; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 15 L0
    NEW main/Solution$1
    DUP
    INVOKESPECIAL main/Solution$1. ()V
    LDC "Hello Lambda!"
    INVOKESTATIC main/Solution.executeFunc (Lmain/Wrapper;Ljava/lang/String;)V
   L1
    LINENUMBER 29 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String
; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}


JVM里是不管你内部内部类的,它只认类和类实例化的对象。众所周知,接口是不能实例化的,也就是说,是不能通过直接new的方式新建一个纯接口对象,而是要编写一个类来实现接口,进而实例化这个类。那注意到我们上面的语句。


//*****
new Wrapper() {
            @Override
            public void myPrint(String w) {
                // 个性化拓展,例如在打印之前记录时间什么的
                System.out.println(w);
            }
        }
//*****


这不就违背了这一规则了吗?其实不然。它看起来是这样写的,实际上还是通过类来实例化对象,只不过这个工作JVM替我们完成了。


回到字节码,在第六行,我们发现编译器新建了一个类,叫Solution$1。在42行进行了new操作,新建了一个Solution$1的对象以供后续操作。在44行进行了这个对象的初始化工作,init嘛,大家都懂。


本来内部类的名字应该是类似Solution$MyInnerClass这样的命名的,$符号之前其所在的父类Solution,MyInnerClass是内部类的名字。但是由于我们创建的是实现了Wrapper接口的匿名内部类,匿名没有名字,编译器就想,干脆就叫1吧,没错,就简简单单用个1来命名了。


什么?不是说不能用数字命名类吗?注意,我们平时编程就是写给老板(JVM)看的,所以有一大串语法规则,老板看了说OK,我再加一个东西,这个东西我就要这样子,你也不能说什么对吧。


不信你就在匿名内部类的实现里加一个语句,通过反射获取当前匿名内部类的类名,就会发现人家这个内部类的的确确叫 1,完整名字是main.Solution$1


// 匿名内部类写法
executeFunc(new Wrapper() {
    @Override
    public void myPrint(String w) {
        // this指向当前匿名内部类的对象,通过对象获取类名
        System.out.println(this.getClass().getName());
        // 个性化拓展,例如在打印之前记录时间什么的
        System.out.println(w);
    }
}, "Hello Lambda!");


输出结果


C:\jdk11\bin\java.exe....
main.Solution$1
Hello Lambda!

Process finished with exit code 0


通过上面的例子,这样也可以理解什么是匿名内部类的“匿名”二字是什么意思了。


Lambda写法的实际操作


那么使用Lambda表达式的时候又发生了什么呢?还是之前的代码,我们注释掉匿名内部类的实现,查看其字节码。


// class version 55.0 (55)
// access flags 0x20
class main/Solution {

  // compiled from: Solution.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x0
  ()V
   L0
    LINENUMBER 8 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object. ()V
    RETURN
   L1
    LOCALVARIABLE this Lmain/Solution; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static executeFunc(Lmain/Wrapper;Ljava/lang/String;)V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    ALOAD 1
    INVOKEINTERFACE main/Wrapper.myPrint (Ljava/lang/String;)V (itf)
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE w Lmain/Wrapper
; L0 L2 0
    LOCALVARIABLE word Ljava/lang/String; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 26 L0
    INVOKEDYNAMIC myPrint()Lmain/Wrapper
; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      (Ljava/lang/String;)V, 
      // handle kind 0x6 : INVOKESTATIC
      main/Solution.lambda$main$0(Ljava/lang/String;)V, 
      (Ljava/lang/String;)V
    ]
    LDC "Hello Lambda!"
    INVOKESTATIC main/Solution.executeFunc (Lmain/Wrapper;Ljava/lang/String;)V
   L1
    LINENUMBER 31 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x100A
  private static synthetic lambda$main$0(Ljava/lang/String;)V
   L0
    LINENUMBER 28 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 29 L1
    RETURN
   L2
    LOCALVARIABLE w Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}


字节码有点长,但是关键的地方就那么几个,我们仔细看看。

首先看到61行的这个地方。

private static synthetic lambda$main$0(Ljava/lang/String;)V


这个看起来很像一个函数的东西,其实就是一个函数,synthetic这个词的意思本来就是“人造的; (人工)合成的; 综合(型)的;”,作为关键字,它表示该方法由编译器自动生成。简而言之,你把它当做void,就很好阅读了。这是一个无返回值的静态方法,名称都定好啦,叫lambda$main$0,接受一个String类型的参数。里面又有一系列操作,println啥的。


诶?仔细想想,是不是很像我们之前写的那个东西?没错,就是那个lambda表达式。


w -> {
    // 个性化拓展,例如在打印之前记录时间什么的
    System.out.println(w);
}
// 就是上面这货


编译器把我们写的lambda表达式转换成了一个静态的私有函数,通过调用这个函数来解决传递一段代码的问题。一切都已经明了~


我们可以尝试在lambda表达式内部使用this关键字,试图像之前匿名内部类那样,获取当前类的类名,其实这个时候通过IDE你也能发现一些端倪了,它会提示你。



不能把this放到一个静态的语境之下,通过上面的分析得知,这个“语境”指的就是静态函数了。







请到「今天看啥」查看全文


推荐文章
开发者全社区  ·  年会上被迫营业的小姐姐
昨天
开发者全社区  ·  天王回乡的瓜
2 天前
开发者全社区  ·  东北复旦投行女自述血泪史
3 天前
开发者全社区  ·  清华马翔宇再次举报
3 天前
开发者全社区  ·  大专生的PDF
3 天前
百度信息流广告服务平台  ·  我有一件大事宣布,不是要结婚...
7 年前