Java <-> Scala interop: transparent List and Map conversion

微笑、不失礼 提交于 2019-11-28 15:48:36

Trust me; you don't want transparent conversion back and forth. This is precisely what the scala.collection.jcl.Conversions functions attempted to do. In practice, it causes a lot of headaches.

The root of the problem with this approach is Scala will automatically inject implicit conversions as necessary to make a method call work. This can have some really unfortunate consequences. For example:

import scala.collection.jcl.Conversions._

// adds a key/value pair and returns the new map (not!)
def process(map: Map[String, Int]) = {
  map.put("one", 1)
  map
}

This code wouldn't be entirely out of character for someone who is new to the Scala collections framework or even just the concept of immutable collections. Unfortunately, it is completely wrong. The result of this function is the same map. The call to put triggers an implicit conversion to java.util.Map<String, Int>, which happily accepts the new values and is promptly discarded. The original map is unmodified (as it is, indeed, immutable).

Jorge Ortiz puts it best when he says that you should only define implicit conversions for one of two purposes:

  • Adding members (methods, fields, etc). These conversions should be to a new type unrelated to anything else in scope.
  • "Fixing" a broken class hierarchy. Thus, if you have some types A and B which are unrelated. You may define a conversion A => B if and only if you would have preferred to have A <: B (<: means "subtype").

Since java.util.Map is obviously not a new type unrelated to anything in our hierarchy, we can't fall under the first proviso. Thus, our only hope is for our conversion Map[A, B] => java.util.Map[A, B] to qualify for the second one. However, it makes absolutely no sense for Scala's Map to inherit from java.util.Map. They are really completely orthogonal interfaces/traits. As demonstrated above, attempting to ignore these guidelines will almost always result in weird and unexpected behavior.

The truth is that the javautils asScala and asJava methods were designed to solve this exact problem. There is an implicit conversion (a number of them actually) in javautils from Map[A, B] => RichMap[A, B]. RichMap is a brand new type defined by javautils, so its only purpose is to add members to Map. In particular, it adds the asJava method, which returns a wrapper map which implements java.util.Map and delegates to your original Map instance. This makes the process much more explicit and far less error prone.

In other words, using asScala and asJava is the best practice. Having gone down both of these roads independently in a production application, I can tell you first-hand that the javautils approach is much safer and easier to work with. Don't try to circumvent its protections merely for the sake of saving yourself 8 characters!

Here are some quick examples using Jorge Ortiz's scalaj-collection library:

import org.scala_tools.javautils.Implicits._

val sSeq = java.util.Collections.singletonList("entry") asScala
// sSeq: Seq[String] 
val sList = sSeq toList // pulls the entire sequence into memory
// sList: List[String]
val sMap = java.util.Collections.singletonMap("key", "value") asScala
// sMap: scala.collection.Map[String, String]

val jList = List("entry") asJava
// jList: java.util.List[String]
val jMap = Map("key" -> "value") asJava
// jMap: java.util.Map[String, String]

the javautils project is available from the central maven repository

With Scala 2.8, it could be done like this:

import scala.collection.JavaConversions._

val list = new java.util.ArrayList[String]()
list.add("test")
val scalaList = list.toList
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!