It’s quite often that we need to use a Java API from Scala. And these usages occurs even more often when you have modules written in Java in your Scala project. The most common problem is to pass Java collections to Scala API and Scala collections to Java. Fortunately, Scala library provides bidirectional implicit converters between Scala and Java collections. There are two classes JavaConverters
and JavaConversions
which provide the same functionality but in a different way.
Assume we have a Java service:
interface Message {
String getText();
getDate();
LocalDateTime }
public class JavaService {
void handleMessages(List<Message> messages) {
.stream()
messages.sorted((o1, o2) -> o1.getDate().compareTo(o2.getDate()))
.forEach(m ->
System.out.println(
.getDate().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
m+ " " + m.getText()));
}
List<Message> generateMessages(int n) {
return IntStream.range(0, n)
.mapToObj(i -> new JavaMessage(String.valueOf(i), LocalDateTime.now()))
.collect(Collectors.toList());
}
}
And a Scala code to work with messages (the same code in Scala):
class ScalaService {
def handleMessages(messages: Seq[Message]): Unit = {
sortWith((a, b) => a.getDate.isBefore(b.getDate)).foreach(m =>
messages.println(s"${m.getDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)} ${m.getText}")
)
}
def generateMessages(n: Int): Seq[Message] = {
0 until n map (i => MessageImpl(i.toString, LocalDateTime.now()))
} }
And we have two different Message implementations in Java and in Scala, which are really simple (just a POJO for Java and a case class for Scala).
Now let’s try to use JavaConversions
, which gives you an implicit magic:
import scala.collection.JavaConversions._
handleMessages(scalaService.generateMessages(3))
javaService.handleMessages(javaService.generateMessages(3)) scalaService.
In contrast with JavaConversions
JavaConverters
require us to explicitly specify what needs to be converted explicitly, using asScala
, asJava
or one of the other methods:
import scala.collection.JavaConverters._
handleMessages(scalaService.generateMessages(3).asJava)
javaService.handleMessages(javaService.generateMessages(3).asScala) scalaService.
What is the reason to have two classes providing similar functionality? I think the JavaConversions
gives you more simplicity. But the JavaConverters
gives you more control. You can handle what’s going on, what type do you pass to a Java code.
Now, let’s try this:
val messages = List("foo", "bar", "baz").map(s => ScalaMessage(s, LocalDateTime.now()))
handleMessages(messages)
scalaService.handleMessages(messages) javaService.
This works fine with JavaConversions
, but it doesn’t compile with JavaConveters
:
type mistmatch
found: java.util.List[ScalaMessage]
required: java.util.List[Message]
What happened here? Why did we get a list of ScalaMessage
s here? Because Scala always tries to infer the most precise type. In this case it is List[ScalaMessage]
because all list elements are ScalaMessage
s. Then, why did ScalaService
take it as a parameter without any error? Even though these methods signature looks same, there is a big difference in Scala and Java versions. Scala list is a covariant type. This means that the list of subtypes of Message
is a subtype of list of Message
. As result, we can use a List[ScalaMessage]
as a List[Message]
.
But there are no type variance in Java. We can emulate it, defining handleMessages as:
void handleMessages(List<? extends Message> messages)
But what if it’s a library method, or we don’t want to change the method signature for some other reason? And how does JavaConversions
manage to solve it?
Let’s check how JavaConversions
works. When the Scala compiler find that an argument type is not compatible with a required type, it tries to find an implicit conversion which can change that type to the required one. In our case the required type is a java.util.List[Message]
, but the actual type is scala.collections.List[ScalaMessage]
. So, the compiler tries to find an implicit way to convert the actual type to the expected one. In this case it uses seqAsJavaList
function from JavaConversions
which have the following definition:
implicit def seqAsJavaList[A](seq: Seq[A]): java.util.List[A]
This function has a required result type (java.util.List[Message]
) and takes a Seq[Message]
as an argument. But the Scala List
is derived from Seq
, so we can pass the List to the places where Seq is required. Seq
type parameter is covariant and because of that List[ScalaMessage]
inherits Seq[Message]
, and it can be passed to the seqAsJavaList[Message]
.
Now let’s check the JavaConverters
method, to understand why it doesn’t work. This class uses another type of Scala implicit conversions. When the Scala compiler finds a method call of unknown method it tries to find an implicit conversion which will return a type with this method. In our case following implicit conversion is used:
implicit def seqAsJavaListConverter[A](b : Seq[A]): AsJava[java.util.List[A]]
This method takes a sequence of type A
and returns an instance of proxy class AsJava
(as you might guess, it has asJava
method) of Java list of same type A
. Here, we have AsJava[java.util.List[ScalaMessage]]
. So, the result of the asJava
call is a Java list of ScalaMessage
, which cannot be passed as a list of Message
in Java.
Well, what can we do?
messages
variable:val messages: List[Message] = List("foo", "bar", "baz")
map(s => ScalaMessage(s, LocalDateTime.now()))
.
handleMessages(messages.asJava) javaService.
It works, but what if we’d like to inline it?
handleMessages(List("foo", "bar", "baz")
javaService.map(s => ScalaMessage(s, LocalDateTime.now())).asJava) .
And we get the same error as previously.
handleMessages(List("foo", "bar", "baz")
javaService.map(s => ScalaMessage(s, LocalDateTime.now())).asInstanceOf[List[Message]].asJava) .
This also works, but it looks ugly.
Seq[A]
. This class will have a method asJava[B]
which returns Java list of B
. But we cannot just put instances of A
in the collection of arbitrary B
. Fortunately we don’t need to do that. We are interested only in B
s which are parents of A
, because such cast is always safe. So the code is:object Converters {
implicit class AsJava[A](val a: Seq[A]) extends AnyVal {
def asJava[B >: A]: java.util.List[B] = {
import scala.collection.convert.WrapAsJava._
seqAsJavaList(a)
}
} }
And now our problem code is compiled:
import Conveters._
handleMessages(List("foo", "bar", "baz")
javaService.map(s => ScalaMessage(s, LocalDateTime.now())).asJava) .
We can specify the type B
explicitly (like messages.asJava[Message]
), but the Scala implicit resolution is smart enough to infer the required type itself.