I have a class structure like this :
public class BaseClass {
}
public class ChildClass extends BaseClass {
}
Lets say I have a method getList that returns List<BaseClass>
:
public List<BaseClass> getList() {
List<BaseClass> b = new ArrayList<>();
return b;
}
List<BaseClass> b = getList();
I want to make the above method getList() generic so that it can either return an object of type List<BaseClass>
or List<ChildClass>
based on some conditions and assign the returned value to an object of type List<BaseClass>
only. The reason for this is that I am dealing with some legacy code where objects are of type List<BaseClass>
and I want to avoid refactoring them to something like List<? extends BaseClass>
as this would block existing operations like add() on these list objects.
I understand that List<BaseClass>
and List<ChildClass>
are not covarient.
However, using the below techniques I am able to cast List<ChildClass>
to List<BaseClass>
and I am confused that how it is happening behind the scenes and whether it is right to do so or not.
Using explicit casting :
List<BaseClass> b = new ArrayList<>();
List<ChildClass> c = new ArrayList<>();
b = c; // gives compilation error
b = (List<BaseClass>) c; // gives compilation error
b = (List<BaseClass>)(List<?>) c; // this works
Using Generics :
public List<ChildClass> getChildClassList() {
// code to fetch/generate list and return
}
public List<BaseClass> getBaseClassList() {
// code to fetch/generate list and return
}
public static <T extends BaseClass> List<T> getList() {
if(something) {
List<ChildClass> c = getChildClassList();
return (List<T>) c; // this cast is required here.
} else {
List<BaseClass> b = getBaseClassList();
return (List<T>) b;
}
}
List<BaseClass> = getList(); // This works perfectly fine but,
// I dont understand how casting is happening here.
// Also with cast to List<T> inside getList how is the final return type determined?
Using wildcards:
public List<ChildClass> getChildClassList() {
// code to fetch/generate list and return
}
public List<BaseClass> getBaseClassList() {
// code to fetch/generate list and return
}
public List<? extends BaseClass> getList() {
if(something) {
List<Child> c = getChildClassList();
return c;
} else {
List<Base> b = getBaseClassList();
return b;
}
}
List<BaseClass> = getList(); // Gives compilation error saying incompatible types
// Casting this, however, works fine
List<BaseClass> = (List<BaseClass>) getList();
These feel a bit like a hack to me probably because Java does not support doing a direct cast. I am afraid there is a catch here which I might be missing.
Although, these work fine for my use case, is there any implication with this approach that I should worry about?
Is it safe to write such code?
Is it a bad way of writing code? If yes, then how this should be handled?
is there any implication with this approach that I should worry about?
With the generic case, yes: you can call List<ChildClass> list = getList();
, and end up with a list full of things that aren't instances of ChildClass
.
Fundamentally, you can't do anything that results in a different type being safely returned from the method. The best you can do is the wildcard case:
List<? extends BaseClass> list = getList();
because everything in that list will either be a BaseClass
(of some flavour), or null.
You can then safely convert this to a List<BaseClass>
by copying it:
List<BaseClass> list2 = new ArrayList<>(list);
Note that if you were using something like Guava's ImmutableList
, this "copy" could be free:
ImmutableList<BaseClass> list2 = ImmutableList.copyOf(list);
Despite the name, copyOf
returns the parameter, cast, if that is an instance of ImmutableList
, because this is a safe covariant cast.
In terms of converting list
to a ChildClass
: there's not much you can do at compile time. You have to turn this into a runtime check:
// Throw an exception or something if this is false.
boolean allAreChildClass = list.stream().allMatch(e -> e == null || e instanceof ChildClass);
And then make a copy of the list, performing the casting.
List<ChildClass> list3 =
list.stream().map(ChildClass.class::cast).collect(Collectors.toList());
Or, if you can be certain that list
isn't going to be used anywhere else, you can simply cast:
List<ChildClass> list3 = (List<ChildClass>) (List<?>) list;
but you have to be really sure that list
is either not used elsewhere, or otherwise not going to be modified by anything other than the casting code.