Scala支持与Java的隐式转换
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。如果像下面这样显式指定为Scala的List或Set类型,则无法转换:val result:Set = configure.readSoftInfos("/config.xml", "version number") //or
val result:List = 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。倘若Java代码需要调用这个方法,则还需要对其进行转换,即要求调用者必须具备Scala的知识,这未必友好。那么应该怎样改善呢?直接的做法就是让readSoftInfos方法返回Java的List,这时候需要使用Scala提供的隐式转换:
import scala.collection.JavaConversions._
class XmlConfigure {
def readSoftInfos(configFileName: String, version: String) : java.util.List = {
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(b : Seq): ju.List = seqAsJavaList(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(seq: Seq): ju.List = seq match {
case JListWrapper(wrapped) => wrapped.asInstanceOf]
case _ => new SeqWrapper(seq)
}
}
//Wrappers
import java.{ lang => jl, util => ju }, java.util.{ concurrent => juc }
import WrapAsScala._
import WrapAsJava._
private trait Wrappers {
case class SeqWrapper(underlying: Seq) extends ju.AbstractList with IterableWrapperTrait {
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 with GenIterableLike {
self =>
def foreach(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
页:
[1]