建议:阅读此文需要Java8及更高版本的基础
问题
Scala中,通常函数(Function)指的是一段代码逻辑的集合,可以有输入和输出,通常在开发中用来封装一段逻辑或者功能。方法(Method)是函数的特殊形式,指的是类或者对象中定义的函数成员。体现在Scala的语法层面就是val
关键字定义的是函数,def
关键字定义的是方法。那么问题来了,在Scala中,val
和def
定义的“Function
”有啥区别?
val isEvenVal = (i: Int) => i % 2 == 0
def isEvenDef(i: Int) = i % 2 == 0
注意:下文没有特殊说明时,
Function
指的是val
定义的函数,Method
指def
定义的方法。
区别
Method在每次被调用时先被实例化成“Function”(new function
)再进行计算。Function则在定义时计算。
val
evaluates when defined,def
when called:
举个例子
val test1: () => Int = {
val r = util.Random.nextInt
() => r
}
// 运行 val test1
test1() //-761561683
test1() //-761561683
test1() //-761561683
def test2: () => Int = {
val r = util.Random.nextInt
() => r
}
// 运行 def test2
test2() //1228034657
test2() //1312215631
test2() //310909645
test1
的返回结果每次执行的的输出都是固定的,说明在test1
被定义的时候,结果已经固定了。而test2
返回的结果每次执行后的输出都是不一样的,说明被调用时才被实际执行。
说到这里有牵扯出了两个细节问题,第一个是闭包,也就是test1
的现象。第二个是Method可以被转换成Function来进行调用或者传递。关于第二个问题简单提一嘴,其实利用Scala的占位句法,很容易手动把Method转成Function,比如下面的例子:
// def 定义方法addOne
def addOne(a: Int): Int = a + 1
// 转成Function,然后传值调用
val addOneFunc = addOne _
addOneFunc(5) // 6
后续的文章会详细进行讨论函数的各种使用。
探索
def
为什么会有上面提到的现象,造成的原因是什么?下面我们来简单进行下探索。Scala也是jvm方言,所以我们可以通过把Scala的二进制文件反编译成Java来观察,就可以很容易的得出结论。
先定义一个类DesTest
,类里面有个方法add1
,如下:
class DefTest {
def add1(a: Int) = a + 1
}
然后使用scalac
,编译并输出:
$ scalac -Xprint:all DefTest.scala
package <empty> {
class DefTest extends Object {
def add1(a: Int): Int = a.+(1); //<-- 定义的方法在这里
def <init>(): DefTest = {
DefTest.super.<init>();
()
}
}
}
然后再使用javap
对编译出的class文件输出其结构:
$ javap DefTest
Compiled from "DefTest.scala"
public class DefTest {
public int add1(int); //<-- 定义的方法在这里
public DefTest();
}
简单观察其实与平时的java类和方法并无大的差别。再使用jad
进行反编译,输出详细的java代码如下。可以看到,与我们平时写的java代码并无区别,也就是说,使用def关键字定义的方法在java中就是常规的方法。
public class DefTest {
public int add1(int a) {
return a + 1;
}
public DefTest() {}
}
val
先定义一个类ValTest
,里面继续包含一个方法add1
,使用val关键字申明。
class ValTest {
val add1 = (a: Int) => a + 1
}
同样使用scalac
编译并输出:
ValTest.class
ValTest$$anonfun$1.class
这里已经和上面有所不同了,我们继续使用javap
来观察:
$ javap ValTest
Compiled from "ValTest.scala"
public class ValTest {
public scala.Function1<java.lang.Object, java.lang.Object> add1();
public ValTest();
}
可以看到,此时add1
在java中还是一个方法,但是返回值却上刚才有了很大的不同。变成了:scala.Function1<java.lang.Object, java.lang.Object>
,对应的是scala的Function1
特质。
关于特质,可以简单理解为一种特殊的接口
最后,来看下反编译的代码:
import scala.Function1;
import scala.Serializable;
import scala.runtime.BoxesRunTime;
public class ValTest {
public Function1 add1() {
return add1;
}
public ValTest() {}
private final Function1 add1 = new Serializable() {
public final int apply(int a) {
return apply$mcII$sp(a);
}
public int apply$mcII$sp(int a) {
return a + 1;
}
public final volatile Object apply(Object v1) {
return BoxesRunTime.boxToInteger(apply(BoxesRunTime.unboxToInt(v1)));
}
public static final long serialVersionUID = 0L;
};
}
可以看到:
-
ValTest
中也有一个方法add1
,返回Function1
的实例 - 同时又有一个私有的实例字段
add1
,对应的为scala.Serializable
接口的匿名实现。 - 而实例
add1
的apply
方法中,调用了apply$mcII$sp
方法,该方法中会包含scala源码中add1
方法的具体实现。 - 最下面,又有一个
apply
的重载形式,为实际调用的具体入口,包含了scala的自动拆箱和装箱
结论
到这里就基本都清楚了,def
在类中定义的Method
对应的就是java中的方法,而val
定义的则对应的是scala.Serializable
接口实现,每次具体调用是都调用的是该实现的入口方法apply
。所以,在val
定义的add1
可以在scala中理解为一个字段(Field)。所以这也就解释了为什么val
在定义时计算,因为已经被实例化了。