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