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 =>
messagesprintln(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.