This convoluted generics pattern crashes Eclipse -

2019-06-17 02:41发布

问题:

(I'm using Eclipse Luna 4.4.0, JDK 1.8.0_05)

I'm making a game, and the topology of the game world can be roughly broken down into World -> Level -> Tile, where a Tile is a a small unit of terrain. I have three projects set up, one which holds some base classes for those structures, and the other two are server and client which extend the structures in the base project for the additional things that each of those projects need. Like so:

Base project:

public class BaseWorld{/* ...code... */}
public class BaseLevel{/* ...code... */}
public class BaseTile{/* ...code... */}

In both server and client projects:

public class World extends BaseWorld{/* ...extended code... */}
public class Level extends BaseLevel{/* ...extended code... */}
public class Tile extends BaseTile{/* ...extended code... */}

Now in many parts of the base project, there are methods which return the base classes, but in the server and client projects this typically means I have to override the methods and cast to the sub-type, because the sub projects exclusively using the sub types. Like so:

public class BaseLevel{
  BaseLevel getNextLevel(){/* ...code... */}
}

public class Level extends BaseLevel{
  Level getNextLevel(){
    return (Level)super.getNextLevel();
  }
}

And that's sort of a pain to maintain. I discovered that I can use generics to solve some of this, with the following pattern:

public class BaseLevel<Level extends BaseLevel<Level>>{
  Level getNextLevel(){/* ...code... */}
}

public class Level extends BaseLevel<Level>{
  //All sorted! getNextLevel() already returns this subclass type.
}

Well I had this crazy idea to bend the above pattern to breaking point and be able to use all the subclasses everywhere in the all the superclasses. Now I must say that I in no way endorse the following code as good programming, I just had the idea and took it as far as I could just to see how it would work out. Anyway, I came up with the following monstrosity.

public class BaseWorld<Tile extends BaseTile<Tile, Level, World>
                      ,Level extends BaseLevel<Tile, Level, World>
                      ,World extends BaseWorld<Tile, Level, World>>{
  /* ...code... */
}

public class BaseLevel<Tile extends BaseTile<Tile, Level, World>
                      ,Level extends BaseLevel<Tile, Level, World>
                      ,World extends BaseWorld<Tile, Level, World>>{
  /* ...code... */
}

public class BaseTile<Tile extends BaseTile<Tile, Level, World>
                 ,Level extends BaseLevel<Tile, Level, World>
                 ,World extends BaseWorld<Tile, Level, World>>{
  /* ...code... */
}

Now, whenever I need to return any of the classes from any other, I can without having to use any casting whatsoever in the sub-projects!

Well unfortunately not, because when Eclipse rebuilds the workspace:

A stack overflow error has occurred.
You are recommended to exit the workbench. [...]

I guess as the generics of one class are being resolved, it has to go and resolve the generics of the other class and then gets caught in a recursive loop.

Question time:

Is this error Eclipse editor having a stroke, or the java compiler?

The above doesn't highlight in the editor as a compiler warning/error - should it, even though it seems technically valid?

Is it likely that this recursion is infinitely deep, or just very deep?

Is it possible to achieve what I'm trying to here while avoiding this error? (I once again want to emphasize that its definitely not a good pattern, I just want to know if it can work)

回答1:

Is the Eclipse editor having a stroke, or the java compiler?

The Eclipse editor in this case. The following declarations together (in different files, of course), are valid Java code.

public class BaseFoo<F extends BaseFoo<F,B>, B extends BaseBar<F,B>> {}
public class BaseBar<F extends BaseFoo<F,B> ,B extends BaseBar<F,B>> {}
public class Foo<F extends BaseFoo<F,B>,B extends BaseBar<F,B>> extends BaseFoo<F,B> {}
public class Bar<F extends BaseFoo<F,B>,B extends BaseBar<F,B>> extends BaseBar<F,B> {}

The above doesn't highlight in the editor as a compiler warning/error - should it, even though it seems technically valid?

It probably should (okay, okay, fine, I joke here...) No. This is valid Java. Just... eh...

Is it likely that this recursion is infinitely deep, or just very deep?

Infinite, and I'm guessing here (more on my guess in a moment).

Consider attempting to instantiate an instance of Foo.

Go ahead, write out an initializer using the default constructor.

Foo<Foo, Bar> foo = new Foo<Foo, Bar>();

There are four errors here, regarding the generic bounds mismatch.

Let's attempt to resolve it.

Foo<Foo<Foo,Bar>, Bar<Foo,Bar>> foo = new Foo<Foo<Foo,Bar>, Bar<Foo,Bar>>();

Now there's 12 errors. We could go on forever trying to initialize this.

Is it possible to achieve what I'm trying to here while avoiding this error? (I once again want to emphasize that its definitely not a good pattern, I just want to know if it can work)

Consider the following basic structure:

class World {
    SomeCollection<Level> levels;
}

class Level {
    SomeCollection<Tile> tiles;
}

class Tile { }

Why?

The basic structure of the preceding code is preferring composition over inheritance, and doesn't use generics except in some built-in generic collection type. One nice example of an initialization style afforded to you by this is a static builder, ending up writing something like this to initialize a World:

World gameWorld = new World.Builder().
                 .loadLevels(levelSource)
                 .loadTiles(tileset)
                 .build();

I hope this helps.

Closing notes

Do not proceed. This is an explored path for a possible solution to your problem. Don't go here again.



回答2:

What a wonderful monstrosity!

Your code compiles fine for me in Eclipse 4.4.1 and javac 1.8.0_45 and seems to work as it should. I think they had some problems with the more delicate parts of the type checking in the early versions of Java 8; maybe they have sorted that out by now.

Contrary to jdphenix's opinion I think this design could be useful. I don't see how his suggested design could have any of the advantages of yours when it comes to static type control and code reuse.

Example:

public class ElegantMonstrosity {
    public void m() {
        SubWorld world = new SubWorld();
        List<SubLevel> levels = world.getTiles(); 
        SubLevel level = levels.get(0);
        List<SubTile> tiles = level.getTiles();
        SubLevel nextLevel = level.getNextLevel();
    }
}

class BaseWorld<
    Tile extends BaseTile<Tile, Level, World>,
    Level extends BaseLevel<Tile, Level, World>,
    World extends BaseWorld<Tile, Level, World>> {

    List<Level> getTiles() { return null; }
}

class BaseLevel<
    Tile extends BaseTile<Tile, Level, World>,
    Level extends BaseLevel<Tile, Level, World>,
    World extends BaseWorld<Tile, Level, World>> {

    Level getNextLevel() { return null; }
    List<Tile> getTiles() { return null; }
}

class BaseTile<
    Tile extends BaseTile<Tile, Level, World>,
    Level extends BaseLevel<Tile, Level, World>,
    World extends BaseWorld<Tile, Level, World>> {
}

class SubTile extends BaseTile<SubTile, SubLevel, SubWorld> {}
class SubLevel extends BaseLevel<SubTile, SubLevel, SubWorld> {}
class SubWorld extends BaseWorld<SubTile, SubLevel, SubWorld> {}