javaabstract

Creating a hierarchy of Java subclasses


I'm trying to setup a hierarchy of places in java. A place can be large or small, and larger places include smaller sublocations. So, a World contains Countries, Countries contain Cities, and so on down to Buildings and Rooms.

I have created an abstract class called Place that is the parent to all these types. Every Place contains a graph of sublocations, a parent location, and the ability to get parent's sublocations (aka one's neighbors)

The main problem I'm having is that one's parent's sublocations should be instances of one's class, but there's no way to specify this with java.

I've considered hard-coding all the types, so that City's getParent returns a Country and getSublocations returns a Graph of Building, but that creates some problems with altering the functionality of the classes or adding new classes, and involves a horrifying amount of code copying. Are there any other ways to solve this?

EDIT:

Thanks for all the answers. This is what I have so far. It's not perfect but it's not too bad either. The only code I had to copy was the implementation of getNeighbors(). It used to be implemented directly in the abstract class (this.getParent().getSublocations().getEdges(this) ) but that was only one line of code.

import java.util.HashSet;
public abstract class Place {
    //World, Region, City, Building, Room
    public enum Direction {
        East, Northeast, North, Northwest, West, Southwest, South, Southeast, Invalid;
        public static Direction fromInt(int i) {
            switch (i) {
            case 0: return East;
            case 1: return Northeast;
            case 2: return North;
            case 3: return Northwest;
            case 4: return West;
            case 5: return Southwest;
            case 6: return South;
            case 7: return Southeast;
            default: return Invalid;
            }
        }
        public static Direction fromAngle(double angle) {
            return fromInt((int) (angle+22.5)/45);
        }
    }

    protected final int locked = Integer.MAX_VALUE;
    protected int x, y; //longitude and latitude
    public abstract String getName();
    public abstract <S extends Place> Graph<S> getSublocations();
    public abstract <T extends Place> T getParent();
    public abstract <U extends Place> HashSet<Passage<U>> getNeighbors();

    public final Direction getDirection(Place otherPlace) {
        if (otherPlace == null) {
            return Direction.Invalid;
        } else if (this.getClass() == otherPlace.getClass()) {
            return Direction.fromAngle(Math.atan2(otherPlace.y-y, otherPlace.x-x));
        } else {
            return Direction.Invalid;
        }
    }
}

Solution

  • I've had this problem a few times and when you're messing with something like this, I do not recommend using generics in this case because you already know what each Place is going to be returning. Hopefully this code will help.

    public interface Place {
        // Place can be an abstract class if you want. 
        // Making it abstract could cause some problems because it might make you use generics
        // I recommend an interface
        List<? extends Place> getSublocations();
        Place getParent();
    }
    public class World implements Place {
    
        private List<Country> countries;
    
        @Override
        public List<Country> getSublocations() {
            return this.countries;
        }
    
        @Override
        public Place getParent() {
            return null;
        }
    }
    public class Country implements Place {
        private List<City> cities;
        private World parent;
    
        @Override
        public List<City> getSublocations() {
            return this.cities;
        }
    
        @Override
        public World getParent() {
            return this.parent;
        }
    }
    public class City implements Place {
        private Country parent;
    
        @Override
        public List<? extends Place> getSublocations() {
            return null;
        }
    
        @Override
        public Country getParent() {
            return this.parent;
        }
    }
    

    There shouldn't be too much copy and pasting in the code, but I would think this is your best option.