elm
elm

Reputation: 21

java - generic type ignored by a compiler

recently I've stumbled upon an edge case with generics that doesn't seem logical.

I've got a simple interface that returns a list of children:

public interface INode<C extends INode> {

    List<C> getChildren();

}

I would expect that if the INode type is referenced without defining the C type, the C type would be inferred by a compiler as the INode. In other words, in below code:

    public void retrieveChildren(INode node)  {

        var children = node.getChildren();

    }

children would be inferred as of List<INode> type. Instead, it's just plain List.

Worth noting that type inference works as expected with the INode method returning a single element, so when this interface:

public interface INode<C extends INode> {
    
    C getFirstChild();
    
}

is used in the following method:

    public void retrieveFirstChild(INode node)  {

        var firstChild = node.getFirstChild();

    }

firstChild is propely inferred as INode.

Is there a rationale for the jdk to work like that? Also, is there a clean way to enforce a C type for a returned list?

Many thanks.

Upvotes: 2

Views: 192

Answers (2)

Eugene
Eugene

Reputation: 120968

First of all, what you really want is:

public interface INode<C extends INode<C>> {

   List<C> getChildren();

   C getFirstChild();

}

Since you are using a raw type here :

 public void retrieveChildren(INode node)  {

     var children = node.getChildren();

}

you are going to apply erasure to INode, and the erasure of that is INode, i.e.: C extends INode is going to be erased to INode. That is explained in the JLS:

To facilitate interfacing with non-generic legacy code, it is possible to use as a type the erasure of a parameterized type or the erasure of an array type whose element type is a parameterized type. Such a type is called a raw type.

And that is visible because of two things:

INode first1 = node.getFirstChild(); // works

and if you de-compile the actual .class file (javap -c -p -v), you will see :

InterfaceMethod DeleteMe$INode.getFirstChild:()LDeleteMe$INode;

or in plain english getFirstChild returns an INode.

On the other hand List<C> getChildren(); since it itself uses generics, will be erased to List, without type arguments. So these will both compile, for example:

 List<INode> children1 = node.getChildren();
 List<String> children2 = node.getChildren();

Upvotes: 0

rzwitserloot
rzwitserloot

Reputation: 103244

children would be inferred as of List< INode> type. Instead, it's just plain List.

Once you use raw types, it infects everything. INode has typeargs, and in your signature (retrieveFirstChild), you use it raw, which means all interactions that involve any generics with that variable are also raw.

The solution is to not use raw, ever (the compiler warns, and you should heed these warnings!):

public void retrieveFirstChild(INode<?> node) {
 // note the <?> up there!
    var firstChild = node.getFirstChild();
}

now firstChild's type is INode, as expected.

Upvotes: 5

Related Questions