2

I created a RollingWindow class in order to have a fixed number of most recent data points stored in an array.

class RollingWindow<T> (private val length: Int) {

    private val window = Array<Any?>(length) {null}
    private var count = 0


    fun push(t: T) {
        if (length == 0)
            return

        window[currentIndex()] = t
        count++
    }

    @Suppress("UNCHECKED_CAST")
    fun toArray(): Array<T> {
        if (length == 0)
            return arrayOf<Any>() as Array<T>

        val firstHalf = window
            .copyOfRange(currentIndex(), window.size)
            .filterNotNull()
            .toTypedArray()
        val secondHalf = window
            .copyOfRange(0, currentIndex())
            .filterNotNull()
            .toTypedArray()
        
        val arr = arrayOf(*firstHalf, *secondHalf) as Array<T>
        print(arr.contentToString()) 
        //this works fine but for some reason the class cast exception is thrown from the unit test
        return arr 
    }

    override fun toString() = toArray().contentToString()

    private fun currentIndex() = count % length
}

I wrote some unit tests and am getting a ClassCastException

@Test
fun testRollingWindowNotFull() {
    val doubleWindow = RollingWindow<Double>(5)
    doubleWindow.push(2.5)
    doubleWindow.push(6.8)
    assertArrayEquals(arrayOf(2.5, 6.8), doubleWindow.toArray()) //this passes

    val variableInWindow = RollingWindow<Double>(5)
    variableInWindow.push(25.6)
    variableInWindow.push(24.32)
    val windowArray = variableInWindow.toArray() // ClassCastException only gets thrown here or whenever it's stored in a variable. If I use variableInWindow.toArray() inline it's fine, as shown in previous assertion
    assertArrayEquals(arrayOf(25.6, 24.32), windowArray) // This never gets reached
}

While running the test I tried casting the Array<Any> into an Array<T>. Casting within the RollingWindow class works fine, no errors, but I specifically get an error in the Unit Test. Here's the StackTrace:

Sep 13, 2021 1:55:49 PM org.junit.platform.launcher.core.EngineDiscoveryOrchestrator lambda$logTestDescriptorExclusionReasons$7
INFO: 0 containers and 4 tests were Method or class mismatch
[Ljava.lang.Object;@7a8051ce[Ljava.lang.Object;@3ba12a08[Ljava.lang.Object;@725e196
class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Double; ([Ljava.lang.Object; and [Ljava.lang.Double; are in module java.base of loader 'bootstrap')
java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Double; ([Ljava.lang.Object; and [Ljava.lang.Double; are in module java.base of loader 'bootstrap')
    at collections.RollingWindowTest.testRollingWindowNotFull(RollingWindowTest.kt:24) 

1 Answer 1

5

Unlike all other generics in classes in Kotlin, the Array class and the Array class only has a reified type, and it is an invariant type. You cannot ever successfully cast an Array of one type to an Array of another type unless you are casting it to have a covariant or contravariant type.

I think the only reason the first part of your test passes is that there must be a compiler optimization that omits the reified cast inside the toArray() function if the returned object is used as an Any or Array<out Any>. The Java-defined assertArrayEquals function takes two Object[] parameters which get mapped to either Array<Any> or Array<out Any>, and perhaps in this case it's doing the latter cast because it sees that nothing is put into the arrays in this function.

So the compiler as an optimization may be replacing your reified cast to Array<Double> with a non-reified cast to Array<out Double>, which is a safe cast.

You might want to consider using List or MutableList instead of Array to avoid having to deal with these problems.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the indepth explanation. Will convert to a List for now, was thinking using Arrays as the underlying data structure would be better performance and memory management, but since I can use a LinkedList as the underlying, it actually wouldn't make too much of a difference...

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.