views:

753

answers:

4

Hello!

I encounter a totally strange behavior of the Java compiler.
I can't cast a supertype to a subtype when cyclic generic type relation is involved.

JUnit test case to reproduce the problem:

public class _SupertypeGenericTest {

 interface ISpace<S extends ISpace<S, A>, A extends IAtom<S, A>> {
 }

 interface IAtom<S extends ISpace<S, A>, A extends IAtom<S, A>> {
 }

 static class Space
   implements ISpace<Space, Atom> {
 }

 static class Atom
   implements IAtom<Space, Atom> {
 }

 public void test() {
  ISpace<?, ?> spaceSupertype = new Space();
  IAtom<?, ?> atomSupertype = new Atom();

  Space space = (Space) spaceSupertype;  // cast error
  Atom atom = (Atom) atomSupertype;  // cast error
 }
}

Compiler error output:

_SupertypeGenericTest.java:33: inconvertible types
found   : pinetag.data._SupertypeGenericTest.ISpace<capture#341 of ?,capture#820 of ?>
required: pinetag.data._SupertypeGenericTest.Space
                Space space = (Space) spaceSupertype;
                                    ^

_SupertypeGenericTest.java:34: inconvertible types
found   : pinetag.data._SupertypeGenericTest.IAtom<capture#94 of ?,capture#48 of ?>
required: pinetag.data._SupertypeGenericTest.Atom
                Atom atom = (Atom) atomSupertype;
                                ^
2 errors

Note: I'm using Netbeans latest trunk, bundled Ant, latest Java 6 release.
I tried using Ant from command line (Netbeans generates a build.xml file) but it results in same errors.

What is wrong?
Is there an elegant way solve the problem?

The strange thing is: Netbeans doesn't mark errors (not even warnings) in given code.

EDIT:
No, now I understand nothing!
Eclipse 3.4.1 doesn't mark neither warnings nor errors, and compiles the code without trouble!!!
How can this be? I thought, using Ant from command line along with build.xml provided by Netbeans' would be neutral.
Am I missing something?

EDIT 2:
Using JDK7 library and JDK7 code format, netbeans compiles without errors/warnings!
(I'm using 1.7.0-ea-b55)

EDIT 3:
Changed title to indicate that we're dealing with a javac bug.

A: 

I've ended up using non-generics for this:

    @Test
    public void test() {
            ISpace spaceSupertype = new Space();
            IAtom atomSupertype = new Atom();

            Space space = (Space) spaceSupertype;  // ok
            Atom atom = (Atom) atomSupertype;  // ok
    }
flicken
+1  A: 

I don't claim to easily understand those complex generic types, but if you find some code that compiles in javac and doesn't in ecj (the eclipse compiler), then file a bug report with both Sun and Eclipse and describe the situations cleary (best if you also mention that you filed both bug reports and mention their respective URLs, although for Sun it may take a while before the bug is publicly accessible).

I've done that in the past and got really good responses where

  1. one of the teams figured out what the correct approach was (give compile error, warning or nothing)
  2. and the faulty compiler was fixed

Since both compilers implement the same spec, one of them is by definition wrong, if only one of them compiles the code.

For the record:

I tried to compile the sample code with javac (javac 1.6.0_13) and ecj (Eclipse Java Compiler 0.894_R34x, 3.4.2 release) and javac complained loudly and failed to produce any .class files, while ecj only complained about some unused variables (warnings) and produced all the expected .class files.

Joachim Sauer
A: 

what holds you back from using the types without wildcards?

public void test() {
    ISpace<Space, Atom> spaceSupertype = new Space();
    IAtom<Space, Atom> atomSupertype = new Atom();

    Space space = (Space) spaceSupertype;  // no error
    Atom atom = (Atom) atomSupertype;  // no error
}

that way it reads much clearer, plus it compiles and runs :) i think this would be "an elegant way solve the problem"

Andreas Petersson
No, it's just a use case. I need supertypes to work with ALL possible subtypes.
ivan_ivanovich_ivanoff
A: 

The problem might be that you're trying to cast IAtom<?, ?> to Atom (which is an Atom<Atom, Space>). How the heck is the system supposed to know that ? might be Atom and Space or not?

When you don't know the types to stick in where the generics are filled, you usually just leave off the entire thing, as in

ISpace spaceSupertype = new Space();

That generates a compiler warning (not error), but your code will still execute (though if the actual type isn't cast compatible, you'll get a runtime error).

This whole thing doesn't make sense to look at, though. You say you need strong typing, then you stick ? where the types go. Then you turn around and try to cast them.

If you need to be able to cast them to Space and Atom, you should probably just use those to start with. If you can't because you're going to stick other types in those variables eventually, your code is going to break like all heck when you change the runtime type anyway, unless you use a bunch of if/then statements (like at the end of this comment).

Really, though, if you're doing stuff this strange, I think we're looking at poor code design. Rethink how you're structuring this. Maybe you need other classes or interfaces to fulfill the functionality that you've got here.

Ask yourself, "Do I really need the strong types? What does it gain me?" It better not add fields since you're using interfaces. (Keep in mind that if the fields are only accessed through mehtods, then you're only adding methods to the public interface.) If it adds methods, then using the single interfaces IAtom and ISpace here is a bad idea because you'll only be able to use test() with that subtype anyway. test() won't generalize to other implementations of ISpace/IAtom. If all the implementations you'll be putting in here have the same methods you need for test() but not all implementations of IAtom/ISpace have them, you need an intermediate subinterface:

public interface IAtom2 extends IAtom
{
    [additional methods]
}

Then you can use IAtom2 instead of IAtom in test(). Then you've automatically got the typing you need and don't need the generics. Remember, if a set of classes all have a common public interface (set of methods and fields), that public interface is a good candidate for a supertype or interface to have. What I'm describing is something like the parallelogram, rectangle, square relationship, where you're trying to skip the rectangle part.

If you're not going to redesign, the other idea is that you drop the cyclic generics and just do instance testing via instanceof:

if (spaceSupertype instanceof Space)
{
    Space space = (Space)spaceSupertype;
    ...
}
else
...