我的理解是,所有未捕获的lambda都不需要在使用站点创建对象,因为它可以创建为静态字段并重复使用。原则上,构成类方法调用的lambdas也是如此——只有字段是非静态的。事实上,我从未尝试过深入研究;现在我在看字节码,在封闭类中看不到字节码,也不知道在哪里看?不过,我看到lambda工厂与Java中的不同,所以这应该有一个明确的答案——至少对于给定的Scala版本来说是这样。
我的动机很简单:分析非常耗时。引入方法值(或者通常情况下,lambdas只捕获封闭对象的状态(作为私有类字段比内联编写方法值更不干净,工作量更大,而且通常也不是好的代码。但是,当编写已知(极有可能(成为热点的区域时,这是一个非常简单的优化,可以直接执行,不会对程序员的时间产生任何实际影响。但是,如果无论如何都没有创建新对象,这就没有意义了。
举个例子:
def alias(x :X) = aliases.getOrElse(x, x)
def alias2(x :X) = aliases.getOrElse(x, null) match {
case null => x
case a => a
}
第一个lambda(一个Function0
(必须是一个新对象,因为它捕获方法参数x
,而第二个lambda返回一个常量(null
(,因此实际上不必这样做。它也不像私有类字段那样混乱(IMO(,后者会污染名称空间,但我希望能够确定-或者有一种方法可以轻松地确认我的期望。
以下证明了至少在某些时候,答案是"否":
scala 2.13.4> def foo = () => 1
def foo: () => Int
scala 2.13.4> foo eq foo
val res5: Boolean = true
查看此代码生成的字节码:
import scala.collection.immutable.ListMap
object ByName {
def aliases = ListMap("Ein" -> "One", "Zwei" -> "Two", "Drei" -> "Three")
val default = "NaN"
def alias(x: String) = aliases.getOrElse(x, x)
def alias2(x: String) = aliases.getOrElse(x, null) match {
case null => x
case a => a
}
def alias3(x: String) = aliases.getOrElse(x, default)
}
编译器为by name参数生成静态方法。它们看起来像这样:
public static final java.lang.String $anonfun$alias$1(java.lang.String);
Code:
0: aload_0
1: areturn
public static final scala.runtime.Null$ $anonfun$alias2$1();
Code:
0: aconst_null
1: areturn
public static final java.lang.String $anonfun$alias3$1();
Code:
0: getstatic #26 // Field MODULE$:LByName$;
3: invokevirtual #138 // Method default:()Ljava/lang/String;
6: areturn
对于编译器来说,最简单的方法是生成实现Function0接口的匿名类。然而,这会导致字节码膨胀。相反,编译器通过invokedynamic
指令将创建这些匿名内部类推迟到运行时。
Scala究竟是如何使用这些invokedynamic
指令的,我不知道。他们可能会以某种方式缓存生成的Function0
对象,但我的猜测是invokedynamic
调用已经充分优化,每次只生成一个新对象会更快。分配寿命短的物品很便宜,而且成本往往被高估。如果重用现有对象意味着缓存未命中,那么重用现有对象甚至可能比创建新对象慢。
我还想指出,这是一个实现细节,随时可能发生变化。Scala编译器开发人员和JVM开发人员知道他们在做什么,所以你最好相信他们的实现很好地平衡了性能。