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)
}
}
Run Code Online (Sandbox Code Playgroud)
我认为scala中的lambda函数也是扩展Function1特性的对象.但是这个代码失败了printValues(fun1, 3, 6),而不是printlnValues(anonfun1, 3, 6).为什么会这样?
这是一个非常有趣的问题需要探索.有一种说法,一个人不应该依赖于代码中的实现细节,我认为这是做这件事的边缘.
让我们尝试分解这里发生的事情.
当您需要结构类型时,例如在此方法中执行的操作:
def printValues(f: {def apply(x: Int): Int}, from: Int, to: Int): Unit = {
println(
(from to to).map(f(_)).mkString(" ")
)
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
这是发出的反编译Java代码.长话短说,它寻找apply方法.
对于2.12之前的任何Scala版本,声明匿名函数会导致编译器生成一个类扩展AbstractFunction*,其中*是函数的arity.这些抽象函数类依次继承Function*,并通过applylambda的实现实现它们的方法.
例如,如果我们接受你的表达:
val fun1 = (x:Int) => x * x
Run Code Online (Sandbox Code Playgroud)
编译器为我们发出:
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)
};
()
Run Code Online (Sandbox Code Playgroud)
当我们查看字节码级别时,我们会看到生成的匿名类及其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.
Run Code Online (Sandbox Code Playgroud)
在Scala 2.12中,我们得到了一种称为SAM转换的东西.SAM类型是Java 8中的一项功能,它允许您缩写实现接口,而是提供lambda表达式.例如:
new Thread(() -> System.out.println("Yay in lambda!")).start();
Run Code Online (Sandbox Code Playgroud)
而不是必须实现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))); }
}
Run Code Online (Sandbox Code Playgroud)
这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))
};
()
Run Code Online (Sandbox Code Playgroud)
我们不再看到方法扩展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
Run Code Online (Sandbox Code Playgroud)
然而,如果我们明确地扩展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()
};
{
()
}
}
Run Code Online (Sandbox Code Playgroud)
我们不再扩展AbstractFunction*,但我们确实有一种apply方法可以满足运行时的结构类型条件.在字节码级别,我们看到@specialization属性注释的一个int apply(int),一个object apply(object)和一堆案例Function*:
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
Run Code Online (Sandbox Code Playgroud)
我们可以看到Scala编译器在某些情况下如何处理lambda表达式的实现细节发生了变化.这是一个错误吗?我的感觉倾向于没有.在Scala规范中没有确保需要一个apply与lambda的签名匹配的名称的方法,这就是我们称之为实现细节的原因.虽然这确实是一个有趣的怪癖,但我不会在任何类型的生产环境中依赖这些代码,因为它可能会发生变化.
| 归档时间: |
|
| 查看次数: |
473 次 |
| 最近记录: |