Collections conversion in Scala

Versions: Scala 2.12.1

Nowadays a lot of SDKs and libraries are written with Java. Fortunately dealing with Java code in Scala is possible and even in case of collections.

This post describes the conversion between Scala and Java collections. The first part explains how to do that with the help of Scala's native conversions. The second section explains the difference between JavaConversions and JavaConverters, and which one we should use in the newest Scala applications.

Collections interoperability

During long time the interoperability between collections was guaranteed by JavaConversions methods. But it became deprecated in 2.12.0. In that version the conversions were replaced by JavaConverters which provides a set of different decoration methods transforming Java collections to Scala and vice versa. The conversion is possible with asScala and asJava extension methods:

describe("Scala to Java collection conversion") {
  it("should convert Seq to a List") {
    val letters = Seq("A", "B", "C")

    val javaList =letters.asJava

    javaList shouldBe a [java.util.List[String]]
  }
}
describe("Java to Scala conversion") {
  it("should convert Java Map to Scala Map") {
    val javaMap = new java.util.HashMap[String, String]()
    val scalaMap = javaMap.asScala

    scalaMap shouldBe a [mutable.Map[String, String]]
  }
}

Internally the converted collections are wrapped by one of classes defined in scala.collection.convert.Wrappers. All case classes beginning with “J" represent Java wrappers returning Scala collections. This mechanism has some natural implications. First and foremost, the underlying collection is the same. That said, if we convert a mutable Scala sequence to a Java one, any change made on them will be automatically visible in both sides:

it("should apply side-effect in converted Java List from Scala mutable sequence") {
  val mutableLetters = new mutable.ListBuffer[String]()
  mutableLetters.appendAll(Seq("A", "B", "C"))
  val javaLettersList = mutableLetters.asJava
  mutableLetters.append("D")

  /**
    * According to the Scaladoc, "The returned Java `Iterable` is backed by
    * the provided Scala `Iterable` and any side-effects of
    * using it via the Java interface will be visible via the Scala interface and vice versa."
    */
  javaLettersList.size shouldEqual 4
  mutableLetters should contain allOf("A", "B", "C", "D")
}

it("should apply Java side-effects to converted Scala sequence") {
  val javaLetters = new util.ArrayList[String]()
  javaLetters.add("A")
  val scalaLetters = javaLetters.asScala
  javaLetters.add("B")

  scalaLetters shouldBe a [mutable.Buffer[String]]
  scalaLetters should have size 2
  scalaLetters should contain allOf("A", "B")
}

But obviously, it doesn't apply to immutable sequences:

it("should not apply side-effects in converted Java List from Scala immutable sequence") {
  var letters = Seq("A", "B", "C")
  val javaLettersList = letters.asJava
  letters = letters :+ "D"

  letters should contain allOf("A", "B", "C", "D")
  javaLettersList.size() shouldEqual 3
}

Moreover, Scala's immutability brings another point - unsupported mutable operations. If we convert a Scala immutable sequence and we try to modify its Java corresponding object, an UnsupportedOperationException will be thrown:

it("should fail when Scala immutable sequence is converted to Java mutable List and add operation is invoked") {
  val scalaLetters = Seq("A", "B", "C")
  val javaLetters = scalaLetters.asJava

  intercept[UnsupportedOperationException] {
    javaLetters.add("D")
  }

As already told, wrapper only wraps the underlying collection. So obviously there is no any object cloning:

it("should prove that the objects in converted collection are the same") {
  val mutableState = new MutableState()
  val javaMutableStates = new util.ArrayList[MutableState]()
  javaMutableStates.add(mutableState)
  val scalaMutableStates = javaMutableStates.asScala
  mutableState.state = 1

  scalaMutableStates(0).state shouldEqual javaMutableStates.get(0).state
  scalaMutableStates(0) shouldEqual javaMutableStates.get(0)
}

Even better, when we try to convert back given sequence to its original format, we'll get the same instance of the class:

it("should convert back to original Scala object") {
  val scalaLetters = Seq("A", "B", "C")
  val javaLetters = scalaLetters.asJava
  val scalaLettersConvertedBack = javaLetters.asScala

  scalaLetters shouldEqual scalaLettersConvertedBack
}
it("should convert back to original Java object") {
  val javaLetters = new util.ArrayList[String]()
  val scalaLetters = javaLetters.asScala
  val javaLettersConvertedBack = scalaLetters.asJava

  javaLetters shouldEqual javaLettersConvertedBack
}

JavaConverters vs JavaConversions

At the beginning of previous section I talked about deprecated JavaConversions class. Without deeping delve into details, it's always good to know the reasons of deprecation.

JavaConversions use implicit conversion methods between Java and Scala. As you could read in the post about Scala implicits, implicitness is good but should be used reasonably. In the case of conversions, it made the code a little bit less understandable. In the other side, JavaConverters use Pimpy my library pattern to add previously seen asScala and asJava methods. Hence, the conversion becomes explicit and the whole logic is much more understandable.

Aside readiness, the implicit conversion can lead to unexpected results. Below you can find a test found in the Scaladoc of ImplicitConversions.scala:

describe("implicit conversion") {
  it("should produce unexpected issue") {
    import collection.JavaConversions._
    case class StringBox(s: String)
    val m = Map(StringBox("one") -> "uno")

    m.get("one") shouldBe null
  }
}

In addition, JavaConversions are scheduled to disappear in Scala 2.13. So there is no reason to still stay with JavaConversions class.

Scala and Java collections interoperability is a great example of Pimp my library pattern. It decorates collections with asScala and asJava methods that convert them in their representations in opposite languages. The "conversion" consists on wrapping the original collections and exposing, sometimes slightly, modified API. The case with Scala's immutable sequence conversion to Java's mutable List is a great example of that.