体验 Scala 2.12 支持的 Java 8 风格(SAM) Lambda

上一次关注 Scala 新版本特性还是在将近五年前,针对的是  Scala 2.10. 后来也一直在使用 Scala,基本上是 Scala 2.11,但对 Scala 2.11 所带来的新特性基本无知,大约有个 Macro 功能,没什么机会用上,应用 sbt 时稍有接触。还是老句老话,了解新特性最可靠的文档是每个版本的的 Release Notes, 比如 Scala 2.12.0 Release Notes.

其中 Scala 2.12 带来的主要特性在于对 Java 8 的充分支持:

  1. Scala 可以有方法实现的 trait 直接编译为带默认方法的 Java 接口
  2. Lambda 表达式无需生成相应的类,而是用到 invokedynamic 字节码指令(这个是 Java 7 加进来的新指令)
  3. 最方便的功能莫过于终于支持 Java  8 风格的 Lambda,即功能性接口的 SAM(Single Abstract Method)

Scala 的 Lambda 内部实现

这儿主要是体验 Scala 2.12 如何使用 Java 8 风格的 Lambda. 在 Scala 2.12 之前,Scala 对 Lambda 的支持是为你准备了一大堆的 trait 类,有

  1. Function0, Function1, ...... Function22 (接收多个参数,返回一个值)
  2. Product1, Product2, ...... Product22 (函数返回多个值,即 TupleX 时用的)

类似的 Scala Tuple 的实现也是一堆的 Tuple1, Tuple2, ...... Tuple22.

Scala 并不需要像 Java 那样区分 Function, Producer 和 Consumer,因为 Scala 的函数没有严格意义上区分是否有返回值,没有就是 Unit。

我们来看一下在 Scala 任意写一个 Lambda 生成了什么样的代码

如果查看在 sbt 项目的 target/scala-2.11/classes 中生成的字节码文件,发现上面一行生成的大致等效的代码如下(忽略细节)

AbstractFunction2 是 scala.runtime 包中的类,它继承自 Function2 特质。

Scala 2.12 之前对 SAM 的支持

但是对于想在 Scala 2.12 之前使用 Java 8 的功能性接口,写出来比 Java 8 还麻烦,必须要写成匿名类(和没有 Lambda 的 Java 一样)。因为 Scala 2.12 才是为 Java 8 而生的。

比如对于简单的声明一个 java.lang.Runnable 实例, 它是一个功能性接口,用 Java 8 可以这样写

而 Scala 2.11 想要简单的写成如下方式是不行的

正常情况下在 Scala 2.11 中创建一个 Runnable 实例需要这么写

是不是有一种似曾相识的感觉,和 Java 8 之前的写法如出一辙, 一点也没占上 Scala Lambda 的光。

其实 Scala 2.11 也是可以支持 Java 8 的 SAM 的,但只是一个实验性的特性,需要打开编译选项 -Xexperimental, 可以通过在 sbt 的 build.sbt 文件中加上一行

来开启实验特性。同时若要让 IntelliJ IDEA 识别支持 SAM 的语法,也需要 IntelliJ IDEA 的 Perferences/../Scala Compiler 中勾选上 Experimental Features 选项框。如此在 Scala 2.11 中也可以写成

上面方法声明的 Runnable 与写成匿名类的方式是一样的了,内部也是实现为 Runnable 的匿名类。

这么一个简单例子打开实验选项来支持 SAM 还是可以的,但是在我经历的一个实际使用 Scala 2.11 的项目中试图打开实验选项却造成项目无法编译。所以实验性的东西需谨慎使用; 譬如说 Spark 2.2 官方并未声明能支持 Scala 2.12 的话,脱了裤子强行上就会有风险。

Scala 2.12 对 SAM 的支持

现在已经没有惊喜了,就是在 Scala 2.11 中打开了 -Xexperimental 选项时的书写方式

只是到了在 Scala 2.12 中,val runnable: Runnable = () => {} 不再是生成一个实现了 Runnable 接口的匿名类,而是产生如下的字节码

实战自定义的 SAM

在 Scala 中我们可以定义一个抽象类或 trait

那么要声明一个 F2 实例的写法可以用

如果不指定变量类型,只是声明为

那么 f2 只是一个 Function2 实例,因为没有上下文。这个和 Java 8 的 Lambda 类型推断差不多,只是 Scala 在无法确定类型的时候还那么多默认的 Function0, Function1, ...... Function22 可用。

同样,我们在给类型提供上下文时,比如方法参数的类型,也可以简化成 Java 8 那样的写法

说白了,就是 Java 8 怎么支持 SAM 的 Lambda, Scala 2.12 也是同样的语法风格,唯一不同的是参数列表与实现的分隔符分别是 -> 与 =>

链接:1. Try Experimental SAM in Scala Plugin 1.7 for IntelliJ IDEA 15 EAP

本文链接 https://yanbin.blog/scala-2-12-java-8-sam-lambda/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

5 Comments
Inline Feedbacks
View all comments
wayne
wayne
6 years ago

博主,有个疑问,completablefuture和callable的差别是什么?两个都有get方法,不阻塞主要体现在哪里呢?

wayne
wayne
6 years ago
Reply to  Yanbin

我可能有点问错问题了,其实我想问的是,callable在实现类放到executor线程池中执行,可以返回feature的返回值,completablefuture也是也是可以指定放到executor中执行,返回的是completablefuture的返回值,两者其实都是异步方式执行,那这两种那个方式更优,差别在哪里呢?谢谢

wayne
wayne
6 years ago
Reply to  Yanbin

哦,这样的,是说呢,我看来着,感觉用的方式是差不多,我开始是用callable写的东西,多线程去执行,看到这个completablefuture后,是新出的api,想换这个来重写,看来我需要重新考虑下。