Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Java Enum Lookup by Name or Field Without Throwing Exceptions

DZone's Guide to

Java Enum Lookup by Name or Field Without Throwing Exceptions

Wish you could call up Enums by names or values while ignoring nonexistent ones? Wish you didn't have to deal with exceptions? Without boilerplate? You can!

· Java Zone
Free Resource

Microservices! They are everywhere, or at least, the term is. When should you use a microservice architecture? What factors should be considered when making that decision? Do the benefits outweigh the costs? Why is everyone so excited about them, anyway?  Brought to you in partnership with IBM.

Java Enums are an incredibly useful feature and are often underutilized 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 nonexistent values?

The Enum

Here's 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 String displayName;
    private String symbol;
    private 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 void setDisplayName(String displayName) {
        this.displayName = displayName;
    }
    public String getSymbol() {
        return symbol;
    }
    public void setSymbol(String symbol) {
        this.symbol = symbol;
    }
    public CardColor getColor() {
        return color;
    }
    public void setColor(CardColor color) {
        this.color = color;
    }

View on GitHub.

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. Oftentimes. 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);
    }
}

View on GitHub.


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 {"displayName":"Spade","symbol":"♠","color":"BLACK"}
2017-02-22 14:46:38.806 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found {"displayName":"Heart","symbol":"♥","color":"RED"}
2017-02-22 14:46:38.806 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found {"displayName":"Diamond","symbol":"♦","color":"RED"}
2017-02-22 14:46:38.806 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found {"displayName":"Club","symbol":"♣","color":"BLACK"}
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;
    }
}

View on GitHub.


log.debug("Running trycatchValueOf");
for (String name : names) {
    log.debug("looking up {} found {}", name, Json.serializer().toString(CardSuit.trycatchValueOf(name)));
}

View on GitHub.


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 {"displayName":"Spade","symbol":"♠","color":"BLACK"}
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found {"displayName":"Heart","symbol":"♥","color":"RED"}
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found {"displayName":"Diamond","symbol":"♦","color":"RED"}
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found {"displayName":"Club","symbol":"♣","color":"BLACK"}
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), but 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 it's 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;
}

View on GitHub.


log.debug("Running iteration");
for (String name : names) {
    log.debug("looking up {} found {}", name, Json.serializer().toString(CardSuit.iterationFindByName(name)));
}

View on GitHub.


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 {"displayName":"Spade","symbol":"♠","color":"BLACK"}
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found {"displayName":"Heart","symbol":"♥","color":"RED"}
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found {"displayName":"Diamond","symbol":"♦","color":"RED"}
2017-02-22 14:46:38.809 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found {"displayName":"Club","symbol":"♣","color":"BLACK"}
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 boilerplate, we have a much more efficient lookup as long as we have a good hash function. A bit more verbose, and it would be nice if there was a way to reduce the boilerplate.

private static final Map<String, CardSuit> nameIndex =
        Maps.newHashMapWithExpectedSize(CardSuit.values().length);
static {
    for (CardSuit suit : CardSuit.values()) {
        nameIndex.put(suit.name(), suit);
    }
}
public static CardSuit lookupByName(String name) {
    return nameIndex.get(name);
}

View on GitHub.


log.debug("Running lookupByName");
for (String name : names) {
    log.debug("looking up {} found {}", name, Json.serializer().toString(CardSuit.lookupByName(name)));
}

View on GitHub.


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 {"displayName":"Spade","symbol":"♠","color":"BLACK"}
2017-02-22 14:46:38.810 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found {"displayName":"Heart","symbol":"♥","color":"RED"}
2017-02-22 14:46:38.810 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found {"displayName":"Diamond","symbol":"♦","color":"RED"}
2017-02-22 14:46:38.813 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found {"displayName":"Club","symbol":"♣","color":"BLACK"}
2017-02-22 14:46:38.813 [main] DEBUG c.s.examples.common.EnumLookup - looking up Missing found null

This is such a common use case that our friends over at Google made a very clean and boilerplate-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) {
    return Enums.getIfPresent(CardSuit.class, name).orNull();
}

View on GitHub.


log.debug("Running Guava getIfPresent");
for (String name : names) {
    log.debug("looking up {} found {}", name, Json.serializer().toString(CardSuit.getIfPresent(name)));
}

View on GitHub.


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 {"displayName":"Spade","symbol":"♠","color":"BLACK"}
2017-02-22 14:46:38.814 [main] DEBUG c.s.examples.common.EnumLookup - looking up HEART found {"displayName":"Heart","symbol":"♥","color":"RED"}
2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up DIAMOND found {"displayName":"Diamond","symbol":"♦","color":"RED"}
2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up CLUB found {"displayName":"Club","symbol":"♣","color":"BLACK"}
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<String, CardSuit> displayNameIndex =
        Maps.newHashMapWithExpectedSize(CardSuit.values().length);
static {
    for (CardSuit suit : CardSuit.values()) {
        displayNameIndex.put(suit.getDisplayName(), suit);
    }
}
public static CardSuit lookupByDisplayName(String name) {
    return displayNameIndex.get(name);
}

View on GitHub.


log.debug("Running lookupByDisplayName");
for (String displayName : displayNames) {
    log.debug("looking up {} found {}", displayName, Json.serializer().toString(CardSuit.lookupByDisplayName(displayName)));
}

View on GitHub.


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 {"displayName":"Spade","symbol":"♠","color":"BLACK"}
2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up Heart found {"displayName":"Heart","symbol":"♥","color":"RED"}
2017-02-22 14:46:38.815 [main] DEBUG c.s.examples.common.EnumLookup - looking up Diamond found {"displayName":"Diamond","symbol":"♦","color":"RED"}
2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Club found {"displayName":"Club","symbol":"♣","color":"BLACK"}
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 <T, E extends Enum<E>> Function<T, E> lookupMap(Class<E> clazz, Function<E, T> mapper) {
        @SuppressWarnings("unchecked")
        E[] emptyArray = (E[]) Array.newInstance(clazz, 0);
        return lookupMap(EnumSet.allOf(clazz).toArray(emptyArray), mapper);
    }

    public static <T, E extends Enum<E>> Function<T, E> lookupMap(E[] values, Function<E, T> mapper) {
        Map<T, E> index = Maps.newHashMapWithExpectedSize(values.length);
        for (E value : values) {
            index.put(mapper.apply(value), value);
        }
        return (T key) -> index.get(key);
    }
}

View on GitHub.


Now we have a fairly boilerplate-free generic solution.

private static final Function<String, CardSuit> func =
        EnumUtils.lookupMap(CardSuit.class, e -> e.getDisplayName());
public static CardSuit lookupByDisplayNameUtil(String name) {
    return func.apply(name);
}

View on GitHub.


log.debug("Running lookupByDisplayNameUtil");
for (String displayName : displayNames) {
    log.debug("looking up {} found {}", displayName, Json.serializer().toString(CardSuit.lookupByDisplayNameUtil(displayName)));
}

View on GitHub.


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 {"displayName":"Spade","symbol":"♠","color":"BLACK"}
2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Heart found {"displayName":"Heart","symbol":"♥","color":"RED"}
2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Diamond found {"displayName":"Diamond","symbol":"♦","color":"RED"}
2017-02-22 14:46:38.816 [main] DEBUG c.s.examples.common.EnumLookup - looking up Club found {"displayName":"Club","symbol":"♣","color":"BLACK"}
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)

Discover how the Watson team is further developing SDKs in Java, Node.js, Python, iOS, and Android to access these services and make programming easy. Brought to you in partnership with IBM.

Topics:
java ,enum ,exceptions ,field indexing ,tutorial

Published at DZone with permission of Bill O'Neil. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}