43

Have enum with inner fields, kind of map.

Now I need to get enum by its inner field.

Wrote this:

package test;

/**
 * Test enum to test enum =)
 */
public enum TestEnum {
    ONE(1), TWO(2), THREE(3);

    private int number;

    TestEnum(int number) {
        this.number = number;
    }      

    public TestEnum findByKey(int i) {
        TestEnum[] testEnums = TestEnum.values();
        for (TestEnum testEnum : testEnums) {
            if (testEnum.number == i) {
                return testEnum;
            }
        }
        return null;
    }
}

But it's not very efficient to look up through all enums each time I need to find appropriate instance.

Is there any other way to do the same?

Stefano Borini
  • 138,652
  • 96
  • 297
  • 431
zeroed
  • 1,081
  • 1
  • 9
  • 18

5 Answers5

73

You can use a static Map<Integer,TestEnum> with a static initializer that populates it with the TestEnum values keyed by their number fields.

Note that findByKey has been made static, and number has also been made final.

import java.util.*;

public enum TestEnum {
    ONE(1), TWO(2), SIXTY_NINE(69);

    private final int number;    
    TestEnum(int number) {
        this.number = number;
    }

    private static final Map<Integer,TestEnum> map;
    static {
        map = new HashMap<Integer,TestEnum>();
        for (TestEnum v : TestEnum.values()) {
            map.put(v.number, v);
        }
    }
    public static TestEnum findByKey(int i) {
        return map.get(i);
    }

    public static void main(String[] args) {
        System.out.println(TestEnum.findByKey(69)); // prints "SIXTY_NINE"

        System.out.println(
            TestEnum.values() == TestEnum.values()
        ); // prints "false"
    }
}

You can now expect findByKey to be a O(1) operation.

References

Related questions


Note on values()

The second println statement in the main method is revealing: values() returns a newly allocated array with every invokation! The original O(N) solution could do a little better by only calling values() once and caching the array, but that solution would still be O(N) on average.

Community
  • 1
  • 1
polygenelubricants
  • 376,812
  • 128
  • 561
  • 623
  • 1
    Nice solution. That's the one I came to think of as well. I usually populate the map inside `static { ... }` – aioobe May 06 '10 at 10:03
  • Why lazy init? The simplest thing would be to initialize the map in a static block. – Péter Török May 06 '10 at 10:09
  • @Peter: added eager version. Thanks for suggestion. I wasn't really sure which one is more common technique. – polygenelubricants May 06 '10 at 10:13
  • just so you know, that eager version won't work in practice (due to how enums are created). you actually need to embed that static block within another nested static class. – james May 06 '10 at 14:04
  • 1
    The lazy version you posted isn't thread-safe. Consider Threads A and B both stop before map = null. A continues and stops before return. B executes map = and stops before initing values. Then A proceeds and can return an incorrect null. Granted this is not very likely, but still the code is incorrect for multi-threading. – M. Jessup May 06 '10 at 15:23
  • @MJessup: Nice comment! Yes, I did not consider concurrency at all. What about the eager version? Why did @james say it won't work in practice? I'll address all these issues with the next revision. – polygenelubricants May 06 '10 at 15:38
  • the enum parent class is initialized before the subclasses are instantiated. therefore, the values array is usually invalid within a static block within the main enum class. if this static block is in another class, then there is no problem. – james May 06 '10 at 16:22
  • 1
    hmm, upon further searching, i was slightly off. i guess this is only a problem if you are trying to access the static map within the constructor of the enum. my bad, this should work as is. – james May 06 '10 at 16:27
  • 1
    Re: "O(N)". Comparing Big-Os is useful when the N is some variable which could get large. When N is the number of constants in my enum, I know exactly how many there are, and one O(N^2) solution could be better than someone else's O(log N) solution. – Kevin Bourrillion May 06 '10 at 17:39
  • @Kevin: Point taken, but since this problem practically screams for a map, then I'd propose a different solution: ala `EnumMap`, we can have a specialized `IntMiniMap implements Map` that has `get(int)` overload (to prevent autoboxing) and does a linear-search behind the screen (or perhaps a binary-search on a sorted array depending on threshold for N). A linear-search for mapping is ugly, but if it has to be done for performance reason, then at least hide it in a specialized `Map` instead of forcing people to bite their tongues and do it all over the place. Thoughts? – polygenelubricants May 07 '10 at 01:49
  • 1
    How did 69 come into the picture? – Confuse Jan 17 '17 at 05:52
  • If the enum values are in a @JsonProperty annotation the following method could be used : https://stackoverflow.com/questions/64415585/get-enum-constant-from-jsonproperty-value/64415586#64415586 – epol Oct 18 '20 at 16:22
  • The initialization can be simplified to `final static Map byValue = Collections.unmodifiableMap(Arrays.stream(values()).collect(Collectors.toMap(e->e.number, e->e)));` (for some reason I don't understand it refuses to accept Function::identity). – Svullo Aug 14 '21 at 19:07
19

Although someone has suggested using Map<Integer, TestEnum> think twice about it.

Your original solution, especially for small enums, may be magnitudes faster than using HashMap.

HashMap will probably be not faster until your enum contains at least 30 to 40 elements.

This is one case of "If it ain't broken, don't fix it".

Alexander Pogrebnyak
  • 44,836
  • 10
  • 105
  • 121
  • What makes you think `HashMap` is so slow? It's initialized once and then it'd be much faster than calling `values()` (which must be a newly allocated array every time; try it `TestEnum.values() == TestEnum.values()` is `false`) and then going through each element. – polygenelubricants May 06 '10 at 11:52
  • 1
    @polygenelubricants: Just an empiric testing I've done once. Look at the implementation of `HashMap.get`. It's doing much more than OP's original implementation. I did this exact testing a while ago and found that it's not worth it to consider a hash map until you have at least 30 elements to traverse. That said, your implementation is perfectly correct and will get a job done. – Alexander Pogrebnyak May 06 '10 at 12:59
  • 1
    I think 30-40 might be an overstatement, but I'll try to benchmark this. I think this answer is essentially correct though, for some choice of that number, and needs to be pointed out. – Kevin Bourrillion May 06 '10 at 16:50
  • 3
    I've done a rough first benchmark using 10 constants in the enum. With small int values, the performance on my macbook is comparable -- the hashmap performance is only about 10% worse than the linear search. Using random, larger ints as values, the map performance gets worse, as every query requires allocating an Integer instance. I tested each of the 10 successful queries and 2 not-found queries in the timing loop. This is far from the final answer, but it should at least suggest that Alexander's answer is not way off-base. I'll finish up and post the benchmark code when I get a chance. – Kevin Bourrillion May 06 '10 at 17:32
  • 3
    Oh! Of course in my benchmark the findByKey method does not call values(). values() is cached in a private static field. It's sad that values() is forced to create and populate a brand new array on every call, because it was declared to return an array instead of a collection type. – Kevin Bourrillion May 06 '10 at 17:35
3

Here is the most convenient way to find enum value by its field:

public enum TestEnum {

  A("EXAMPLE_1", "Qwerty", 1),
  B("EXAMPLE_2", "Asdfgh", 2),
  C("EXAMPLE_3", "Zxcvbn", 3);

  private final String code;
  private final String name;
  private final Integer typeID;

  TestEnum(String code, String name, Integer typeID) {
    this.code = code;
    this.name = name;
    this.key = typeID;
  }

  public String getCode() {
    return code;
  }

  public String getName() {
    return name;
  }

  public Integer getKey() {
    return key;
  }

  public static TestEnum findValueByTypeId(Integer key) {
    return Arrays.stream(TestEnum.values()).filter(v ->
        v.getKey().equals(key)).findFirst().orElseThrow(() ->
        new Exception(String.format("Unknown TestEnum.key: '%s'", key)));
  }
}
Julian Kolodzey
  • 359
  • 3
  • 9
0

You should have a HashMap with the numbers as keys and the enum values as values.

This map can typically be in your repository. Then you can easily replace an int variable from the database with your preferred enum value.

If your keys (int values) are stored in a database, then I will say its bad design to carry those keys around in an enum on your business layer. If that's the case, I will recommend not to store the int value in the enum.

Espen
  • 10,545
  • 5
  • 33
  • 39
  • Could you elaborate? It seems natural to me to use an `enum` on the Java side for an enumeration represented by an `int` at the database level. What makes using an `enum` a bad design? What alternative do you suggest? – Pascal Thivent May 06 '10 at 12:03
  • @Pascal: In a layered application, your service code should not know about key values in the database. That's not loose coupling between the layers. What if you're changing database or replaces it with some sort of datagrid in for example a cloud solution? Then the database keys in the enum doesn't make any sense at all. But if you're using the enum in your integration logic, then I will say polygenelubricants post is a nice solution. – Espen May 06 '10 at 12:14
  • And if the key is a business key and not just primary/foreign key, then I would also vote up for polygenelubricants solution even if it's used on the service layer. – Espen May 06 '10 at 12:18
  • Thanks. But I still don't see the problem if you use the `int` at the persistence layer level but the `enum` at the business layer level. I don't see the difference with a Business Object that would carry his key. – Pascal Thivent May 06 '10 at 12:41
  • If you carry around an int value in the enum that's not used in a future datagrid solution or if you changes the database and the int values are wrong, then you have at best an application that's hard to read. You also risk that someone codes against the int value instead of the enum since it's available. – Espen May 06 '10 at 13:07
0

One solution is to add

public final Test[] TESTS = { null, ONE, TWO, THREE };

public static Test getByNumber(int i) {
    return TESTS[i];
}

To the enum.

If the internal data is not an integer, you could have a Map which you populate in a static { ... } initializer. This map could later be used in the getByNumber method above.

aioobe
  • 413,195
  • 112
  • 811
  • 826
  • The problem is that if you add more elements to the enum later you will need to remember to manually update this array. Goal is to avoid having this duplication and just automatically find the right value based on the input. – Adam Burley Jun 06 '22 at 16:42