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