简体   繁体   中英

How to use generics in a Map with values of different types

I have a generic Command interface:

public interface Command<T> {
    public void execute(T value);
}

And some implementations:

public class ChangeName implements Command<String>{
    public void execute(String value) {...}
}
public class SetTimeout implements Command<Integer>{
    public void execute(Integer value) {...}
}

What I need is a Map to link command names with a specific Command object:

Map<String, Command> commands = new HashMap<>();
...
commands.put("changeName", new ChangeName());

Obviously, I am getting rawtypes warnings when declaring the Map . If I use a question mark I end up with a compilation error:

Map<String, Command<?>> commands = new HashMap<>();
...
commands.get("changeName").execute("Foo"); // -> compilation error

The method execute(capture#2-of ?) in the type Command is not applicable for the arguments (String)

I know that you cannot have a typesafe heterogeneous container with a non-reifiable type ( Item 29 in Effective Java ), but what is the best approach to address this problem?

If you think about it logically, what is that highest common interface which all your command template have to satisfy?

Looking at your example of String, Integer, it seems like it can't be anything but Java Object. Try this,

Map<String, Command<? extends Object>> commands = new HashMap<>();

Edit : Basically, you are adding template information while declaring but would want to completely erase it while using it. There are two options here:

a) You don't use generics because you are not able to use them to their potential. Deal with simple Object class instead and in your specific execute functions just test for the right types.

b) Create different maps for different types. This way you would be able to use templates to their potential.

I think you need to make the Commands aware of their acceptable argument at run-time:

public abstract class Command<T> {
    private final Class<T> argumentClass;

    protected Command(Class<T> argumentClass) {
        this.argumentClass = argumentClass;
    }

    public abstract <U extends T> void execute(U argument);


    @SuppressWarnings("unchecked")
    public final <U> Command<? super U> cast(Class<U> argumentClass) {
        if (this.argumentClass.isAssignableFrom(argumentClass)) {
           return (Command<? super U>) this;
        } else {
           throw new UnsupportedOperationException("this command cannot handle argument of type " + argumentClass.getName());
        }
    }
}

Now the using code would be something like this:

private <U> void executeCommand(final String name, final U arg) {
     @SuppressWarnings("unchecked")
     Class<U> clazz = (Class<U>) arg.getClass();
     commands.get(name).cast(clazz).execute(arg);
}

The suppress-warning above is an annoying one as that cast must always be true but is a limitation of the final definition of getClass as returning Class<?> .

The map could be typed as:

Map<String, Command<?>> commands = new HashMap<>();

And each command subtype class would extend of the abstract Command class.

For example an anonymous inner class definition oa print string command to stderr:

final Command<String> printString = new Command<String>(String.class) {
    public <U extends String> void execute(U arg) {
        System.err.println(arg);
    }
};

The standalone version:

public StdErrPrintCommand extends Command<String> {

     public StdErrPrintCommand() { super(String.class); }

     @Override
     public <U extends String> void excecute(U arg) { 
            System.err.println(arg);
     }
} 

If you prefer you could extract an Command interface and rename the abstract class as AbstractCommand .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM