- An interface IBar and an interface IFoo with a method:
public Set<? extends IBar> getBars ();
- Bar is a concrete implementation of IBar, and Foo is a concrete implementation of IFoo that gives a covariant return for the same method:
public Set<Bar> getBars ();
- Another method that declares it returns a Set<? extends IBar> where I know the Set actually holds instances of Bar.
- The implementation of Foo.getBars() calls this other method and intends to return the Set.
- The compiler balks because this is not acceptable:
Set<? extends IBar>() bars = x();return (Set<Bar>) bars;
An easy hack would be to subvert the Java Generics compiler checks, such as:
Set<? extends IBar>() bars = x();
return (Set<Bar>)(Set<?>) bars;
For those who don't care about type-safety, I'm sure you will jump at that solution. For those who treasure type-safety, though, this option is highly unpalatable.
For a type-safe solution, a direct cast is not acceptable, so a copy with the correct type must be generated. To provide a single method that would satisfy this requirement for any Collection type took a bit of finagling of both Java Generics and reflection. This is what I finally came up with:
public static <T,
U extends T,
C extends Collection<? extends T>,
D extends Collection<U>>
D specializedCollection (C generalized, Class<? extends U> type) {
try {
D specialized = (D) generalized.getClass().newInstance();
for (T item : generalized) {
if (type.isInstance(item)) {
specialized.add((U)item);
}
else {
throw new ClassCastException("Element not castable to " + type.getClass() + ": " + item);
}
}
return specialized;
} catch (IllegalAccessException | InstantiationException e) {
throw new IllegalArgumentException("Could not create a " + generalized.getClass().getName(), e);
}
}
- The U parameterized type had to be passed into the method because it cannot be discovered via the D type, or the C instance provided.
- Collections are supposed to provide a public no-argument constructor, so the use of reflection to create the new instance is acceptable.
- The cast of item from a type T to a type U is an acceptable cast because by the types declared, U is a subtype of T. However, this cast does not occur in the bytecode since parameterized types are deleted upon compilation.
- In order to ensure type safety, Class.isInstance() is explicitly called using the U parameterized type in order to generate the expected ClassCastException.
Set<? extends IBar>() bars = x();
return specializedCollection(bars, Bar.class);
Less efficient, sure, but now I am type safe!
This post is also available at Quantum Strides, and while you're there check out my Java Fundamentals course!