I can't figure out why I can't cast a self-referencing generic.
In Java, I have a self-referencing generic. There are a bunch of things (Intent
s), and strategies for looking up (resolving) those things (ResolutionStrategy
s).
The self-referencing Intent
type is defined below. I want, at compile time, to define classes that can only receive a ResolutionStrategy
that accepts the same intent.
public interface Intent<I extends Intent<I, R>, R extends Resolution>
{
void resolve(ResolutionStrategy<I, R> strategy);
R getResolution();
}
Resolution strategy is thus:
public interface ResolutionStrategy<I extends Intent<I, R>, R extends Resolution>
{
R resolve(I intent);
}
So when I'm operating on a list of these Intent
s, I don't really care what they are. However, I do want to create particular types that represent a concrete thing in my domain model. Here's an example:
public class OrgIntent implements Intent<OrgIntent, IdentifiableResolution>
{
public final String name;
public OrgIntent(String name)
{
this.name = name;
}
@Override
public void resolve(ResolutionStrategy<OrgIntent, IdentifiableResolution> strategy)
{
// Do stuff
}
@Override
public IdentifiableResolution getResolution()
{
//Return resolution got from strategy at some point in the past
return null;
}
}
IdentifiableResolution
is a simple and uninteresting implementation of Resolution
.
All good so far. The plan is then to build a nice graph of these Intent
s, then iterate over them, passing each to a ResolutionStrategyFactory
to get the relevant strategy for resolving them. However, I can't cast OrgIntent
to anything generic enough to add to a list!
private <I extends Intent<I, R>, R extends Resolution> DirectedAcyclicGraph<Intent<I, R>, DefaultEdge> buildGraph(Declaration declaration) throws CycleFoundException
{
DirectedAcyclicGraph<Intent<I, R>, DefaultEdge> dag = new DirectedAcyclicGraph<>(DefaultEdge.class);
// Does not compile
Intent<I, R> orgIntent = new OrgIntent("some name");
// Compiles, but then not a valid argument to dag.addVertex()
Intent<OrgIntent, IdentifiableResolution> orgIntent = new OrgIntent("some name");
// Compiles, but then not a valid argument to dag.addVertex()
OrgIntent orgIntent = new OrgIntent("some name");
//Then do this
dag.addVertex(orgIntent);
...
Any ideas what I should declare orgIntent
as?
Update
Thanks to @zapl I realised the generic type parameter on the method definition was a complete red herring.
This compiles, but presumably means I could somehow have an Intent
that is genericised to have any old nonsense as the first generic type?
private DirectedAcyclicGraph<Intent<?, ? extends Resolution>, DefaultEdge> buildGraph(Declaration declaration) throws CycleFoundException
{
DirectedAcyclicGraph<Intent<?, ? extends Resolution>, DefaultEdge> dag = new DirectedAcyclicGraph<>(DefaultEdge.class);
OrgIntent orgIntent = new OrgIntent("some name");
dag.addVertex(orgIntent);
Like zapl suggests in the comments, generics don't provide strong enough type guarantees to handle the pattern you're describing. In particular because Java generics are non-reified there's no way for the JVM to recover a more specific type (OrgIntent
) after it's cast to a more general type (Intent<I, R>
). Since the generic type information is lost at runtime the JVM can only rely on the concrete raw types (Intent
).
This is the same reason, for example, that you can't define two methods with different generic signatures but the same concrete signature - foo(List<String>)
and foo(List<Integer>)
both become simply foo(List)
at runtime and therefore the compiler won't allow you to define two such methods in the same class.
Broadly speaking (and I'm afraid I don't understand your use case well enough to be more precise) the solution is to explicitly associate objects with the desired generic type either via the associated Class
object or a TypeToken
. For example you might be able to get the following signature to work:
R resolve(Class<I> intentClass, I intent);
The advice offered in Effective Java Item 29: Consider typesafe heterogeneous containers also ought to be helpful:
Sometimes, however, you need more flexibility [than a fixed number of type parameters].... The idea is to parameterize the key instead of the container. Then present the parameterized key to the container to insert or retrieve a value. The generic type system is used to guarantee that the type of the value agrees with its key.
...
Java's type system is not powerful enough to express [the type relationship between keys and values]. But we know that it’s true, and we take advantage of it when it comes time to retrieve a favorite.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With