Kat
Kat

Reputation: 4695

Casting parameterized classes into subclasses of the parameter

Is there a way to cast, say, a list of some super type into a list of a subtype? It seems like this should be possible, since, if you create non-generic types that encapsulate the generic list, you can cast as expected. For example:

class Foo {}
class Bar extends Foo {}

public class MyClass {
    public static void main(String[] args) {
        ArrayList<Foo> fooList = new ArrayList<>();
        fooList.add(new Bar());

        // No can do. "Cannot cast from ArrayList<Foo> to ArrayList<Bar>"
        // ArrayList<Bar> barList = (ArrayList<Bar>) fooList;
        // Bar bar = barList.get(0);

        FooList newAndImproveFooList = new FooList();
        newAndImproveFooList.add(new Bar());
        BarList barList = (BarList) newAndImproveFooList;
        barList.get(0);
    }
}

class FooList {
    private ArrayList<Foo> list = new ArrayList<>();

    // Simple delegates
    public void add(Foo foo) { list.add(foo); }
    public Foo get(int index) { return list.get(index); }
}

class BarList extends FooList {
    public Bar get(int index) { return (Bar) super.get(index); }
}

The point of this code being to show that this casting issue can be "easily" gotten around. And of course, we can cast arrays of different types (so the fact that FooList could contain Foos that aren't Bars shouldn't be an issue.

It seems that the only solution to this problem is to not use generics, but then you'd have to create an instance of this list for every type that you need to make a list of, which is the very problem that generics were intended to solve.

We could cast every access to the list, but the current layout of the program (and can't easily change that) would result in thousands of casts, where as if we could cast the lists, we'd only need a couple of casts in a few places.

And we can't make a copy of the list, as it is modified in places.

Upvotes: 0

Views: 984

Answers (2)

John Bollinger
John Bollinger

Reputation: 180171

Part of the point of generics is that you do not cast. If you are using generics correctly then all types involved can be determined (closely enough) statically.

In your particular case, you cannot cast a List<Foo> to List<Bar> because it may contain Foos that are not Bars, either at the time of the cast or at some later time. Similarly, you cannot cast a List<Bar> to a List<Foo>, because then it would be possible to add a Foo that is not a Bar. A type parameter is not specifically about the current state of an object; rather, it is an aspect of the object's type, affecting, among other things, the space of all possible present and future states.

You are correct that you can avoid type-safety checking altogether by using bare types instead of parameterized ones. If parameterization gets in your way more than it helps, then perhaps that's what you should do, but it's a sign of code brittleness (because your code relies on assumptions that cannot be statically checked).

And you can defeat type checking (at the expense of a few warnings) by casting between incompatible parameterized types by going through bare intermediate type, but that in no way means that you should do so, nor that Java should accept direct casts between incompatible types.

As for your legacy code, you can possibly use type bounds and covariant return types to make at least some progress:

abstract class Thing {
    public List<? extends Thing> getAgentList() {
        return Collections.<Thing>emptyList();
    }

    public List<? extends Thing> getHouseholdList() {
        return Collections.<Thing>emptyList();
    }
}

class Agent extends Thing {
    public List<Agent> getAgentList() {
        // ...
    }
}

class Household extends Thing {
    public List<Household> getHouseholdList() {
        // ...
    }
}

The whole design you describe is uncomfortable, however, for the superclass has methods that imply knowledge of its subclasses, even if it doesn't have an actual compile-time dependency. Moreover, it seems unlikely to be sensible for Household to have a method getAgentList() (for example). As long as you're going to do that, though, you might as well go all the way:

abstract class Thing {
    public List<Agent> getAgentList() {
        return Collections.<Agent>emptyList();
    }

    public List<Household> getHouseholdList() {
        return Collections.<Household>emptyList();
    }
}

Personally, as long as you're working on this code, I'd consider taking the opportunity to refactor the whole mess into something a little less convoluted.

Upvotes: 0

Sotirios Delimanolis
Sotirios Delimanolis

Reputation: 279940

The simplest way would be to do something equivalent to

ArrayList<Bar> barList = (ArrayList<Bar>) (ArrayList<? extends Foo>) fooList;

This will throw up a bunch of compiler warnings about unchecked casts. The compiler is trying to warn you that nothing good can come of this. By performing this cast, you are losing all type information and setting yourself up for ClassCastExceptions if you were to use the elements of barList as Bar objects.

You could add Bar items to barList, but you couldn't retrieve them as Bar objects since they are Foo objects.

Performing the cast is legal just like

Integer value = (Integer) (Object) "I'm not an Integer.";

but that doesn't mean it's safe. Use it wisely.

Upvotes: 1

Related Questions