tags:

views:

152

answers:

5

Hi!

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?

A: 

how about:

TestEnum[] arr = new TestEnum[]{null, ONE,TWO,THREE};

TestEnum one = arr[1];
Omry
Terrible hack, doesn't work well if `number` is not contiguous or negative, etc.
polygenelubricants
yes, code is known to change itself when you are not looking.
Omry
@Omry: code does change, and this technique would make maintenance a nightmare, since the array and the numbers must be manually kept in sync.
polygenelubricants
granted you need to maintain it (but really it's not such a big deal, how often do you change your enums once your code is stable? once a year?). on the other hand, it's much faster than a hashtable based solution (measure it if you don't believe me). since the author is concerned about performance it's a valid answer. you do many ugly things in the name of optimization.
Omry
-1 actually, I do _few_ ugly things in the name of optimization. When I really, really have to.
Kevin Bourrillion
and you know for a fact that the person that asked the question does not really have to?if performance is not a real issue, the original soltuion in the question will do just fine. I am done arguing. vote me down if it makes you feel right.
Omry
A: 

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
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
@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
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
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
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
+7  A: 

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.

polygenelubricants
Nice solution. That's the one I came to think of as well. I usually populate the map inside `static { ... }`
aioobe
Why lazy init? The simplest thing would be to initialize the map in a static block.
Péter Török
@Peter: added eager version. Thanks for suggestion. I wasn't really sure which one is more common technique.
polygenelubricants
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
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
@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
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
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
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
@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<Integer,V>` 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
A: 

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
+6  A: 

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
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
@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
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
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
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