1

In my project I'm working with various handlers to perform logic on arrays of different primitive types, and I came across this runtime error:

[error] java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [B ([Ljava.lang.Object; and [B are in module java.base of loader 'bootstrap')

In this case I was working with Bytes, hence the [B, but one of my functions returned [Ljava.lang.Object instead (at runtime!). How can I ensure my generic functions return primitive arrays instead of object arrays?

Here's a minimal example to reproduce it:

package asdf

import scala.reflect.ClassTag
import java.nio.charset.StandardCharsets

object Main {
  trait Helper[A, B] {
    def decode(bytes: Array[Byte]): Array[A]
    def aggregate(values: Array[A]): B
  }

  object StringHelper extends Helper[Byte, String] {
    def decode(bytes: Array[Byte]): Array[Byte] = bytes.filter(_ != 0)
    def aggregate(values: Array[Byte]): String = new String(values, StandardCharsets.UTF_8)
  }

  object IntHelper extends Helper[Int, Int] {
    def decode(bytes: Array[Byte]): Array[Int] = bytes.map(_.toInt)
    def aggregate(values: Array[Int]): Int = values.sum
  }

  def decodeAgg[A, B](bytes: Array[Byte], helper: Helper[A, B])(implicit ev: ClassTag[A]): B = {
    val decoded = helper.decode(bytes)
    val notFirstDecoded = decoded
      .zipWithIndex
      .filter({case (_, i) => i != 0})
      .map(_._1)
      .toArray
    helper.aggregate(notFirstDecoded)
  }

  def main(args: Array[String]) {
    val helper: Helper[_, _] = args(1) match {
      case "int" => IntHelper
      case "string" => StringHelper
      case _ => throw new Exception("unknown data type")
    }
    val bytes = Array(97, 98, 99).map(_.toByte)
    val aggregated = decodeAgg(bytes, helper)
    println(s"aggregated to $aggregated")
  }
}

Run with sbt "run -- string".

Full stack trace on this example:

[error] java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [B ([Ljava.lang.Object; and [B are in module java.base of loader 'bootstrap')
[error]     at asdf.Main$StringHelper$.aggregate(Main.scala:12)
[error]     at asdf.Main$.decodeAgg(Main.scala:29)
[error]     at asdf.Main$.main(Main.scala:39)

I was using Scala 2.12, JDK 13. I've tried using @specialized to no effect.

0

1 Answer 1

4

The problem lies in your toArray in call:

val notFirstDecoded = decoded
      .zipWithIndex
      .filter({case (_, i) => i != 0})
      .map(_._1)
      .toArray

toArray takes implicit ClassTag argument - it needs to know the runtime type of array elements to create the Array.

As you provide implicit ClassTag argument to decodeAgg, toArray happily takes what you deliver.

def decodeAgg[A, B](bytes: Array[Byte], helper: Helper[A, B])(implicit ev: ClassTag[A]): B

You can see that ClassTag corresponds to first generic argument of Helper.

You pass following helper:

val helper: Helper[_, _] = args(1) match {
      case "int" => IntHelper
      case "string" => StringHelper
      case _ => throw new Exception("unknown data type")
    }

The ClassTag is thus inferred to Object, which is why you get an Array of objects.

Note that if you use an IntHelper directly, the ClassTag is constrained to the correct type, and the function works as expected.

val aggregated = decodeAgg(bytes, IntHelper)

Solution ideas

There might be multiple ways to resolve it. One idea might be to provide the classTag explicitly via Helper

import scala.reflect.ClassTag
import java.nio.charset.StandardCharsets

object Main {
  trait Helper[A, B] {
    def decode(bytes: Array[Byte]): Array[A]
    def aggregate(values: Array[A]): B
    def classTag: ClassTag[A]
  }

  object StringHelper extends Helper[Byte, String] {
    def decode(bytes: Array[Byte]): Array[Byte] = bytes.filter(_ != 0)
    def aggregate(values: Array[Byte]): String = new String(values, StandardCharsets.UTF_8)
    def classTag: ClassTag[Byte] = ClassTag(classOf[Byte])
  }

  object IntHelper extends Helper[Int, Int] {
    def decode(bytes: Array[Byte]): Array[Int] = bytes.map(_.toInt)
    def aggregate(values: Array[Int]): Int = values.sum
    def classTag: ClassTag[Int] = ClassTag(classOf[Int])
  }

  def decodeAgg[A, B](bytes: Array[Byte], helper: Helper[A, B]): B = {
    val decoded = helper.decode(bytes)
    val notFirstDecoded = decoded
      .zipWithIndex
      .filter({case (_, i) => i != 0})
      .map(_._1)
      .toArray(helper.classTag)
    helper.aggregate(notFirstDecoded)
  }

  def main(args: Array[String]) {
    val helper = args(1) match {
      case "int" => IntHelper
      case "string" => StringHelper
      case _ => throw new Exception("unknown data type")
    }
    val bytes = Array(97, 98, 99).map(_.toByte)
    val aggregated = decodeAgg(bytes, helper)
    println(s"aggregated to $aggregated")
  }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.