Chen
Chen

Reputation: 11

scala extends java generic array failure

  1. I have a Java interface with generic param.
public interface JavaInterface<T> {
  void sayHi(T[] keys);
}
  1. I can implement a Java class like
public class JavaInterfaceImpl implements JavaInterface<String> {
  @Override
  public void sayHi(String[] array) {
  }
}
  1. However, when creating a Scala class to implement the interface, it failed
class ScalaInterfaceImpl extends JavaInterface[String] {
   override def sayHi(keys: Array[String]): Unit = {}
}

Here are the error messages.

class ScalaInterfaceImpl needs to be abstract, since method sayHi in trait JavaInterface of type (x$1: Array[String])Unit is not defined
(Note that Array[T with Object] does not match Array[String]: their type parameters differ)
class ScalaInterfaceImpl extends JavaInterface[String] {

and

method sayHi overrides nothing.
Note: the super classes of class ScalaInterfaceImpl contain the following, non final members named sayHi:
def sayHi(x$1: Array[String]): Unit
   override def sayHi(keys: Array[String]): Unit = {}
  1. However, if I'm implementing the Scala class for this trait, things goes fine.
trait ScalaInterface[T] {
  def sayHi(keys: Array[T]): Unit
}
  1. The Java interface in 1 is not changeable (from a third-party lib), hence I'm blocked in 3.

update 1. I find it works with the following code

class ScalaInterfaceImpl extends JavaInterface[String] {
  override def sayHi(keys: Array[String with Object]): Unit = {}
}

Can anyone help explain it?

Upvotes: 1

Views: 50

Answers (1)

Alin Gabriel Arhip
Alin Gabriel Arhip

Reputation: 2638

Generic arrays are represented slightly different between the two languages. Even though they are not type erased, their enclosing generic trait and interface have their type parameter T erased at compile-time.

First, consider the Scala example:

trait ScalaInterface[T] {
  def sayHi(keys: Array[T]): Unit
}

After scalac compiles it, this becomes:

public interface ScalaInterface {
  void sayHi(Object var1);
}

Whereas the Java interface:

public interface JavaInterface<T> {
  void sayHi(T[] keys);
}

After javac compiles it, this becomes:

public interface JavaInterface {
  void sayHi(Object[] var1);
}

The slightly different signature between the two looks harmless at first, but it actually makes the difference. Why Scala represents them differently is explained here:

What about genericity? In Java, you cannot write a T[] where T is a type parameter. How then is Scala’s Array[T] represented? In fact a generic array like Array[T] could be at run-time any of Java’s eight primitive array types byte[], short[], char[], int[], long[], float[], double[], boolean[], or it could be an array of objects. The only common run-time type encompassing all of these types is AnyRef (or, equivalently java.lang.Object), so that’s the type to which the Scala compiler maps Array[T].

When you used JavaInterface<T>, you actually placed the type Object as the upper bound of type T, at every location where T is used in the interface, in this case in the sayHi method. This happens because of type erasure.

On the other hand, the type T in the generic trait ScalaInterface[T] is not bounded to Object; it's bounded to Any because that's the base type in Scala. This means T has different upper bounds between the two languages in the method sayHi.

Therefore, even though JavaInterface<T> and ScalaInterface[T] look similar, their sayHi method is not really equivalent. The scalac compiler does some type inference under the hood, checks the location where type T is used and concludes that the sayHi method needs the following type constraint:

trait ScalaInterface[T] {
  def sayHi(keys: Array[T with Object]): Unit
}

This is actually how your JavaInterface is treated and why using String with Object fixes the issue.

A question that arises is why Scala does not instead use Object as the upper bound of the generic type:

trait ScalaInterface[T <: Object] {
  def sayHi(keys: Array[T]): Unit
}

While this works for this particular use case, it's too constraining, because you would never be able to use subtypes of AnyVal in type T at all, so ScalaInterface[Int] or ScalaInterface[Double] would be impossible, just because the interface has a method that takes a generic array as argument.

If your interface did not have a method taking a generic array as argument it would not need this constrain at all. Or if your interface had another method that took a simple argument of type T, that would constrained as well. Such considerations show the issue is not actually in the type argument T, but in the method sayHi, so this is where the issue is pinpointed and contained.

Upvotes: 1

Related Questions