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文件:
和源码中的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接口的apply$mcVI$sp方法。 之所以说scala..Function1是接口, 是因为调用apply$mcVI$sp方法的字节码指令是invokeinterface 。 也就是说doSomething方法接收的那个参数, 实现了scala.Function1接口。
分析到这里, doSomething方法就分析完了。
然后再看doSomething1 方法:
1 在源码中, doSomething1 函数调用了doSomething函数, 并且传入了一个函数字面量。
2 在class文件中, doSomething1 函数中使用new字节码指令创建了一个FunctionTest$$anonfun$doSomething1$1类型的对象, 并且使用invokespecial字节码指令调用这个对象的构造方法<init> 。 从反编译输出结果的上面的部分, 可以看到该类的InnerClasses属性, 这个属性描述当前类的内部类:
InnerClasses: public #24; //class FunctionTest$$anonfun$doSomething1$1
可以看到 FunctionTest$$anonfun$doSomething1$1类被编译成了当前类的内部类。 也就是说编译器自动为当前类生成了内部类FunctionTest$$anonfun$doSomething1$1。
在调用构造函数初始化这个FunctionTest$$anonfun$doSomething1$1对象之后, 又使用invokevirtual指令调用了当前类的doSomething方法, 并且把这个新创建的FunctionTest$$anonfun$doSomething1$1对象作为参数传入了doSomething方法中。
在上面分析doSomething时, 我们知道doSomething方法有一个scala.Function1接口类型的参数, 从这里不难看出, 生成的内部类FunctionTest$$anonfun$doSomething1$1实现了scala.Function1接口 。
分析到这里, doSomething1方法的实现就分析完了。
现在, 我们把注意力集中到自动生成的内部类FunctionTest$$anonfun$doSomething1$1上。 下面反编译这个类:
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.AbstractFunction1$mcVI$sp的类, 并没有直接实现scala.Function1接口, 我们猜测scala.runtime.AbstractFunction1$mcVI$sp类实现了scala.Function1接口 。
可以看到, 该类中有三个方法(其实还有一个构造方法, 省略掉了), 其中有我们关心的apply$mcVI$sp方法。 现在我们直接分析apply$mcVI$sp方法, 由于这个方法由编译器自动生成, 不存在对应的源码, 所以我们直接分析class文件中的字节码:
首先使用getstatic指令访问scala/Predef$类中的静态字段MODULE$
然后调用scala/runtime/BoxesRunTime类中的boxToInteger静态方法, 这个方法实现将int装箱成Integer 。
最后调用MODULE$对象的println方法, 打印整型的参数。
也就是说, 我们定义的函数字面量中的prinln打印逻辑, 是在这个apply$mcVI$sp方法中实现的! 到此为止, 内部类FunctionTest$$anonfun$doSomething1$1也分析完了。
总结
到此为止, 我们大概可以知道, 函数字面量在Scala中是如何实现的了。 现在把实现过程总结一下:
1 如果一个方法接收一个函数字面量作为参数, 那么在编译时把这个参数编译成scala.FunctionN接口类型, 这里的N和参数的个数相同。
2 如果一个方法中创建了字面量, 比如写上了 (x : Int) => println(x) , 那么就会创建一个内部类, 这个内部类间接实现上述的scala.FunctionN接口, 并且实现一个类似于apply$mcVI$sp的函数(这个函数的参数和返回值, 和函数字面量的参数和返回值相对应)。 然后创建一个这个内部类的对象, 也就是说, 在实现方式上, 函数字面量就是这个内部类对象。
3 如果将这个字面量传入其他接收字面量的函数中, 相当于把上述的内部类对象传入接收字面量的函数中, 我们已经知道, 接收函数字面量的方法, 被编译成接收scala.FunctionN, 而这个内部类正好实现了这个scala.FunctionN接口, 所以这个代表函数字面量的内部类对象, 正好可以传给接收字面量的方法。
4 在接收函数字面量的方法中, 如果调用了这个函数字面量, 相当于调用代表该函数字面量的对象的apply$mcVI$sp函数。
在本例中使用的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编程的时候, 编译器也在帮你写代码。
本文基于分析class字节码来分析scala, 对class文件格式不熟悉的同学, 可以参考我的专栏: 深入理解Java语言 。