简体   繁体   中英

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 ( 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM