Use generic enum classes in maps

xZise :

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?

Joshua Taylor :

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.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=76834&siteId=1