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

2021-01-22  本文已影响0人  tracy_668

[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编程的时候, 编译器也在帮你写代码。

上一篇下一篇

猜你喜欢

热点阅读