Java <-> Scala interop: transparent List and Ma

2019-01-21 02:54发布

问题:

I am learning Scala and I have a Java project to migrate to Scala. I want to migrate it by rewriting classes one-by-one and checking that new class didn't break the project.

This Java project uses lots of java.util.List and java.util.Map. In new Scala classes I would like to use Scala’s List and Map to have good-looking Scala code.

The problem is that new classes (those are wtitten in Scala) do not integrate seamelessly with existing Java code: Java needs java.util.List, Scala needs its own scala.List.

Here is a simplified example of the problem. There are classes Main, Logic, Dao. They call each other in a line: Main -> Logic -> Dao.

public class Main {
    public void a() {
        List<Integer> res = new Logic().calculate(Arrays.asList(1, 2, 3, 4, 5));
    }
}

public class Logic {
    public List<Integer> calculate(List<Integer> ints) {
        List<Integer> together = new Dao().getSomeInts();
        together.addAll(ints);
        return together;
    }
}

public class Dao {
    public List<Integer> getSomeInts() {
        return Arrays.asList(1, 2, 3);
    }
}

In my situation, classes Main and Dao are framework classes (I don’t need to migrate them). Class Logic is business-logic and will benefit a lot from Scala cool features.

I need to rewrite class Logic in Scala while preserving integrity with classes Main and Dao. The best rewrite would look like (doesn’t work):

class Logic2 {
  def calculate(ints: List[Integer]) : List[Integer] = {
      val together: List[Integer] = new Dao().getSomeInts()
      together ++ ints
  }
}

Ideal behaviour: Lists inside Logic2 are native Scala Lists. All in/out java.util.Lists get boxed/unboxed automagically. But this doesn't work.

Instead, this does work (thanks to scala-javautils (GitHub)):

import org.scala_tools.javautils.Implicits._

class Logic3 {
  def calculate(ints: java.util.List[Integer]) : java.util.List[Integer] = {
      val together: List[Integer] = new Dao().getSomeInts().toScala
      (together ++ ints.toScala).toJava
  }
}

But it looks ugly.

How do I achieve transparent magic conversion of Lists and Maps between Java <-> Scala (without need to do toScala/toJava)?

If it is not possible, what are the best practices for migrating Java -> Scala code that uses java.util.List and friends?

回答1:

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!



回答2:

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



回答3:

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