Neal Ford在几年前提出的“Poly Programming”思想,已经逐渐成为主流。这种思想并非是为了炫耀多语言的技能,然后选择“高大上”。真正的目的在于更好地利用各种语言处理不同场景、不同问题的优势。
由于都运行在JVM上,Java与Scala之间基本能做到无缝的集成,区别主要在于各自的API各有不同。由于Scala为集合提供了更多便捷的函数,因此,Java与Scala在集合之间的互操作,或许是在这种多语言平台下使用最为频繁的。
Scala中操作Java集合
两种情况需要在Scala中操作Java集合。一种是Scala调用了其他的Java库,针对Java集合需要转换为Scala集合,如此才能享受Scala集合提供的福利;另一种是编写了Scala程序,但需要提供给Java库,为了更好地无缝集成,要让Java库体会不到Scala的存在。
Scala调用Java库
为了享用Scala提供的集合特性,在Scala程序中若要调用Java库,通常需要将其转换。例如,JavaXmlConfigure为一个Java类,它的readSoftInfos()方法返回的是一个Java的List。现在,我在Scala中调用该方法(这里以ScalaTest编写的测试来表现Scala程序):
class XmlConfigureSpec extends FlatSpec with ShouldMatchers {
it should "load all package soft nodes for version config" in {
val configure = new JavaXmlConfigure
val result = configure.readSoftInfos("/config.xml", "version number")
result.foreach {
softInfo => println(softInfo)
}
}
}
|
这时,编译器会提示无法找到result的foreach方法。因为这里的result的类型为java.util.List。若要将其转换为Scala的集合,就需要增加如下语句:
import scala.collection.JavaConversions._
|
注意,经过隐式转换后,这里的result类型为Seq[SoftInfo]。如果像下面这样显式指定为Scala的List或Set类型,则无法转换:
val result:Set[SoftInfo] = configure.readSoftInfos("/config.xml", "version number") //or
val result:List[SoftInfo] = configure.readSoftInfos("/config.xml", "version number")
|
Scala的代码以Java库的形式提供给Java调用者
在JVM平台下进行多语言开发时,多数情况下会以Java为主,而对于一些特定场景,能够更好发挥Scala特性的,例如并发处理等,则会选择Scala。此时,若要做到对Java友好,则对于Scala的方法返回值,应尽量屏蔽Scala的类型信息。
举例来说,我用Scala来读取一个配置文件,并对配置文件进行解析和转换,得到一个Scala的Seq集合对象,如下代码所示:
class XmlConfigure {
def readSoftInfos(configFileName: String, version: String) = {
val document = XML.load(getClass.getResource(configFileName))
val pkgSoftNodes = document \\ "PKGSOFT"
val softInfoNodes = pkgSoftNodes.filter(node => node.attributes.get("version").mkString.equalsIgnoreCase(version))
(softInfoNodes \\ "SOFTINFO").map {
softInfoNode => {
val attributes = softInfoNode.attributes
new SoftInfo(attributes.get("fileName").mkString,
attributes.get("softType").mkString,
attributes.get("softUseType").mkString,
attributes.get("size").mkString.toLong)
}
}
}
}
|
如上的readSoftInfos方法返回的是对xml节点进行map的结果,类型为scala的Seq[SoftInfo]。倘若Java代码需要调用这个方法,则还需要对其进行转换,即要求调用者必须具备Scala的知识,这未必友好。
那么应该怎样改善呢?直接的做法就是让readSoftInfos方法返回Java的List,这时候需要使用Scala提供的隐式转换:
import scala.collection.JavaConversions._
class XmlConfigure {
def readSoftInfos(configFileName: String, version: String) : java.util.List[SoftInfo] = {
val document = XML.load(getClass.getResource(configFileName))
val pkgSoftNodes = document \\ "PKGSOFT"
val softInfoNodes = pkgSoftNodes.filter(node => node.attributes.get("version").mkString.equalsIgnoreCase(version))
(softInfoNodes \\ "SOFTINFO").map {
softInfoNode => {
val attributes = softInfoNode.attributes
new SoftInfo(attributes.get("fileName").mkString,
attributes.get("softType").mkString,
attributes.get("softUseType").mkString,
attributes.get("size").mkString.toLong)
}
}
}
|
此时,只需要导入scala.collection.JavaConversions._,我们并不需要将map返回的Seq显式地转换为java.util.List。对于Java的调用者而言,可以直接认为XmlConfigure就是一个Java类。
Java中操作Scala集合
Java要调用Scala代码,而不幸的,这个需要调用的Scala代码不够体贴,直接返回了Scala的集合类型。由于Java不提供自定义隐式转换的功能,因此,只能调用Scala提供的转换类进行显式转换。例如Scala中的XmlConfigure类,其readSoftInfos()返回的是Scala的Seq:
import scala.collection.JavaConversions;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
public class XmlConfigureJavaTest {
@Test
public void should_load_xml_file() {
XmlConfigure xmlConfigure = new XmlConfigure();
List<SoftInfo> softInfos = JavaConversions.asJavaList(xmlConfigure.readSoftInfos("/config.xml", "version number"));
assertThat(softInfos.size(), is(7));
}
}
|
在readSoftInfos()函数返回的为Scala集合类型的情况下,若不进行显示转换,则无法通过编译。
Scala的隐式转换
Scala对Java集合与Scala集合之间的互相转换都用到了Scala提供的隐式转换功能。我们导入的JavaConversions就是承担这种转换的一个Facade Object。它扩展了两个trait:WrapAsScala和WrapAsJava。在JavaConversions对象中定义的方法实际上是将请求委派自它继承的trait的隐式转换函数。例如将Seq转换为java的List:
object JavaConversions extends WrapAsScala with WrapAsJava {
def asJavaList[A](b : Seq[A]): ju.List[A] = seqAsJavaList[A](b)
}
|
seqAsJavaList就是定义在WrapAsJava中的隐式转换函数。在这个函数中又作了一个模式匹配。如果匹配JListWrapper,则调用传入的wrapped参数的asInstanseOf进行类型转换;否则,就将该seq作为参数传递给包装器SeqWrapper。包装器SeqWrapper是Scala定义的样例类(case class),扩展自Java的AbstractList:
//WrapAsJava
import java.{ lang => jl, util => ju }, java.util.{ concurrent => juc }
import scala.language.implicitConversions
trait WrapAsJava {
import Wrappers._
implicit def seqAsJavaList[A](seq: Seq[A]): ju.List[A] = seq match {
case JListWrapper(wrapped) => wrapped.asInstanceOf[ju.List[A]]
case _ => new SeqWrapper(seq)
}
}
//Wrappers
import java.{ lang => jl, util => ju }, java.util.{ concurrent => juc }
import WrapAsScala._
import WrapAsJava._
private[collection] trait Wrappers {
case class SeqWrapper[A](underlying: Seq[A]) extends ju.AbstractList[A] with IterableWrapperTrait[A] {
def get(i: Int) = underlying(i)
}
}
|
隐式转换与扩展方法
在前面我们提到,在Scala中如果导入了JavaConversions,那么即使得到的是Java的List对象,我们仍然可以对其调用foreach函数。即如下代码:
val result = configure.readSoftInfos("/config.xml", "version number")
result.foreach {
softInfo => println(softInfo)
}
|
若为result加上类型,应该会更清晰:
Liquid error: invalid byte sequence in US-ASCII
显然,这里的result为java.util.List类型,为何却可以调用foreach函数呢?这种形式让我想起C#提供的扩展方法。例如在C# 3.0之前的集合类型,如List,并没有例如first(),where()等方法,但通过引入的扩展方法机制,我们可以对List进行静态扩展,但调用的时候却好像是集合对象自身拥有的实例方法那样。这一实现与动态语言的直接扩展不同,而是C#的一种语法糖。通过使用隐式转换,Scala也可以做到这一点。
上面代码中的result,实则是通过隐式转换,将其转换为一个扩展自scala的Iterable[+A],而最终扩展自trait IterableLike,其中定义了foreach()函数。当然,在这个foreach()函数中,实则又调用了object Iterator的foreach()函数:
trait IterableLike[+A, +Repr] extends Any with Equals with TraversableLike[A, Repr] with GenIterableLike[A, Repr] {
self =>
def foreach[U](f: A => U): Unit =
iterator.foreach(f)
}
|
我们可以利用这种机制为已定义好的无法修改的类(尤其是Java提供的类)进行扩展。例如为java.io.File进行扩展,使其支持read功能:
class RichFile(val from: File) {
def read = Source.fromFile(from.getPath).mkString
}
implicit def file2RichFile(from: File) = new RichFile(from)
|
直接import该隐式转换,File就可以像真正提供read方法那样调用了:
val fileContent = new File("README.txt").read
|