I have a project that has the following components:
public abstract class BaseThing {
public abstract ThingDoer g
Let's look first at your BaseThing
class that you don't want to make generic:
public abstract class BaseThing {
public abstract ThingDoer getThingDoer();
}
This is not a generic class, but it contains a generic method. Frequently, generic methods like this are designed so that the type
is bound by the compiler based on some argument to the method. For example: public
. 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 the Collections
utility class: public
. This method takes no arguments, but the type
will be inferred from the calling context; it works only because the implementation of emptyList()
returns an object that is type-safe in all cases. Due to type erasure, the method doesn't ever actually know the type of T
when it's called.
Now, back to your classes. When you create these subclasses of BaseThing
:
public class SomeThing extends BaseThing {
public ThingDoer getThingDoer() {
return Things.getSomeThingDoer();
}
}
public class SomeOtherThing extends BaseThing {
public ThingDoer getThingDoer() {
return Things.getSomeOtherThingDoer();
}
}
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 returns Number
with a specific implementation that always returns Integer
for that method, because Integer
is a Number
.
With generics, however, a List
is not a List
. So while your abstract method is defined to return ThingDoer
(for some T extends BaseThing
), your overloads that return ThingDoer
and ThingDoer
are not generally compatible with some unknown T
even though SomeThing
and SomeOtherThing
both extend from BaseThing
.
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:
public abstract ThingDoer extends BaseThing, String> getThingDoer();
This makes it clear to the caller that it's getting a ThingDoer
with its first type parameter bound to something that extends BaseThing
(so it can use it as if it were a BaseThing
) 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:
BaseThing thing = /* ... */;
thing.getThingDoer().do(thing);
Notice how the same thing
reference is passed back into a method in the object returned from that same thing's getThingDoer()
method. The object returned by getThingDoer()
needs to be tightly bound to the concrete implementation type of thing
(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 the ThingDoer
as an internal implementation detail. The resulting API would look something like:
thing.doTheThing();
And implemented somewhat like:
public class SomeThing extends BaseThing {
@Override public void doTheThing() {
Things.getSomeThingDoer().do(this);
}
}
public class SomeOtherThing extends BaseThing {
@Override public void doTheThing() {
Things.getSomeOtherThingDoer().do(this);
}
}