逸言

Scala支持与Java的隐式转换

| Comments

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

Comments