Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Self-Referencing Generics in Java

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 (Intents), and strategies for looking up (resolving) those things (ResolutionStrategys).

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 Intents, 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 Intents, 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 image 827
EngineerBetter_DJ Avatar asked Oct 30 '22 02:10

EngineerBetter_DJ


1 Answers

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.

like image 57
dimo414 Avatar answered Nov 15 '22 06:11

dimo414