I have a project that has the following components:
public abstract class BaseThing {
public abstract <T extends BaseThing> ThingDoer<T, String> getThingDoer();
}
public class SomeThing extends BaseThing {
public ThingDoer<SomeThing, String> getThingDoer() {
return Things.getSomeThingDoer();
}
}
public class SomeOtherThing extends BaseThing {
public ThingDoer<SomeOtherThing, String> getThingDoer() {
return Things.getSomeOtherThingDoer();
}
}
public class Things {
public ThingDoer<SomeThing, String> getSomeThingDoer {
return getThingDoer(SomeThing.class);
}
public ThingDoer<SomeOtherThing, String> getSomeOtherThingDoer {
return getThingDoer(SomeOtherThing.class);
}
private <D extends ThingDoer<T, String> D getThingDoer(Class<T> clazz) {
//get ThingDoer
}
}
public class ThingDoer<T, V> {
public void do(T thing) {
//do thing
}
}
public class DoThing {
private BaseThing thing;
public void doIt() {
thing.getThingDoer().do(thing);
}
}
I'm getting a compiler warning in SomeThing.getThingDoer()
that says:
Unchecked overriding: return type requires unchecked conversion.
Found
ThingDoer<SomeThing, String>
, requiredThingDoer<T, String>
Everthing compiles fine, and while I didn't get a chance to test out DoThing.doIt()
yet, I have no reason to believe that it won't work.
My question is, can this break and is there a better way to do this? I could make DoThing
a base class, and have subclasses for both SomeThing
and SomeOtherThing
but that doesn't seem very elegant.
EDIT: I would like to avoid making BaseThing
generic.
Let's look first at your
BaseThing
class that you don't want to make generic:This is not a generic class, but it contains a generic method. Frequently, generic methods like this are designed so that the type
<T>
is bound by the compiler based on some argument to the method. For example:public <T> Class<T> classOf(T object)
. But in your case, your method takes no arguments. That is also somewhat common, in cases where the implementation of the method returns something "universally" generic (my term) like this method from theCollections
utility class:public <T> List<T> emptyList()
. This method takes no arguments, but the type<T>
will be inferred from the calling context; it works only because the implementation ofemptyList()
returns an object that is type-safe in all cases. Due to type erasure, the method doesn't ever actually know the type ofT
when it's called.Now, back to your classes. When you create these subclasses of
BaseThing
:Here, you want to override the
abstract
method from the base class. Overriding the return type is allowed in Java as long as the return type is still valid in the context of the original method. For instance you can override a method that returnsNumber
with a specific implementation that always returnsInteger
for that method, becauseInteger
is aNumber
.With generics, however, a
List<Integer>
is not aList<Number>
. So while your abstract method is defined to returnThingDoer<T, String>
(for someT extends BaseThing
), your overloads that returnThingDoer<SomeThing, String>
andThingDoer<SomeOtherThing, String>
are not generally compatible with some unknownT
even thoughSomeThing
andSomeOtherThing
both extend fromBaseThing
.The caller (from the abstract API) expects some unknown, unenforceable
T
that cannot be guaranteed to be satisfied by either of your concrete implementations. In fact, your concrete overloads are no longer generic (they return specific, statically-bound type parameters) and that conflicts with the definition in the abstract class.EDIT: The "correct" way (no warnings) to define the abstract method should be something like:
This makes it clear to the caller that it's getting a
ThingDoer
with its first type parameter bound to something that extendsBaseThing
(so it can use it as if it were aBaseThing
) but the caller will not know the specific implementation when accessed by the abstract API.EDIT #2 - Findings from our discussion in chat...
The OP's original example usage is:
Notice how the same
thing
reference is passed back into a method in the object returned from that same thing'sgetThingDoer()
method. The object returned bygetThingDoer()
needs to be tightly bound to the concrete implementation type ofthing
(according to the OP). To me, this smells like broken encapsulation.Instead, I suggest exposing the logical operation as a part of the
BaseThing
API and encapsulating the delegation to theThingDoer
as an internal implementation detail. The resulting API would look something like:And implemented somewhat like: