object MyApp {
def printValues(f: {def apply(x: Int): Int}, from: Int, to: Int): Unit = {
println(
(from to to).map(f(_)).mkString(" ")
)
}
def main(args: Array[String]): Unit = {
val anonfun1 = new Function1[Int, Int] {
final def apply(x: Int): Int = x * x
}
val fun1 = (x:Int)=>x*x
printValues(fun1, 3, 6)
}
}
我认为scala中的lambda函数也是扩展Function1特性的对象。但是,此代码对printValues(fun1, 3, 6)
而非printlnValues(anonfun1, 3, 6)
失败。为什么会这样?
答案 0 :(得分:8)
这是一个非常有趣的问题。有一种说法,一个人不应该依赖于代码中的实现细节,我认为这是做到这一点的边缘。
让我们尝试分解这里发生的事情。
当您需要结构类型时,例如您在此方法中所做的:
def printValues(f: {def apply(x: Int): Int}, from: Int, to: Int): Unit = {
println(
(from to to).map(f(_)).mkString(" ")
)
}
Scala所做的是使用反射在运行时尝试查找apply
方法,并动态调用它。它被翻译成如下所示:
public static Method reflMethod$Method1(final Class x$1) {
MethodCache methodCache1 = Tests$$anonfun$printValues$1.reflPoly$Cache1.get();
if (methodCache1 == null) {
methodCache1 = (MethodCache)new EmptyMethodCache();
Tests$$anonfun$printValues$1.reflPoly$Cache1 = new SoftReference((T)methodCache1);
}
Method method1 = methodCache1.find(x$1);
if (method1 != null) {
return method1;
}
method1 = ScalaRunTime$.MODULE$.ensureAccessible(x$1.getMethod("apply", (Class[])Tests$$anonfun$printValues$1.reflParams$Cache1));
Tests$$anonfun$printValues$1.reflPoly$Cache1 = new SoftReference((T)methodCache1.add(x$1, method1));
return method1;
}
这是发出的反编译Java代码。长话短说,它寻找apply
方法。
对于2.12之前的任何Scala版本,声明匿名函数会导致编译器生成一个扩展AbstractFunction*
的类,其中*
是函数的arity。这些抽象函数类依次继承Function*
,并使用lambda的实现实现它们的apply
方法。
例如,如果我们接受你的表达:
val fun1 = (x:Int) => x * x
编译器为我们发出:
val fun2: Int => Int = {
@SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractFunction1$mcII$sp with Serializable {
def <init>(): <$anon: Int => Int> = {
$anonfun.super.<init>();
()
};
final def apply(x: Int): Int = $anonfun.this.apply$mcII$sp(x);
<specialized> def apply$mcII$sp(x: Int): Int = x.*(x)
};
(new <$anon: Int => Int>(): Int => Int)
};
()
当我们查看字节码级别时,我们会看到生成的匿名类及其apply
方法:
Compiled from "Tests.scala"
public final class othertests.Tests$$anonfun$1 extends scala.runtime.AbstractFunction1$mcII$sp implements scala.Serializable {
public static final long serialVersionUID;
public final int apply(int);
Code:
0: aload_0
1: iload_1
2: invokevirtual #21 // Method apply$mcII$sp:(I)I
5: ireturn
public int apply$mcII$sp(int);
Code:
0: iload_1
1: iload_1
2: imul
3: ireturn
So what happens when you request the `apply` method at runtime? The run-time will see that theirs a method defined on `$anonfun` called `apply` which takes an `Int` and returns an `Int`, which is exactly what we want and invoke it. All is good and everyone's happy.
在Scala 2.12中,我们得到了一种称为SAM转换的东西。 SAM类型是Java 8中的一项功能,它允许您缩写实现接口,而是提供lambda表达式。例如:
new Thread(() -> System.out.println("Yay in lambda!")).start();
而不是必须实施Runnable
并覆盖public void run
。
Scala 2.12设定了一个目标,即在可能的情况下通过SAM转换与SAM类型兼容。
在我们的特定情况下,可以进行SAM转换,这意味着我们会获得Function1[Int, Int]
的专用版本而不是Scala scala.runtime.java8.JFunction1$mcII$sp
。此JFunction
与Java兼容,并具有以下结构:
package scala.runtime.java8;
@FunctionalInterface
public interface JFunction1$mcII$sp extends scala.Function1, java.io.Serializable {
int apply$mcII$sp(int v1);
default Object apply(Object t) { return scala.runtime.BoxesRunTime.boxToInteger(apply$mcII$sp(scala.runtime.BoxesRunTime.unboxToInt(t))); }
}
这个JFunction1
已被专门化(就像我们在Scala中使用@specialized注释一样)为def apply(i: Int): Int
发出特殊方法。请注意这里的一个重要因素,即此方法仅实现apply
形式的Object => Object
方法,而不是Int => Int
。现在我们可以开始了解为什么这可能会有问题。
现在,当我们在Scala 2.12中编译相同的示例时,我们看到:
def main(args: Array[String]): Unit = {
val fun2: Int => Int = {
final <artifact> def $anonfun$main(x: Int): Int = x.*(x);
((x: Int) => $anonfun$main(x))
};
()
我们不再看到扩展AbstractFunction*
的方法,我们只看到名为$anonfun$main
的方法。当我们查看生成的字节码时,我们会在内部看到它会调用JFunction1$mcII$sp.apply$mcII$sp(int v1);
:
public void main(java.lang.String[]);
Code:
0: invokedynamic #41, 0 // InvokeDynamic #0:apply$mcII$sp: ()Lscala/runtime/java8/JFunction1$mcII$sp;
5: astore_2
6: return
然而,如果我们自己明确地扩展Function1
并实现apply
,我们会得到与之前的Scala版本类似的行为,但不是完全相同:
def main(args: Array[String]): Unit = {
val anonfun1: Int => Int = {
final class $anon extends AnyRef with Int => Int {
def <init>(): <$anon: Int => Int> = {
$anon.super.<init>();
()
};
final def apply(x: Int): Int = x.*(x)
};
new $anon()
};
{
()
}
}
我们不再扩展AbstractFunction*
,但我们确实有一个apply
方法,它在运行时满足结构类型条件。在字节码级别,我们看到int apply(int)
,object apply(object)
以及注释Function*
的@specialization属性的一堆案例:
public final int apply(int);
Code:
0: aload_0
1: iload_1
2: invokevirtual #183 // Method apply$mcII$sp:(I)I
5: ireturn
public int apply$mcII$sp(int);
Code:
0: iload_1
1: iload_1
2: imul
3: ireturn
public final java.lang.Object apply(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: invokestatic #190 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
5: invokevirtual #192 // Method apply:(I)I
8: invokestatic #196 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
11: areturn
我们可以看到,在某些情况下,Scala编译器如何处理lambda表达式的实现细节发生了变化。这是一个错误吗?我的感觉倾向于没有。在任何地方,Scala规范都不能保证需要一个名为apply
的方法与lambda的签名匹配,这就是我们称之为实现细节的原因。虽然这确实是一个有趣的怪癖,但我不会在任何生产环境中依赖这些代码,因为它可能会发生变化。
答案 1 :(得分:2)
我认为我并没有完全弄清楚问题是什么。 您的代码似乎也可以正常工作。
但该行
如果Scala版本是&lt;我认为scala中的lambda函数也是扩展Function1
的对象
是正确的。 2.12
scala中的函数文字只是FunctionN的一个语法糖,它是Function1到Function22。 所以下面的代码完全相同。
val f: Int=> Int = new Function1[Int, Int] {
def apply(x:Int): Int = x * x
}
val f2: Int => Int = (x: Int) => x * x
如果你想检查它们都是FunctionN,那么你的代码printValues
应该是这样的
def printValues(f: (Int) => Int, from: Int, to: Int): Unit = {
println((from to to).map(f).mkString(" "))
}
//or
def printValues(f: Function1[Int,Int], from: Int, to: Int): Unit = {
println((from to to).map(f).mkString(" "))
}
再次,由于语法糖,它们是相同的含义。
您对参数类型的定义是AnyRef{def apply(x: Int): Int}
所以
object Foo {
def apply(x: Int): Int = x * x
}
可用于printValues
。
如果您正在使用Scala 2.12,那就有点不同了。
如果您的函数文字满足某些条件,它将自动转换为SAM类型。 有关详细信息,请查看此sam-conversion
<强> EDITED 强>
您必须使用2.12,因为如果代码在2.11或者预告片上,代码可以正常工作。
就像我在上一次提到的那样,这都是因为sam转换。
将printValues
的参数修改为(Int) => Int
或Function1[Int,Int]
以明确定义以使用FunctionN
答案 2 :(得分:2)
在结构类型的实例上调用方法涉及运行时反射。
对于val f: { def apply(i: Int): Int} = (x: Int) => x * x
,来电f(10)
(或f.apply(10)
)已解析为f.getClass.getMethod("apply", classOf[Int]).invoke(f, 10)
。
并且一个lambda在Scala 2.11.8中有这样的方法(并且Function1
在2.12.1中仍然有它,所以你的代码可以与anonfun1
一起使用):
scala> val f: { def apply(i: Int): Int } = (x: Int) => x * x
f: AnyRef{def apply(i: Int): Int} = <function1>
scala> f.getClass.getMethods.filter(_.getName == "apply") foreach println
public final int $line11.$read$$iw$$iw$$anonfun$1.apply(int)
public final java.lang.Object $line11.$read$$iw$$iw$$anonfun$1.apply(java.lang.Object)
但是在2.12.1中它只有一个方法需要Object
:
scala> val f: { def apply(i: Int): Int } = (x: Int) => x * x
f: AnyRef{def apply(i: Int): Int} = $$Lambda$1015/1912769093@364b1061
scala> f.getClass.getMethods.filter(_.getName == "apply") foreach println
public default java.lang.Object scala.runtime.java8.JFunction1$mcII$sp.apply(java.lang.Object)
当然可以调用它:
scala> f.getClass.getMethod("apply", classOf[AnyRef]).invoke(f, Int.box(10)).asInstanceOf[Int]
res2: Int = 100
但不适用于标准结构类型方法调用。
我相信这是Scala标准库中的一个错误。 2.12.1中的Lambdas由scala.runtime.java8.JFunction*
类的实例表示。例如:
@FunctionalInterface
public interface JFunction1$mcDF$sp extends scala.Function1, java.io.Serializable {
double apply$mcDF$sp(float v1);
default Object apply(Object t) { return scala.runtime.BoxesRunTime.boxToDouble(apply$mcDF$sp(scala.runtime.BoxesRunTime.unboxToFloat(t))); }
}
我没有看到为什么那些没有专门方法的原因,类似于为scala.Function*
特征生成的方法:
@FunctionalInterface
public interface JFunction1$mcDF$sp extends scala.Function1, java.io.Serializable {
double apply$mcDF$sp(float v1);
default Object apply(Object t) { return scala.runtime.BoxesRunTime.boxToDouble(apply$mcDF$sp(scala.runtime.BoxesRunTime.unboxToFloat(t))); }
default double apply(float t) { return apply$mcDF$sp(t); }
}