美文网首页
scala函数字面量是如何实现的

scala函数字面量是如何实现的

作者: tracy_668 | 来源:发表于2021-01-22 10:47 被阅读0次

    [TOC]
    Scala提拱了强大且简洁的函数式的编程方式。说实话, 到目前为止, 我还没有真正体验到函数式编程的好处, 因为确实缺少这方面的实战经验, 从毕业到现在, 一直在写Java代码。 但是Scala的函数式编程, 一眼看上去就给人简洁的感觉。

    本文介绍Scala函数式编程中的一个重要内容: 函数字面量。

    所谓的函数字面量, 说白了就是一段代码, 和Java 8中的lambda表达式相似。 lambda翻译成中文, 有匿名函数的意思, 也可以把Scala中的函数字面量或者Java中的lambda表达式叫做匿名函数。

    下面先看一下Scala中的函数字面量长什么样。 打开Scala命令行, 敲下一个简单的Scala函数字面量:

    scala> (x:Int) => println(x)
    res19: Int => Unit = <function1>
    

    其中 (x:Int) => println(x) 就是一个简单的函数字面量。 => 左边是字面量的参数列表, =>右边是函数体。 如果函数体超出一行, 要用花括号括起来。如下:

    scala> (x:Int) => { println(x)
         | println(x + 1)
         | }
    res20: Int => Unit = <function1>
    

    函数字面量是有类型的。 有上面的打印信息可知, 函数字面量 (x:Int) => println(x) 的类型是 Int => Unit 。 这个类型同样由两部分组成, =>左边是参数的类型, =>右边是函数体的返回值类型。

    函数字面量使用最多的方式是作为参数传递。 下面定义一个这样一个类:

    class FunctionTest{
        
        def doSomething(func : Int => Unit){
            func(4)
        }
     
        def doSomething1(){
            doSomething( ( (x : Int) => println(x) ) )
        }
    }
    

    在这个类中, 有两个方法,doSomething 方法接收一个 Int => Unit 型的字面量作为参数, 并且在方法体中调用了这个函数字面量。 doSomething1 方法调用doSomething 方法, 并且为doSomething 方法传入一个函数字面量 (x : Int) => println(x) 作为参数。

    下面编译这个类:

    scalac FunctionTest.scala
    

    编译完成之后, 可以看到FunctionTest.scala源码相同目录下, 多出两个class文件:

    image.png

    和源码中的FunctionTest类相对应的是FunctionTest.class 。 另一个名称奇怪的class是scalac编译器自动生成的。 我们可以猜想, 这个类是为了辅助函数字面量的实现。

    下面反编译Function.class :

    javap -c -v -classpath . -private FunctionTest
    

    下面是反编译之后的结果(为了减少篇幅, 省略了部分无关信息)

    public class FunctionTest
      SourceFile: "FunctionTest.scala"
      InnerClasses:
           public #24; //class FunctionTest$$anonfun$doSomething1$1
      RuntimeVisibleAnnotations:
        0: #6(#7=s#8)
        ScalaSig: length = 0x3
         05 00 00
      minor version: 0
      major version: 50
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
     
      ......
      //省略了常量池
      ......
     
     
     
    {
      public void doSomething(scala.Function1<java.lang.Object, scala.runtime.BoxedUnit>);
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_1
             1: iconst_4
             2: invokeinterface #16,  2           // InterfaceMethod scala/Function1.apply$mcVI$sp:(I)V
             7: return
     
     
      public void doSomething1();
        flags: ACC_PUBLIC
        Code:
          stack=4, locals=1, args_size=1
             0: aload_0
             1: new           #24                 // class FunctionTest$$anonfun$doSomething1$1
             4: dup
             5: aload_0
             6: invokespecial #28                 // Method FunctionTest$$anonfun$doSomething1$1."<init>":(LFunctionTest;)V
             9: invokevirtual #30                 // Method doSomething:(Lscala/Function1;)V
            12: return
     
        ......
        //省略了自动生成的构造方法
        ......
     
    }
    

    首先看doSomething方法:

    1在源码中, 它接收一个Int => Unit 类型的函数字面量, 而在class文件中, 它被编译成接收一个scala.Function1类型的参数。

    2 在源码中, doSomething方法中调用了函数字面量 func(4) 。 而在class文件中, 转换成使用scala.Function1类型的对象调用scala.Function1接口的applymcVIsp方法。 之所以说scala..Function1是接口, 是因为调用applymcVIsp方法的字节码指令是invokeinterface 。 也就是说doSomething方法接收的那个参数, 实现了scala.Function1接口。

    分析到这里, doSomething方法就分析完了。

    然后再看doSomething1 方法:

    1 在源码中, doSomething1 函数调用了doSomething函数, 并且传入了一个函数字面量。

    2 在class文件中, doSomething1 函数中使用new字节码指令创建了一个FunctionTest$$anonfundoSomething11类型的对象, 并且使用invokespecial字节码指令调用这个对象的构造方法<init> 。 从反编译输出结果的上面的部分, 可以看到该类的InnerClasses属性, 这个属性描述当前类的内部类:

      InnerClasses:
           public #24; //class FunctionTest$$anonfun$doSomething1$1
    

    可以看到FunctionTest
    anonfundoSomething11类被编译成了当前类的内部类。也就是说编译器自动为当前类生成了内部类FunctionTest
    anonfundoSomething11。

    在调用构造函数初始化这个FunctionTest

    anonfundoSomething11对象之后,又使用invokevirtual指令调用了当前类的doSomething方法,并且把这个新创建的FunctionTestanonfundoSomething11对象作为参数传入了doSomething方法中。

    在上面分析doSomething时, 我们知道doSomething方法有一个scala.Function1接口类型的参数, 从这里不难看出, 生成的内部类FunctionTest$$anonfundoSomething11实现了scala.Function1接口 。

    分析到这里, doSomething1方法的实现就分析完了。

    现在, 我们把注意力集中到自动生成的内部类FunctionTest$$anonfundoSomething11上。 下面反编译这个类:

    javap -c -v -classpath . -private FunctionTest$$anonfun$doSomething1$1
    

    输出结果如下(省略了一些不相关的信息):

    public final class FunctionTest$$anonfun$doSomething1$1 extends scala.runtime.AbstractFunction1$mcVI$sp implements scala.Serializable
      SourceFile: "FunctionTest.scala"
      EnclosingMethod: #9.#12                 // FunctionTest.doSomething1
      InnerClasses:
           public #2; //class FunctionTest$$anonfun$doSomething1$1
        Scala: length = 0x0
     
      minor version: 0
      major version: 50
      flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
    Constant pool:
        ......
        ......
        
    {
      public static final long serialVersionUID;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
        ConstantValue: long 0l
     
     
      public final void apply(int);
        flags: ACC_PUBLIC, ACC_FINAL
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: iload_1
             2: invokevirtual #21                 // Method apply$mcVI$sp:(I)V
             5: return
     
     
      public void apply$mcVI$sp(int);
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: getstatic     #31                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
             3: iload_1
             4: invokestatic  #37                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
             7: invokevirtual #41                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
            10: return
     
     
      public final java.lang.Object apply(java.lang.Object);
        flags: ACC_PUBLIC, ACC_FINAL, ACC_BRIDGE, ACC_SYNTHETIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: aload_1
             2: invokestatic  #46                 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
             5: invokevirtual #48                 // Method apply:(I)V
             8: getstatic     #54                 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
            11: areturn
    }
    

    先从最上面看起, 这个类继承了一个叫做scala.runtime.AbstractFunction1mcVIsp的类, 并没有直接实现scala.Function1接口, 我们猜测scala.runtime.AbstractFunction1mcVIsp类实现了scala.Function1接口 。

    可以看到, 该类中有三个方法(其实还有一个构造方法, 省略掉了), 其中有我们关心的applymcVIsp方法。 现在我们直接分析applymcVIsp方法, 由于这个方法由编译器自动生成, 不存在对应的源码, 所以我们直接分析class文件中的字节码:

    首先使用getstatic指令访问scala/Predef类中的静态字段MODULE

    然后调用scala/runtime/BoxesRunTime类中的boxToInteger静态方法, 这个方法实现将int装箱成Integer 。

    最后调用MODULE$对象的println方法, 打印整型的参数。

    也就是说, 我们定义的函数字面量中的prinln打印逻辑, 是在这个applymcVIsp方法中实现的! 到此为止, 内部类FunctionTest$$anonfundoSomething11也分析完了。

    总结

    到此为止, 我们大概可以知道, 函数字面量在Scala中是如何实现的了。 现在把实现过程总结一下:

    1 如果一个方法接收一个函数字面量作为参数, 那么在编译时把这个参数编译成scala.FunctionN接口类型, 这里的N和参数的个数相同。

    2 如果一个方法中创建了字面量, 比如写上了 (x : Int) => println(x) , 那么就会创建一个内部类, 这个内部类间接实现上述的scala.FunctionN接口, 并且实现一个类似于applymcVIsp的函数(这个函数的参数和返回值, 和函数字面量的参数和返回值相对应)。 然后创建一个这个内部类的对象, 也就是说, 在实现方式上, 函数字面量就是这个内部类对象。

    3 如果将这个字面量传入其他接收字面量的函数中, 相当于把上述的内部类对象传入接收字面量的函数中, 我们已经知道, 接收函数字面量的方法, 被编译成接收scala.FunctionN, 而这个内部类正好实现了这个scala.FunctionN接口, 所以这个代表函数字面量的内部类对象, 正好可以传给接收字面量的方法。

    4 在接收函数字面量的方法中, 如果调用了这个函数字面量, 相当于调用代表该函数字面量的对象的applymcVIsp函数。

    在本例中使用的Scala源码如下:

    class FunctionTest{
        
        def doSomething(func : Int => Unit){
            func(4)
        }
     
        def doSomething1(){
            doSomething( ( (x : Int) => println(x) ) )
        }
    }
    

    如果用java描述的话, 这个过程是这样的(伪代码):

    class FunctionTest {
        
        void doSomething(scala.Function1 arg){
            arg.apply$mcVI$sp(4);
        }
     
        void doSomething1(){
     
            scala.Function1  obj = new FunctionTest$$anonfun$doSomething1$1();
     
            doSomething(obj)l
        }
     
        /*内部类*/
        class FunctionTest$$anonfun$doSomething1$1 impliments scala.Function1{
     
            public void apply$mcVI$sp(int arg){
     
                scala.Predef$.MODULE$.println(new Interger(arg));
     
            }
        }
    }
    

    所以可以得出如下的结论, 在Scala中, 函数字面量虽然作为一个函数,但是在class的底层实现上, 是使用对象实现的。 其中Scalac编译器做了大量的工作, 包括生成对应的内, 创建对应的对应等。

    所以, 请记住, 在你用Scala编程的时候, 编译器也在帮你写代码。

    相关文章

      网友评论

          本文标题:scala函数字面量是如何实现的

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