简体   繁体   中英

ClassCastException when extending a generic class

Let's say I have a class Text which implement an interface called Drawable which is defined as follow :

public interface Drawable {

  public void draw (DrawConfig drawConfig);
}

I want to have an object which acts like an array with items but which implements Drawable as well, to be able to write drawables.draw(drawConfig) and having it forward the call to all its children instead of having to do a for loop. So I first created a wrapper class which looks like this :

public class Drawables implements Drawable {

  private final Array<Drawable> items = new Array<Drawable>();

  public void add (final Drawable value) {
    this.items.add(value);
  }

  @Override
  public void draw (final DrawConfig drawConfig) {
    for (final T item : this.items)
      item.draw(drawConfig);
  }
}

The first problem is that as it's a wrapper it doesn't have any of the Array class methods and I have to manually add the ones I need (like add() in the example above).

The second problem is that if I store only Text objects in it, I can't write :

for (Text t : drawables)
    t.setText("blablabla");

Which means I have to make another array and I'll end up with two arrays just for that. I want to avoid that so I thought about using genericity as the Array class is generic and I turned my Drawables class into this :

public class Drawables<T extends Drawable> extends Array<T> implements Drawable {

  @Override
  public void draw (final DrawConfig drawConfig) {
    for (final T item : this.items) // Line 17
      item.draw(drawConfig);
  }
}

And now I can write :

Drawables<Text> drawables = new Drawables<Text>();

drawables.add(new Text());
drawables.add(new Text());

for (Text t : drawables)
    t.setText("blablabla");

drawables.draw(drawConfig);

And it compiles !! Except that at runtime I get the following error :

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [La.b.c.ui.Drawable;
    at a.b.c.Drawables.draw(Drawables.java:17)
    at a.b.c.ui.components.Text_UseTest.render(Text_UseTest.java:80)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication.mainLoop(LwjglApplication.java:215)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication$1.run(LwjglApplication.java:120)

See the comment in my code for line 17.

What is the problem? And is it even possible to achieve what I want to do?

I don't think you have your design right. Instead of separating drawing a single item and drawing multiple items, Drawable should only specify in its contract that it draws according to some config. How it does this, or what other objects it uses to do this, is not important.

Hence, I would first do this:

public interface Drawable {
    void draw (DrawConfig drawConfig);    
}

Now it appears that you want your Drawable to maintain a private member. This private member has some bearing on how things are drawn. The interesting bit is that it could be the thing itself being drawn, or perhaps it could contain things that need to be drawn, or maybe it's something else entirely. We don't know at this point how it would impact the behavior of draw . So how do we handle that? Well we can create an abstract class that maintains this private member and also implements Drawable :

public abstract class AbstractDrawable<T> implements Drawable {    
    protected T data;           
}

Notice that there is no implementation for draw . This is on purpose because it's only in the concrete implementation that we will actually define a behavior for draw . You can easily see now how we have split two concerns (maintaining state and implementing behavior). Now we can define a concrete implementation which define how it will maintain state and how it will implement behavior:

public class Drawables<T extends Drawable> extends AbstractDrawable<List<T>> {

    public Drawables() {
        this.data = new ArrayList<T>();
    }

    public void addDrawable(T item) {
        this.data.add(item);
    }

    @Override
    public void draw (final DrawConfig drawConfig) {
        for (final T item : this.data) {
            item.draw(drawConfig);
        }
    }
}

Here we have a very specific implementation of AbstractDrawable<T> that itself works with a list of drawables. This concrete implementation has defined how it manages state (it uses an internal list of Drawable instances) and how it manages behavior as well (its "draw" behavior is to "draw" each Drawable instance that it maintains).

You mentioned that you want all the list methods, etc. It's not a good idea to extend a class simply to get access to its method. Your list of drawable items is not really a list. It is a drawable that happens to work with a list of drawables. The approach above implicitly uses composition over inheritance, because AbstractDrawable<T> encapsulates some internal data that will be drawn in some manner (not important how this is done; just that it is done). So instead of extending List<T> , you can now maintain an internal list that you can then "draw" in a manner you choose. This is exactly what happens in the concrete implementation Drawables<T extends Drawable> .

Your next question is about not being able to call t.setText() on Text instances. The only way you can do this is if you have a concrete instance that works specifically on Text instances, or if you performed instanceof checks inside draw . This is because all you have is T extends Drawable ; the class knows is that it is some kind of Drawable , and that it is able to draw . Other than that, it has no idea about the specific implementations.

The class Array implements Iterable. So you can write like this.

@Override
public void draw (final DrawConfig drawConfig) {
  for (final T item : this) // Line 17
      item.draw(drawConfig);
}

The problem after erasure:

for (final Drawable item : this.items) // this.items is of Object[]!!!

Now the for loop doesn't know how to cast (no implicit casts in java). You couldn't simple write Drawable item = new Object(); . You have to do the cast yourself:

for (Object o : this.items) {
  Drawable item = (Drawable) o;
  item.draw(drawConfig);
}

Related: Cast element in Java For Each statement

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM