arraysscalagenericsscala-3erasure

How to create an Array[T | Null] where T is a type parameter with `AnyRef` as upper bound


I need to create a temporary Array[T | Null] where T is a type parameter with AnyRef as upper bound. Given the arrays implementation will use an Array[AnyRef] after erasure for whatever T is, why should I relay on a ClassTag?

def method[T <: AnyRef](elements: Iterable[T])(using ctTorNull: ClassTag[T | Null]): T = 

    val array = elements.toArray[T | Null]
    ???

}

Is there a way to achieve the same without using ClassTag nor asInstanceOf in scala 3.4?

Edit: As Dmytro pointed out in his answer, the type information that the JVM has about Array[A] and of Array[B] is different even if both A and B extend AnyRef.

Evidently, arrays hold more information about the declared type of their elements than many collections do. So, to avoid the ClassTag in this scenario use a mutable collection that does not require a ClassTag. mutable.ArraySeq requires it but ArrayBuffer does not. The example below uses the IndexedSeq factory which creates an ArrayBuffer.

def method[T <: AnyRef](elements: Iterable[T]): T = 

    val array = scala.collections.mutable.IndexedSeq.from[T | Null](elements)
    ???

}

If for efficiency reasons you need that the temporary collection be an Array, you may avoid the contextual ClassTag parameter if the received collection exposes the ClassTag of its backing Array. mutable.ArraySeq and mutable.ArrayStack do it in the elemTag member.

def method[T <: AnyRef](elements: mutable.ArraySeq[T]): T = 

    val array = elements.toArray[T | Null](using elements.elemTag.asInstanceOf[ClassTag[T | Null]])(elements)
    ???
}

Here I assumed that the erased type of T and T | Null is the same when T <: AnyRef.


Solution

  • Given the arrays implementation will use an Array[AnyRef] after erasure for whatever T is

    (where T is a type parameter with AnyRef as upper bound)

    It's not true that erasure of Array[T] (T <: AnyRef) is Array[AnyRef]. See the spec:

    The erasure of the parameterized type scala.Array[T] is scala.Array[|T|].

    https://www.scala-lang.org/files/archive/spec/3.4/03-types.html#type-erasure

    (Here |T| denotes the erasure of T).

    You can check that Array[Int] and Array[String] have different classes at runtime

    classOf[Array[Int]])   // class [I
    classOf[Array[String]] // class [Ljava.lang.String;
    

    .asInstanceOf will throw if you specify wrong class

    Array(1, 2, 3).asInstanceOf[Array[String]]
    // java.lang.ClassCastException: class [I cannot be cast to class [Ljava.lang.String;
    

    Similarly,

    class SomeClass
    class OtherClass
    implicitly[SomeClass <:< AnyRef] // compiles
    implicitly[OtherClass <:< AnyRef] // compiles
    
    classOf[Array[SomeClass]]) // class [LSomeClass;
    classOf[Array[OtherClass]] // class [LOtherClass;
    
    Array(new SomeClass()).asInstanceOf[Array[OtherClass]]
    // java.lang.ClassCastException: [LSomeClass; cannot be cast to [LOtherClass;
    

    It's T in Iterable[T] that is erased. So you need ClassTag to preserve the information about T from compile time to runtime (in order to get correct Array[T] at runtime).