- Java Enum Lookup by Name or Field Without Throwing Exceptions
- The Enum
- The Problem
- Poor Implementations
- Enum.valueOf with Try Catch (Poor)
- Find By Iteration (Poor)
- Better Implementations
- Static Map Index (Better)
- Guava Enums.getIfPresent (Recommended)
- One Step Further Indexing by Field
- Static Map Indexed by Field (Better)
- Static Map Indexed by Field Utility (Better)
- Conclusion
- BONUS — Serializing an Enum as an Object with Jackson
- Java.lang.Enum.valueOf() Method
- Declaration
- Parameters
- Return Value
- Exception
- Example
Java Enum Lookup by Name or Field Without Throwing Exceptions
Java Enums are an incredibly useful feature and often under utilized because some libraries don’t treat them as first class citizens. They are also often used properly but there is a recurring issue that plagues many code bases which has inspired this post. The problem is simple, how should you get an Enum by its name or value and ignore non existant values?
The Enum
The enum we will be using in our examples. Let’s pick a more complex enum to also showcase looking an enum up by another field.
public enum CardColor < RED, BLACK, ; >// Jackson annotation to print the enum as an Object instead of the default name. @JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum CardSuit < // Unicode suits - https://en.wikipedia.org/wiki/Playing_cards_in_Unicode SPADE("Spade", String.valueOf((char) 0x2660), CardColor.BLACK), HEART("Heart", String.valueOf((char) 0x2665), CardColor.RED), DIAMOND("Diamond", String.valueOf((char) 0x2666), CardColor.RED), CLUB("Club", String.valueOf((char) 0x2663), CardColor.BLACK), ; private final String displayName; private final String symbol; private final CardColor color; private CardSuit(String displayName, String symbol, CardColor color) < this.displayName = displayName; this.symbol = symbol; this.color = color; >public String getDisplayName() < return displayName; >public String getSymbol() < return symbol; >public CardColor getColor()
The Problem
Using Enum.valueOf is great when you know the input is valid. However if you pass in an invalid name an exception will be thrown. In some cases this is fine. Often times we would prefer to just ignore it and return null.
log.debug("Running valueOf"); for (String name : names) < try < log.debug("looking up <>found <>", name, Json.serializer().toString(CardSuit.valueOf(name))); > catch (Exception ex) < log.warn("Exception Thrown", ex); >>
2017-02-22 14:46:38.556 [main] DEBUG c.s.examples.common.EnumLookup - Running valueOf 2017-02-22 14:46:38.804 [main] DEBUG c.s.examples.common.EnumLookup - looking up SPADE found 2017-02-22 14:46:38.806 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found 2017-02-22 14:46:38.806 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found 2017-02-22 14:46:38.806 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found 2017-02-22 14:46:38.808 [main] WARN c.s.examples.common.EnumLookup - Exception Thrown java.lang.IllegalArgumentException: No enum constant com.stubbornjava.examples.common.EnumLookup.CardSuit.Missing at java.lang.Enum.valueOf(Enum.java:238) at com.stubbornjava.examples.common.EnumLookup$CardSuit.valueOf(EnumLookup.java:1) at com.stubbornjava.examples.common.EnumLookup.main(EnumLookup.java:154)
Poor Implementations
It’s unfortunate how often the following two approaches appear in code bases. Please don’t do this.
Enum.valueOf with Try Catch (Poor)
This bad practice is most commonly made by beginners. Exceptions shouldn’t be used for control flow and could have some performance implications. Don’t be lazy do it the right way.
/* * Please don't do this! Using try / catch for * control flow is a bad practice. */ public static CardSuit trycatchValueOf(String name) < try < return CardSuit.valueOf(name); >catch (Exception ex) < log.warn("Exception Thrown", ex); return null; >>
log.debug("Running trycatchValueOf"); for (String name : names) < log.debug("looking up <>found <>", name, Json.serializer().toString(CardSuit.trycatchValueOf(name))); >
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - Running trycatchValueOf 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up SPADE found 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found 2017-02-22 14:46:38.809 [main] WARN c.s.examples.common.EnumLookup - Exception Thrown java.lang.IllegalArgumentException: No enum constant com.stubbornjava.examples.common.EnumLookup.CardSuit.Missing at java.lang.Enum.valueOf(Enum.java:238) at com.stubbornjava.examples.common.EnumLookup$CardSuit.valueOf(EnumLookup.java:1) at com.stubbornjava.examples.common.EnumLookup$CardSuit.trycatchValueOf(EnumLookup.java:89) at com.stubbornjava.examples.common.EnumLookup.main(EnumLookup.java:171) 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up Missing found null
Find By Iteration (Poor)
This approach is also quite common (see here), at least the authors know not to try / catch the exceptions. What is wrong with this approach? It’s iterating over all enums until it finds the matching enum or returning null with a worst case of N where N is the number of enum values. Some may argue this is being nitpicky and its premature optimization. However, data structures and algorithms are CS fundamentals it’s not that much effort to use a Map instead of iterating a collection. Will it drastically improve performance? No, but it is a good habbit. When interviewing a candidate for a job would you be happy with a linear complexity search algorithm? You shouldn’t let this code review pass in that case.
/* * Please don't do this! It is inefficient and it's * not very hard to use Guava or a static Map as an index. */ public static CardSuit iterationFindByName(String name) < for (CardSuit suit : CardSuit.values()) < if (name.equals(suit.name())) < return suit; >> return null; >
log.debug("Running iteration"); for (String name : names) < log.debug("looking up <>found <>", name, Json.serializer().toString(CardSuit.iterationFindByName(name))); >
2017-02-22 14:46:38.808 [main] DEBUG c.s.examples.common.EnumLookup - Running iteration 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up SPADE found 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up Missing found null
Better Implementations
The following all work by using an index in the form of a Map. There are some minor differences as well as boilerplate concerns.
Static Map Index (Better)
What is the correct data structure to use for quick lookups of fixed size? A HashMap. Now with a little extra boiler plate we have a much more efficient lookup as long as we have a good hash function. A bit more verbose, it would be nice if there was a way to reduce the boilerplate.
private static final Map nameIndex = Maps.newHashMapWithExpectedSize(CardSuit.values().length); static < for (CardSuit suit : CardSuit.values()) < nameIndex.put(suit.name(), suit); >> public static CardSuit lookupByName(String name)
log.debug("Running lookupByName"); for (String name : names) < log.debug("looking up <>found <>", name, Json.serializer().toString(CardSuit.lookupByName(name))); >
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - Running lookupByName 2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up SPADE found 2017-02-22 14:46:38.810 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found 2017-02-22 14:46:38.810 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found 2017-02-22 14:46:38.813 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found 2017-02-22 14:46:38.813 [main] DEBUG c.s.examples.common.EnumLookup - looking up Missing found null
Guava Enums.getIfPresent (Recommended)
This is such a common use case our friends over at Google made a very clean and boiler plate free solution for us. Under the hood it even uses WeakReferences and WeakHashMaps. Basically this code will create a global static map keyed on the Enum’s class name and use it for lookups.
public static CardSuit getIfPresent(String name)
log.debug("Running Guava getIfPresent"); for (String name : names) < log.debug("looking up <>found <>", name, Json.serializer().toString(CardSuit.getIfPresent(name))); >
2017-02-22 14:46:38.813 [main] DEBUG c.s.examples.common.EnumLookup - Running Guava getIfPresent 2017-02-22 14:46:38.814 [main] DEBUG c.s.examples.common.EnumLookup - looking up SPADE found 2017-02-22 14:46:38.814 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found 2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found 2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found 2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up Missing found null
One Step Further Indexing by Field
This exact same approach can be used for additional fields of the enum. It’s not uncommon to want to look up an enum by its display name or some other property.
Static Map Indexed by Field (Better)
Same approach as above but indexed on the display name instead of the enum name.
private static final Map displayNameIndex = Maps.newHashMapWithExpectedSize(CardSuit.values().length); static < for (CardSuit suit : CardSuit.values()) < displayNameIndex.put(suit.getDisplayName(), suit); >> public static CardSuit lookupByDisplayName(String name)
log.debug("Running lookupByDisplayName"); for (String displayName : displayNames) < log.debug("looking up <>found <>", displayName, Json.serializer().toString(CardSuit.lookupByDisplayName(displayName))); >
2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - Running lookupByDisplayName 2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up Spade found 2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up Heart found 2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up Diamond found 2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Club found 2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Missing found null
Static Map Indexed by Field Utility (Better)
We can’t leverage Guava here since it would be difficult to create unique global keys for the static index. However that doesn’t mean we can’t make our own helpers!
public class EnumUtils < public static > Function lookupMap(Class clazz, Function mapper) < @SuppressWarnings("unchecked") E[] emptyArray = (E[]) Array.newInstance(clazz, 0); return lookupMap(EnumSet.allOf(clazz).toArray(emptyArray), mapper); >public static > Function lookupMap(E[] values, Function mapper) < Mapindex = Maps.newHashMapWithExpectedSize(values.length); for (E value : values) < index.put(mapper.apply(value), value); >return (T key) -> index.get(key); > >
Now we have a fairly boilerplate free generic solution.
private static final Function func = EnumUtils.lookupMap(CardSuit.class, e -> e.getDisplayName()); public static CardSuit lookupByDisplayNameUtil(String name)
log.debug("Running lookupByDisplayNameUtil"); for (String displayName : displayNames) < log.debug("looking up <>found <>", displayName, Json.serializer().toString(CardSuit.lookupByDisplayNameUtil(displayName))); >
2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - Running lookupByDisplayNameUtil 2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Spade found 2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Heart found 2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Diamond found 2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Club found 2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Missing found null
Conclusion
There are several ways to solve the same problem. Some are better than others.
BONUS — Serializing an Enum as an Object with Jackson
If you happened to notice our json output is a full object not just the enum name the magic comes from the Jackson annotation @JsonFormat(shape = JsonFormat.Shape.OBJECT)
Java.lang.Enum.valueOf() Method
The java.lang.Enum.valueOf() method returns the enum constant of the specified enumtype with the specified name. The name must match exactly an identifier used to declare an enum constant in this type.
Declaration
Following is the declaration for java.lang.Enum.valueOf() method
public static > T valueOf(Class enumType, String name)
Parameters
- enumType − This is the Class object of the enum type from which to return a constant.
- name − This is the name of the constant to return.
Return Value
This method returns the enum constant of the specified enum type with the specified name.
Exception
- IllegalArgumentException − if the specified enum type has no constant with the specified name, or the specified class object does not represent an enum type.
- NullPointerException − if enumType or name is null.
Example
The following example shows the usage of java.lang.Enum.valueOf() method.
package com.tutorialspoint; import java.lang.*; // enum showing Mobile prices enum Mobile < Samsung(400), Nokia(250),Motorola(325); int price; Mobile(int p) < price = p; >int showPrice() < return price; >> public class EnumDemo < public static void main(String args[]) < System.out.println("CellPhone List:"); for(Mobile m : Mobile.values()) < System.out.println(m + " costs " + m.showPrice() + " dollars"); >Mobile ret; ret = Mobile.valueOf("Samsung"); System.out.println("Selected : " + ret); > >
Let us compile and run the above program, this will produce the following result −
CellPhone List: Samsung costs 400 dollars Nokia costs 250 dollars Motorola costs 325 dollars Selected : Samsung