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?
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:
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With