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?
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.