Using Scala from Java, collection type erasure?

Hey folks, I’m returning to a task with some Scala+Java interop for the first time in 10 years (!!). I’ve solved this problem before, but that knowledge is locked inside a firewall… a few firewalls ago… and I can’t seem to find a public solution here, on StackOverflow, etc. I think I’m just not wording it right in my search terms.

I’m working in Scala 2.12.20 and Java 8—hard requirements.

I’ve got a Scala class that looks like this:

class RankAggregator(
  val minNumRecs: Int    = 10,
  val method:     String = "median",
  val weights:    List[Double] = Nil // regular Scala list
)

I need to instantiate it somehow from some Java code. Now, I know I’d have to use JavaConverters to convert a java.util.ArrayList to the scala.collection.immutable.List. But, it seems like the type of the collection is getting erased, such that the Java side complains that the constructor needs a List[Object] instead of a List[Double].

List<Double> weights = new ArrayList<>();

// fails, unconverted from java to Scala, duh
RankAggregator aggregator2 = new RankAggregator(50, "median", weights);
// fails, incompatible types:
//   scala.collection.immutable.List<java.lang.Double>
//   cannot be converted to
//   scala.collection.immutable.List<java.lang.Object>
RankAggregator aggregator3 = new RankAggregator(50, "median",
    JavaConverters.asScalaBuffer(weights).toList());

I don’t really want to bring Scala into the Java code as a direct dependency and I control the Scala code, too, so I can just provide a nicer interface, right? I remember this from the last time I did this… 10 years ago.

I distinctly remember creating a set of builders/factories to handle the Java consumers of the Scala code we were writing back then, so I did this up real quick:

import scala.collection.JavaConverters._
import java.util.{List => JavaList}

object RankAggregator {
  def create(minNumRecs: Int    = 10,
             method:     String = "median",
             weights:    JavaList[Double]): RankAggregator = {
    new RankAggregator(
      minNumRecs=minNumRecs,
      method=method,
      weights=weights.asScala.toList
    )
  }

However, this is failing, too, with the type incompatibility because the collection’s type is being dropped.

//fails, incompatible types: java.util.List<java.lang.Double>
//       cannot be converted to java.util.List<java.lang.Object>
RankAggregator aggregator = RankAggregator.create(50, "median", weights);

Then I indulged in some thrashing with creating a builder that should be unnecessary:

object RankAggregator {
  def builder(): Builder = new Builder()
  case class Builder(
                 minNumRecs: Option[Int] = None,
                 method: Option[String] = None,
                 weights: Option[List[Double]] = None) {
    def minNumRecs(v: Int): Builder = {
      this.copy(minNumRecs = Option(v))
    }
    def method(v: String): Builder = {
      this.copy(method = Option(v))
    }
    def weights(v: JavaList[Double]): Builder = {
      this.copy(weights = Option(v).map(_.asScala.toList))
    }
    def build(): RankAggregator = {
      new RankAggregator(
        minNumRecs=minNumRecs.get,
        method=method.get,
        weights=weights.get)
    }
  }
}

Then to use it from Java:

// fails,
// incompatible types: java.util.List<java.lang.Double>
//     cannot be converted to java.util.List<java.lang.Object>
// (and obviously would break at runtime because of the None.get)
RankAggregator aggregator = RankAggregator.builder().weights(weights).build();

I’m stumped.

  • I don’t think I need the Manifest stuff here because I’m not creating generic methods.
  • I think think I need the TypeTag, etc. either.

Do I need to give up and cast?

1 Like

I think this should work, could you give it a try and report back:

import scala.collection.JavaConverters._
import java.util.{List => JavaList}
import java.lang.{Double => JavaDouble}

object RankAggregator {
  def create(
    minNumRecs: Int    = 10,
    method:     String = "median",
    weights:    JavaList[JavaDouble]
): RankAggregator =
    new RankAggregator(
      minNumRecs = minNumRecs,
      method = method,
      weights = weights.asScala.iterator.map(Double.unbox).toList
    )

In case you decide to give up, you reminded me of double casting:

public C g(List<Double> xs) {
    return new C((scala.collection.immutable.List<Object>)
      (scala.collection.immutable.List<?>) JavaConverters.asScalaBuffer(xs).toList());
}

I’ve forgotten why I needed to do that when I worked only in Java.

That works, it’s used in Americium to delegate from the Java API to the Scala API.

Having said that, I think one could try defining a functional interface in Java that accepts the troublesome Java list of Java doubles, ie. in boxed form, and to implement that interface in Scala - that will do the unboxing via a bridge method in the implementing subclass. That implementation then calls RankAggregator.apply.

Worth a shot if you want to hide the unboxing away from prying eyes.

1 Like

Thank you so much for your responses. I went in the direction that @BalmungSan suggested and it worked perfectly. I even added a test in Java to catch any compilation problems down the line as we make more of this Scala library available to Java easily.

1 Like