Reputation: 2379
I have a map of class names to their enum class and I have method that parses a string like "SomeEnum.FIRST"
into the actual object. But Enum.valueOf
doesn't accept Class<? extends Enum<?>>
while the map cannot store Class<T extends Enum<T>>
.
For the code, the map looks something like this:
private static final HashMap<String, Class<? extends Enum<?>>> enumsMap;
static {
enumsMap = new HashMap<>();
// These are two DIFFERENT enum classes!
registerEnum(SomeEnum.class);
registerEnum(AnotherEnum.class);
}
private static void registerEnum(Class<? extends Enum<?>> enumClass) {
enumsMap.put(enumClass.getSimpleName(), enumClass);
}
And here is the parser (removed unnecessary code):
public <T extends Enum<T>> Object[] parse(List<String> strParameters) {
Object[] parameters = new Object[strParameters.size()];
for (int i = 0; i < parameters.length; i++) {
String strParameter = strParameters.get(i);
int delim = strParameter.lastIndexOf('.');
String className = strParameter.substring(0, delim - 1);
String enumName = strParameter.substring(delim + 1);
Class<T> enumClass = (Class<T>) enumsMap.get(className);
parameters[i] = Enum.valueOf(enumClass, enumName);
}
return parameters;
}
And now if I call this parse
, my IDE (Android Studio) tells me, that "Unchecked method 'parse(List)' invocation", and afaik this is because of that generic type. If I remove it in parse
, it wouldn't compile but the warning disappears. Is there a good way around it?
Upvotes: 5
Views: 1340
Reputation: 44414
There is no safe way to have Map values whose generic type depends on the corresponding key.
You can, however, store the enum constants yourself:
private static final Map<String, Map<String, ?>> enumsMap;
static {
enumsMap = new HashMap<>();
// These are two DIFFERENT enum classes!
registerEnum(SomeEnum.class);
registerEnum(AnotherEnum.class);
}
private static <T extends Enum<T>> void registerEnum(Class<T> enumClass) {
Map<String, ?> valuesByName =
EnumSet.allOf(enumClass).stream().collect(
Collectors.toMap(Enum::name, Function.identity()));
enumsMap.put(enumClass.getSimpleName(), valuesByName);
}
public Object[] parse(List<String> strParameters) {
Object[] parameters = new Object[strParameters.size()];
for (int i = 0; i < parameters.length; i++) {
String strParameter = strParameters.get(i);
int delim = strParameter.lastIndexOf('.');
String className = strParameter.substring(0, delim);
String enumName = strParameter.substring(delim + 1);
Map<String, ?> enumValues = enumsMap.get(className);
parameters[i] = enumValues.get(enumName);
if (parameters[i] == null) {
throw new IllegalArgumentException("Class " + className
+ " does not contain constant " + enumName);
}
}
return parameters;
}
What I’ve changed:
enumsMap
is now Map<String, Map<String, ?>>
. Each value is a Map of enum constants keyed by constant name. ?
is sufficient; there is no benefit to remembering that the constant values are enums, since parse
returns Object[]
.registerEnum
has a generic type, to guarantee its argument is a valid enum type. Instead of storing the class argument, it stores that enum’s constants.parse
doesn’t need a generic type, since it returns Object[]
.parse
does not use any methods of Enum, so generic type safety is no longer a concern.strParameter.substring(0, delim);
instead of delim - 1
. You want the entire substring up to but not including the period.Upvotes: 1
Reputation: 85923
If you have enums like:
enum Foo {
A, B, C
}
enum Bar {
D, E, F
}
Then you can implement the kind of map you're talking about with the following code.
class MyEnums {
private final Map<String, Class<? extends Enum<?>>> map = new HashMap<>();
public void addEnum(Class<? extends Enum<?>> e) {
map.put(e.getSimpleName(), e);
}
private <T extends Enum<T>> T parseUnsafely(String name) {
final int split = name.lastIndexOf(".");
final String enumName = name.substring(0, split);
final String memberName = name.substring(split + 1);
@SuppressWarnings("unchecked")
Class<T> enumType = (Class<T>) map.get(enumName);
return Enum.valueOf(enumType, memberName);
}
public Object parse(String name) {
return parseUnsafely(name);
}
public Object[] parseAll(String... names) {
return Stream.of(names)
.map(this::parse)
.collect(toList())
.toArray();
}
}
This does not get around an unchecked cast, though; it only hides it from you temporarily. You can see where where SuppressWarnings
is used to muffle the warning about enumType
. It's generally good practice to apply the warning suppression in as limited a scope as possible. In this case, it's for that single assignment. While this could be a red flag in general, in the present case we know that the only values in the map are, in fact, enum classes, since they must have been added by addEnum
.
Then, it can be used as:
MyEnums me = new MyEnums();
me.addEnum(Foo.class);
me.addEnum(Bar.class);
System.out.println(me.parse("Foo.A"));
System.out.println(me.parse("Bar.E"));
System.out.println(Arrays.toString(me.parseAll("Foo.B", "Bar.D", "Foo.C")));
which prints:
A
E
[B, D, C]
You'll notice that I broke parseUnsafely
and parse
into separate methods. The reason that we don't want to expose parseUnsafely
directly is that it makes a guarantee by its return type that we cannot actually enforce. If it were exposed, then we could write code like
Bar bar = me.parseUnsafely("Foo.B");
which compiles, but fails at runtime with a cast class exception.
Upvotes: 5