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.

6
  • Register your classes with a factory using the classes' string key as the key, and the class itself as the value. Commented Sep 16, 2015 at 16:59
  • Is this continual input or just when it's launched? Commented Sep 16, 2015 at 17:02
  • probably some sort of switch statement based on the names available, and then just return a new instance of whichever matches. Commented Sep 16, 2015 at 17:04
  • @CasualT Can't do that because OP wants to use strings contained within the classes themselves as the keys. Commented Sep 16, 2015 at 17:06
  • well, you can still reference those classes in a switch... otherwise, would need to use reflection to scan dynamically. Commented Sep 16, 2015 at 17:08

4 Answers 4

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;
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

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 valid point, I edited it in now that I'm not on a pesky phone soft keyboard, thanks for the info
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
Answered that too now.
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(); 

2 Comments

Would this be possible to do with a non default constructors?
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
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
}

1 Comment

The ::new (from Java 8) as suggested in another answer is perhaps better than my first option.
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.");
    }

}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.