2

In my program, the user needs to input what type of players the game will have. The players are "human", "good" (for a good AI), "bad" (for a bad AI) and "random" (for a random AI). Each of these players have their own class that extend one abstract class called PlayerType.

My struggle is mapping a String to the object so I can A) create a new object using the String as sort of a key and B) get the related String from an object of its subclass

Ultimately, I just want the implicit String to only appear once in the code so I can change it later if needed without refactoring.

I've tried using just a plain HashMap, but that seems clunky with searching the keys via the values. Also, I'm guessing that I'll have to use the getInstance() method of Class, which is a little less clunky, which is okay if it's the only way.

Kyefer
  • 98
  • 2
  • 12

4 Answers4

4

What I would do is create an enum which essentially functions as a factory for the given type.

public enum PlayerTypes {
    GOOD { 
        @Override
        protected PlayerType newPlayer() { 
            return new GoodPlayer();
        }
    }, 
    BAD {
        @Override
        protected PlayerType newPlayer() { 
            return new BadPlayer();
        }
    },
    RANDOM {
        @Override
        protected PlayerType newPlayer() { 
            return new RandomPlayer();
        }
    };

    protected abstract PlayerType newPlayer();

    public static PlayerType create(String input) {
        for(PlayerTypes player : PlayerTypes.values()) {
             if(player.name().equalsIgnoreCase(input)) {
                 return player.newPlayer();
             }
        }
        throw new IllegalArgumentException("Invalid player type [" + input + "]");
    }
)

Because then you can just call it like so:

String input = getInput();
PlayerTypes.create(input);

Of course, you'll get an IllegalArgumentException which you should probably handle by trying to get the input again.

EDIT: Apparently in this particular case, you can replace that loop with just merely

return PlayerTypes.valueOf(input).newPlayer();

And it'll do the same thing. I tend to match for additional constructor parameters in the enum, so I didn't think of using valueOf(), but it's definitely cleaner.

EDIT2: Only way to get that information back is to define an abstract method in your PlayerType class that returns the PlayerTypes enum for that given type.

public class PlayerType {
    public abstract PlayerTypes getType();
}

public class GoodPlayer extends PlayerType {
    @Override
    public PlayerTypes getType() {
        return PlayerTypes.GOOD;
    }
}
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • 1
    If you are willing to picky about upper/lower-casing, you just `return PlayerTypes.valueOf(input).newPlayer();`. You'll get the IllegalArgumentException for free since that's the exception that `valueOf()` throws if it can't match up the type. – azurefrog Sep 16 '15 at 17:10
  • @azurefrog valid point, I edited it in now that I'm not on a pesky phone soft keyboard, thanks for the info – EpicPandaForce Sep 16 '15 at 17:32
  • This answers my problem to A). Could it be possible to have an instance of, say, a `GoodPlayer` and then grab name it's tied to? (for my problem B) I guess the enum has to be bidirectional – Kyefer Sep 16 '15 at 18:21
  • Answered that too now. – EpicPandaForce Sep 16 '15 at 18:30
1

I like the answer provided by Epic but I don't find maps to be clunky. So it's possible to keep a map and get the constructor call directly.

Map<String, Supplier<PlayerType> map = new HashMap<>();
map.put("human", Human::new);
Human h = map.get("human").get(); 
ChiefTwoPencils
  • 13,548
  • 8
  • 49
  • 75
  • Would this be possible to do with a non default constructors? – Kyefer Sep 16 '15 at 18:34
  • No, you'd have to use a slightly different interface that allows for parameters depending on how many you have. Or provide an init method to call on the instance. @Kyefer – ChiefTwoPencils Sep 16 '15 at 18:59
0

The two main options I can think of:

Using Class.newInstance(), as you mentioned (not sure if you had this exact way in mind):

// Set up your map
Map<String, Class> classes = new HashMap<String, Class>();
classes.put("int", Integer.class);
classes.put("string", String.class);

// Get your data
Object s = classes.get("string").newInstance();

You could use Class.getDeclaredConstructor.newInstance if you want to use a constructor with arguments (example).


Another option is using switch:

Object getObject(String identifier) {
    switch (identifier) {
        case "string": return new String();
        case "int": return new Integer(4);
    }
    return null; // or throw an exception or return a default object
}
Community
  • 1
  • 1
Bernhard Barker
  • 54,589
  • 14
  • 104
  • 138
  • The `::new` (from Java 8) as suggested in [another answer](http://stackoverflow.com/a/32614878/1711796) is perhaps better than my first option. – Bernhard Barker Sep 16 '15 at 17:30
0

One potential solution:

public class ForFunFactory {

    private ForFunFactory() {
    }

    public static AThing getTheAppropriateThing(final String thingIdentifier) {
        switch (thingIdentifier) {
            case ThingImplApple.id:
                return new ThingImplApple();
            case ThingImplBanana.id:
                return new ThingImplBanana();
            default:
                throw new RuntimeException("AThing with identifier "
                        + thingIdentifier + " not found.");
        }
    }
}

public interface AThing {
    void doStuff();
}

class ThingImplApple implements AThing {

    static final String id = "Apple";

    @Override
    public void doStuff() {
        System.out.println("I'm an Apple.");
    }

}

class ThingImplBanana implements AThing {

    static final String id = "Banana";

    @Override
    public void doStuff() {
        System.out.println("I'm a Banana.");
    }

}
Bernhard Barker
  • 54,589
  • 14
  • 104
  • 138
CasualT
  • 4,869
  • 1
  • 31
  • 53