Strange java.lang.ClassCastException when using []

2019-07-07 03:26发布

问题:

I'm using LibDGX library for my game. And i faced common exception ClassCastException, but it occurs in strange case.

I'm using Animation class from LibGDX library.

I'm getting error on this line only if i am using [] operation.

val tex = animation.keyFrames[0]

If i change it to get() error disappears.

tex = animation.keyFrames.get(0)

Here is full method. I simplified example.

override fun create() {


    val frames = Array<TextureRegion>()
    frames.add(TextureRegion(Texture("badlogic.jpg")))

    val animation = Animation(frames)

    val tex1 = animation.keyFrames.get(0)  // Compiles

    val tex = animation.keyFrames[0]  // error - [Ljava.lang.Object; cannot be cast to [Lcom.badlogic.gdx.graphics.g2d.TextureRegion;


}

Strange, isn't it?

Here is Animation class that i used to minimise other error reasons. It is the same as in LibGDX api.

    public class Animation<T> {

    T[] keyFrames;
    private float frameDuration;


    public Animation (float frameDuration, Array<? extends T> keyFrames) {
        this.frameDuration = frameDuration;

        Class arrayType = keyFrames.items.getClass().getComponentType();
        T[] frames = (T[]) ArrayReflection.newInstance(arrayType, keyFrames.size);
        for (int i = 0, n = keyFrames.size; i < n; i++) {
            frames[i] = keyFrames.get(i);
        }

        setKeyFrames(frames);
    }

    protected void setKeyFrames (T... keyFrames) {
        this.keyFrames = keyFrames;
    }
}

I also noticed that error disappears if i use Array.with construction instead of creating Array<TextureRegion> I mean LibGDX Array not from Kotlin.

val animation = Animation(Array.with(TextureRegion(Texture("badlogic.jpg"))))

Could you tell me the reason why this occurs or it is possible a bug?

回答1:

Edit: In older versions of libGDX, Animation does not use Array's reflection capabilities. If it had, you wouldn't have this error, because the array would be of the correct type! It should be in libGDX in version 1.9.7. Note: you must specify the Class of the Array when creating it.

This was fixed recently in #4476. However, the following "bug" with get and [] resulting in different bytecode for an array may still be worth reporting/inquiring about.


This is interesting!

Decompiling

val tex1 = animation.keyFrames.get(0)

gives

ALOAD 2
INVOKEVIRTUAL com/badlogic/gdx/graphics/g2d/Animation.getKeyFrames ()[Ljava/lang/Object;
ICONST_0
AALOAD
CHECKCAST com/badlogic/gdx/graphics/g2d/TextureRegion
ASTORE 3

while decompiling

val tex = animation.keyFrames[0]

gives

ALOAD 2
INVOKEVIRTUAL com/badlogic/gdx/graphics/g2d/Animation.getKeyFrames ()[Ljava/lang/Object;
CHECKCAST [Lcom/badlogic/gdx/graphics/g2d/TextureRegion;
ICONST_0
AALOAD
ASTORE 4

It is important to note that the type of keyFrames is T[], so it cannot possibly have a custom get method (assuming no extensions exist.)

It seems that although the two should be equal ([] invokes operator fun get), they are not!

The only difference I can see is that the get variant checks the type (CHECKCAST) after retrieving it via AALOAD, while the [] variant attempts to check that the type of the array is T[] immediately after retrieving the array.

However, because frames is declared in Animation's constructor:

public Animation (float frameDuration, Array<? extends T> keyFrames) {
    this.frameDuration = frameDuration;
    T[] frames = (T[]) new Object[keyFrames.size];
    for (int i = 0, n = keyFrames.size; i < n; i++) {
        frames[i] = keyFrames.get(i);
    }
    setKeyFrames(frames);
}

as

T[] frames = (T[]) new Object[keyFrames.size];

the actual type of the array is Object[]! So this cast to TextureRegion[] fails.

To me, this seems like a libGDX bug.


I am able to very easily reproduce this with a simple class in Java:

public class JavaObjWithArray<T> {

    private T[] items;

    public JavaObjWithArray(T[] items) {
        this.items = (T[]) new Object[items.length];
        System.arraycopy(items, 0, this.items, 0, items.length);
    }

    public T[] getItems() {
        return items;
    }

}

fun main(args: Array<String>) {
    val obj = JavaObjWithArray(arrayOf("a", "b"))

    val one = obj.items.get(0)
    val two = obj.items[0]
}

This will also throw a ClassCastException on the line with []! Additionally, the bytecode has the same difference.


The problem really arises with the lack of generic arrays in Java (and Kotlin). There are two main solutions for this in Java:

  • Store an Object[] and cast the values upon get
  • Store an Object[], but cast it to T[]

This is a problem - because the actual type of the array is Object[]. As a consequence, these "generic" arrays should not be exposed! I can understand why libGDX does so, namely, to improve performance and remove the small overhead of method calls, but this is definitely not safe.


The fact that get and [] differ may be a bug, though. Perhaps looking into this or filing a bug report may help.


Java bytecode instruction list