16

I have written an enum class and I want to either get the attribute by type or get the type by attribute, but it seems impossible.

public enum AreaCode {
    area1(7927),
    area2(7928),
    area3(7929);

    private final int ac;

    AreaCode(int ac) {
        this.ac = ac;
    }

    int areaCode(){
        return ac;
    }

    AreaCode area(int n) {
        switch (n) {
            case 7927: return AreaCode.area1;
            case 7928: return AreaCode.area2;
            case 7929: return AreaCode.area3;
        }
    }
}

The code above will not compile. How to make area(int n) work?

Thetsu
  • 345
  • 2
  • 3
  • 6
  • Your `area()` method really should be `static` (i.e., a static factory), otherwise you'd always need an instance of an `AreaCode` in order to get an `AreaCode` by it's instance field. – scottb Jan 24 '17 at 00:24
  • Possible duplicate of [Get enum by its inner field](http://stackoverflow.com/questions/2780129/get-enum-by-its-inner-field) – scottb Jan 24 '17 at 00:37
  • Enum constants are, well, constants and should be written in UPPER_SNAKE_CASE. – MC Emperor Jan 22 '21 at 15:17

11 Answers11

41

Apart from the issues pointed at by the other posters, I'd rewrite the method to avoid duplicating the information (keep it DRY!):

public static AreaCode area(int n) {
  for (AreaCode c : values()) {
    if (c.ac == n) {
      return c;
    }
  }
  // either throw the IAE or return null, your choice.
  throw new IllegalArgumentException(String.valueOf(n));
}
Joachim Sauer
  • 302,674
  • 57
  • 556
  • 614
  • 5
    Works, but if there are ever a large number of area codes, this will become inefficient. A Map seems better. – G_H Oct 25 '11 at 11:53
  • 3
    @G_H: that inefficiency is **highly** unlikely to have any effect on the overall performance of your application, unless you really have thousands of area codes (and even then, you probably won't notice it). And if *that* is the case, then chances are you shouldn't be using an `enum` for this in the first place. – Joachim Sauer Oct 25 '11 at 11:59
  • 4
    True about not using an enum for that number of area codes. Still, a Map is a clean solution that limits the amount of code, and it will make it easier to move to a regular class later. Premature optimization should be avoided, but choosing the most suitable data structures for a problem is not premature in my opinion. – G_H Oct 25 '11 at 12:09
  • 1
    @G_H i totally agree especially since using a Map wouldnt have much more overhead. – Stefan Oct 25 '11 at 14:39
  • 2
    If the `area()` function accounts for a small percentage of total application execution time (which is most likely), then optimizing the function as a hash map is very unlikely to improve real world application performance, even if the function is an order of magnitude faster. The proposition that *"...Map is a clean solution that limits the amount of code"* does not account for the give and take nature of any optimization. Namely, in this case, that you are trading execution time for a longer initialization time and a larger memory footprint (significant in particular environments). – scottb Jan 24 '17 at 00:19
25

As they say, there is more than one way to skin a cat. First off, enum values should be uppercase (words delimited by underscores) as they are constant values and should be treated as such by Java naming conventions. At the very least, they should begin with a capital letter as all class names should.

public enum AreaCode {
    AREA_1(7927),
    AREA_2(7928),
    AREA_3(7929);

    private int areaCode;

    private AreaCode(int areaCode) {
        this.areaCode = areaCode;
    }

    public int getAreaCode() {
        return areaCode;
    }
}

Now, there are three ways to retrieve an enum by an instance variable. A switch statement, a loop with an equality condition, and a lookup map. The last scenario may add more memory to your program, but if you need to lookup a lot of enums quickly, this will help you do it at a constant rate O(1) time.

Each of the enum classes below are act identical, but each one does something different internally. By adding the following main() method to any of these classes, you will get the same result.

public static void main(String[] args) {
    System.out.println(retrieveByAreaCode(7928));
}

The example above will print:

AreaCode[name="AREA_2", value="7928"]

Switch

Lookup is O(1) (constant time), but you need to hard-code each case (not very dynamic).

public enum AreaCode {
    AREA_1(7927),
    AREA_2(7928),
    AREA_3(7929);

    private int areaCode;

    private AreaCode(int areaCode) {
        this.areaCode = areaCode;
    }

    public int getAreaCode() {
        return areaCode;
    }

    public static AreaCode retrieveByAreaCode(int n) {
        switch (n) {
            case 7927:
                return AreaCode.AREA_1;
            case 7928:
                return AreaCode.AREA_2;
            case 7929:
                return AreaCode.AREA_3;
            default:
                return null;
        }
    }

    @Override
    public String toString() {
        return String.format("%s[name=\"%s\", value=\"%d\"]",
                this.getClass().getName(), this.name(), this.getAreaCode());
    }
}

Loop

Lookup is O(n) (linear time), so you need to loop over each value until you find a match, but you do need to hard-code each case (dynamic).

public enum AreaCode {
    AREA_1(7927),
    AREA_2(7928),
    AREA_3(7929);

    private int areaCode;

    private AreaCode(int areaCode) {
        this.areaCode = areaCode;
    }

    public int getAreaCode() {
        return areaCode;
    }

    public static AreaCode retrieveByAreaCode(int n) {
        for (AreaCode areaCode : AreaCode.values()) {
            if (areaCode.getAreaCode() == n) {
                return areaCode;
            }
        }

        return null;
    }

    @Override
    public String toString() {
        return String.format("%s[name=\"%s\", value=\"%d\"]",
                this.getClass().getName(), this.name(), this.getAreaCode());
    }
}

Lookup

Lookup is O(1) (constant time), and you do not need to hard-code each value (dynamic), but you need to store the map which takes up memory.

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public enum AreaCode {
    AREA_1(7927),
    AREA_2(7928),
    AREA_3(7929);

    private static final Map<Integer, AreaCode> LOOKUP_MAP;
    private int areaCode;

    static {
        LOOKUP_MAP = new HashMap<Integer, AreaCode>();
        for (AreaCode areaCode : AreaCode.values()) {
            LOOKUP_MAP.put(areaCode.getAreaCode(), areaCode);
        }
        LOOKUP_MAP = Collections.unmodifiableMap(LOOKUP_MAP);
    }

    private AreaCode(int areaCode) {
        this.areaCode = areaCode;
    }

    public int getAreaCode() {
        return areaCode;
    }

    public static AreaCode retrieveByAreaCode(int n) {
        return LOOKUP_MAP.get(n);
    }

    @Override
    public String toString() {
        return String.format("%s[name=\"%s\", value=\"%d\"]",
                this.getClass().getName(), this.name(), this.getAreaCode());
    }
}

Generic Approach

EnumUtils.java

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class EnumUtils {
    public static interface EnumProperty<T extends Enum<T>, U> {
        U getValue(T type);
    }

    public static <T extends Enum<T>, U> Map<U, T> createLookup(Class<T> enumTypeClass, EnumProperty<T, U> prop) {
        Map<U, T> lookup = new HashMap<U, T>();

        for (T type : enumTypeClass.getEnumConstants()) {
            lookup.put(prop.getValue(type), type);
        }

        return Collections.unmodifiableMap(lookup);
    }
}
import java.util.Map;

public enum AreaCode {
    AREA_1(7927),
    AREA_2(7928),
    AREA_3(7929);

    private static final EnumUtils.EnumProperty<AreaCode, Integer> ENUM_PROP;
    private static final Map<Integer, AreaCode> LOOKUP_MAP;

    static {
        ENUM_PROP = new EnumUtils.EnumProperty<AreaCode, Integer>() {
            @Override
            public Integer getValue(AreaCode code) {
                return code.getAreaCode();
            }
        };
        LOOKUP_MAP = EnumUtils.createLookup(AreaCode.class, ENUM_PROP);
    }

    private int areaCode;

    private AreaCode(int areaCode) {
        this.areaCode = areaCode;
    }

    public int getAreaCode() {
        return areaCode;
    }

    public static AreaCode retrieveByAreaCode(int n) {
        return LOOKUP_MAP.get(n);
    }

    @Override
    public String toString() {
        return String.format("%s[name=\"%s\", value=\"%d\"]",
                this.getClass().getName(), this.name(), this.getAreaCode());
    }
}
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
  • Very good run down of the three approaches. Only comment I'd have is that the loop in the static initializer block in the lookup approach might be extraneous - using the enum constructor to populate the map as this answer shows is a little bit more slick: http://stackoverflow.com/a/7888738/1563178 – icyitscold Jun 03 '15 at 18:44
  • Yeah, I know that that approach adds them to the map inside the constructor, but... that map is mutable, which *may cause* threading issues. My implementation is actually slicker, because I use a static-block which, in my opinion, is much more clever, because the map is populated before it is sealed-up. – Mr. Polywhirl Jun 03 '15 at 20:01
  • Well the enum instances are all constructed during static initialization too so it should all occur on the same thread. Even so, declaring your map as final doesn't make it immutable - you need to pass it through a`Collections.unmodifiableMap()` call to "seal it up" and prevent modifications to its contents – icyitscold Jun 05 '15 at 14:38
16

All you need to do is add a default case so the method always returns something or throws an exception:

AreaCode area(int n){
    switch (n) {
    case 7927: return AreaCode.area1;
    case 7928: return AreaCode.area2;
    case 7929: return AreaCode.area3;
    default: return null;
    }
}

or perhaps better

AreaCode area(int n){
    switch (n) {
    case 7927: return AreaCode.area1;
    case 7928: return AreaCode.area2;
    case 7929: return AreaCode.area3;
    default: throw new IllegalArgumentException(String.valueOf(n));
    }
}
Michael Borgwardt
  • 342,105
  • 78
  • 482
  • 720
  • 1
    Liked your IllegalArgumentException approach. – Efthymis Oct 25 '11 at 11:36
  • 3
    The main problem with this solution is that it is not robust and it represents a potential maintenance problem. Every time a new enum constant is added, the switch-case block needs to be edited. The advantage of the solution posted by @Joachim Sauer is that it is valid for an arbitrary number of enum constants, even if the enum evolves over time. – scottb Jan 24 '17 at 00:14
4

I have written a helper to avoid to pollute enum code and which lets you get Enum of any type by attribute. You won't have anymore to declare specific methods on each enum type.

In your case, you can use it like following (your getter must be public):

// Java 8
AreaCode area = FunctionalEnumHelper.getEnum(AreaCode.class, AreaCode::areaCode, 7927); // value is area1

// Java 6
AreaCode area = EnumHelper.getEnum(AreaCode.class, 7927); // value is area1

Details:

Given the following enum :

public enum Move {
    FORWARD("F"),
    RIGHT("R"),
    LEFT("L");

    private String label;

    private Move(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label; 
    }
}

Helpers

With Java 8: uses functional programming

import java.util.function.Function;

/**
 * Helper to get an {@link Enum} of any Type by attribute or method
 *
 */
public final class FunctionalEnumHelper {

    // Constructors
    //-------------------------------------------------

    /**
     * Private constructor
     * A helper should not be instantiated in order to force static calls
     */
    private FunctionalEnumHelper() {}


    // Static methods
    //-------------------------------------------------

    /**
     * Get the enum of type <code>E</code> associated to the attribute
     * @param enumType
     * @param method
     * @param expectedValue
     * @return
     */
    public static <E extends Enum<E>, R> E getEnum(final Class<E> enumType, final Function<E, R> method, final R expectedValue) {
        E enumVariable = null;
        E[] values = enumType.getEnumConstants();
        if(values != null) {
            for(E e : values) {
                if(e != null) {
                    Object value = method.apply(e);
                    if(value == null && expectedValue == null || value != null && value.equals(expectedValue)) {
                        enumVariable = e;
                        break;
                    }
                }
            }
        }
        return enumVariable;
    }

    /* Functional style */
    public static <E extends Enum<E>, R> E getEnum(final Class<E> enumType, final Function<E, R> method, final R expectedValue) {
        return Arrays.stream(enumType.getEnumConstants())
                     .filter(e -> {
                        final Object value = method.apply(e);
                        return value == null && expectedValue == null || value != null && value.equals(expectedValue);
                      })
                     .findAny()
                     .orElse(null);
    }

}

Use:

Move move = FunctionalEnumHelper.getEnum(Move.class, Move::getLabel, "F") // value is Move.FORWARD

With Java 6: uses the reflection API

import java.lang.annotation.ElementType;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper to get an {@link Enum} of any Type by attribute or method
 *
 */
public final class EnumHelper {

    private static final Logger logger = LoggerFactory.getLogger(EnumHelper.class);

    // Constructors
    //-------------------------------------------------

    /**
     * Private constructor
     * A helper should not be instantiated in order to force static calls
     */
    private EnumHelper() {}


    // Static methods
    //-------------------------------------------------

    /**
     * Get the enum of type <code>E</code> which first attribute has value {@link obj}.
     * @param enumType
     * @param value
     * @return
     */
    public static <E extends Enum<E>> E getEnum(final Class<E> enumType, final Object value) {
        return getEnum(enumType, null, value);
    }

    /**
     * Get the enum of type <code>E</code> which attribute {@link attributeName} has value {@link obj}.
     * @param enumType
     * @param value
     * @return
     */
    public static <E extends Enum<E>> E getEnum(final Class<E> enumType, final String attributeName, final Object value) {
        return getEnum(enumType, ElementType.FIELD, attributeName, value);
    }

    /**
     * Get the enum of type <code>E</code> associated to the attribute
     * @param enumType
     * @param elementType
     * @param name
     * @param value
     * @return
     */
    public static <E extends Enum<E>> E getEnum(final Class<E> enumType, final ElementType elementType, final String name, final Object value) {
        E enumVariable = null;
        E[] enumObjs = enumType.getEnumConstants();
        if(enumObjs != null) {
            ReflectionData reflectionData = new ReflectionData();
            for(E enumObj : enumObjs) {
                if(enumObj != null) {
                    Object val = getValue(reflectionData, elementType, name, enumObj);
                    if(val == null && value == null || val != null && val.equals(value)) {
                        enumVariable = enumObj;
                        break;
                    }
                }
            }
        }
        return enumVariable;
    }

    private static Object getValue(final ReflectionData reflectionData, final ElementType type, final String name, final Object obj) {
        Object value = null;
        final String parameter = name != null ? name.trim() : null;
        switch(type) {
            case FIELD  : value = getFieldValue(reflectionData, obj, parameter); break;
            case METHOD : value = callMethod(reflectionData, obj, parameter);        break;
            default     : throw new IllegalArgumentException("The elementType '" + type.toString() + "' is not allowed!");
        }
        return value;
    }

    /**
     * Get the attribute value
     * @param reflectionData
     * @param obj
     * @param fieldName
     * @return
     */
    private static Object getFieldValue(final ReflectionData reflectionData, final Object obj, final String fieldName) {
        Object value = null;
        try {
            Field field = reflectionData.getField();
            if(field == null) {
                field = computeField(obj, fieldName);
                reflectionData.setField(field);
            }
            boolean accessible = field.isAccessible();
            field.setAccessible(true);
            value = field.get(obj);
            field.setAccessible(accessible);
        }
        catch (NoSuchFieldException | SecurityException e) {
            logger.error("No attribute {} : {}", fieldName, e.getMessage());
        }
        catch (IllegalArgumentException | IllegalAccessException e) {
            logger.error(e.getMessage());
        }
        return value;
    }

    private static Field computeField(final Object obj, final String fieldName) throws NoSuchFieldException {
        Field field = null;
        if(fieldName != null) {
            field = obj.getClass().getDeclaredField(fieldName);
        }
        else {
            Field[] fields = obj.getClass().getDeclaredFields();
            if(fields != null) {
                int position = obj.getClass().getEnumConstants().length;
                field = fields[position];
            }
        }
        return field;
    }

    /**
     * Call the method
     * @param reflectionData
     * @param obj
     * @param methodName
     * @return
     */
    private static Object callMethod(final ReflectionData reflectionData, final Object obj, final String methodName) {
        Object returnValue = null;
        try {
            Method method = reflectionData.getMethod();
            if(method == null) {
                method = obj.getClass().getMethod(methodName);
                reflectionData.setMethod(method);
            }

            returnValue = method.invoke(obj);
        }
        catch (SecurityException | NoSuchMethodException e) {
            logger.error("No method {} : {}", methodName, e.getMessage());
        }
        catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
            logger.error("Error when calling method {} on class {} : {}", methodName, obj.getClass(), e.getMessage());
        }
        return returnValue;
    }

    private static class ReflectionData {
        private Field field;
        private Method method;

        public Field getField() {
            return field;
        }
        public Method getMethod() {
            return method;
        }
        public void setField(final Field field) {
            this.field = field;
        }
        public void setMethod(final Method method) {
            this.method = method;
        }
    }

}

Use:

// Basic use
Move move = EnumHelper.getEnum(Move.class, "F"); // value is Move.FORWARD

You can also specify which attribute you want to use (by default it is the first attribute of the Enum)

// Get by attribute
Move move = EnumHelper.getEnum(Move.class, "label", "F");
// Get by method
Move move = EnumHelper.getEnum(Move.class, ElementType.METHOD, "getLabel", "F");

Benefits

The code is centralised and you don't need to code the same handling into each enumeration. Don't reinvent the wheel ! It is easy to use, it increases the productivity and enums remain cleans.

Disadvantages

Execution time: The complexity is O(n), so not as well as accessing to a static hashmap declared in an enumeration Type (which is O(1)). Otherwise, because it uses the reflection API (java 6) or Functional (java 8), performance are slower than with a standard code fragment. It is, by magnitude, more expensive.

However it is possible to add a cache system (EhCache, map..).

Safety: This helper can throw exceptions at runtime if you call it with bad arguments, while standard code would have produced errors during the compilation.


Performance test

The reflection API is slow, so it is not production friendly ! Don't use it in a production environment with time constraints.

Just add a static method Move::getMove for test comparison:

public enum Move {
    FORWARD("F"),
    RIGHT("R"),
    LEFT("L");

    private String label;

    private Move(final String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }

    // Only used by regular test
    public static Move getMove(final String label) {
        Move move = null;
        for(Move curr : Move.values()) {
            if(curr.label.equals(label)) {
                move = curr;
                break;
            }
        }
        return move;
    }
}

Now we can compare performance of each solution:

public class Main {

    public static void main(final String[] args) {
        int nbrIterations = 1000000;
        doTrivial(nbrIterations);
        doRegular(nbrIterations);
        doFunctional(nbrIterations);
        doReflection(nbrIterations);
    }

    private static void doTrivial(final int nbrIterations) {
        long start = System.currentTimeMillis();
        for (int i=0; i<nbrIterations; ++i) {
            Move.valueOf("FORWARD");
            Move.valueOf("RIGHT");
            Move.valueOf("LEFT");
        }
        System.out.println("Trivial: " + (System.currentTimeMillis() - start) + "ms");
    }

    private static void doRegular(final int nbrIterations) {
        long start = System.currentTimeMillis();
        for (int i=0; i<nbrIterations; ++i) {
            Move.getMove("F");
            Move.getMove("R");
            Move.getMove("L");
        }
        System.out.println("Regular: " + (System.currentTimeMillis() - start) + "ms");
    }

    private static void doFunctional(final int nbrIterations) {
        long start = System.currentTimeMillis();
        for (int i=0; i<nbrIterations; ++i) {
            FunctionalEnumHelper.getEnum(Move.class, Move::getLabel, "F");
            FunctionalEnumHelper.getEnum(Move.class, Move::getLabel, "R");
            FunctionalEnumHelper.getEnum(Move.class, Move::getLabel, "L");
        }
        System.out.println("Functional: " + (System.currentTimeMillis() - start) + "ms");
    }

    private static void doReflection(final int nbrIterations) {
        long start = System.currentTimeMillis();
        for (int i=0; i<nbrIterations; ++i) {
            EnumHelper.getEnum(Move.class, "F");
            EnumHelper.getEnum(Move.class, "R");
            EnumHelper.getEnum(Move.class, "L");
        }
        System.out.println("Reflection (argument): " + (System.currentTimeMillis() - start) + "ms");

        long start2 = System.currentTimeMillis();
        for (int i=0; i<nbrIterations; ++i) {
            EnumHelper.getEnum(Move.class, ElementType.METHOD, "getLabel", "F");
            EnumHelper.getEnum(Move.class, ElementType.METHOD, "getLabel", "R");
            EnumHelper.getEnum(Move.class, ElementType.METHOD, "getLabel", "L");
        }
        System.out.println("Reflection (method): " + (System.currentTimeMillis() - start2) + "ms");
    }

}

The results are: Trivial: 28ms | Regular: 53ms | Functional: 171ms | Reflection (argument): 890ms | Reflection (method): 800ms

This benchmark shows that the functional solution is a little more expensive than the regular solution (with ugly code in enums..), but it remains acceptable. The solution with reflection is beautiful to read but it is not suitable to an environment with time constraint.

Fabienr_75
  • 539
  • 7
  • 15
4

Here is another way (using Guava and Java 8) to create a Map for a lookup:

import com.google.common.collect.Maps;

public enum AreaCode {
  area1(7927),
  area2(7928),
  area3(7929);

  private final int ac;

  private final static Map<Integer, AreaCode> AREA_BY_CODE =
      Maps.uniqueIndex(EnumSet.allOf(AreaCode.class), AreaCode::areaCode);

  AreaCode(int ac) {
    this.ac = ac;
  }

  public static AreaCode area(int n) {
    return AREA_BY_CODE.get(n);
  }

  int areaCode() {
    return ac;
  }
}
Philipp Paland
  • 329
  • 4
  • 9
4

I suggest to add static map that maps the integers to area codes and then just use this map.

public enum AreaCode {
area1(7927), area2(7928), area3(7929);
private final int ac;
private static Map<Integer, AreaCode> id2code = new HashMap<Integer, AreaCode>();

AreaCode(int ac) {
    this.ac = ac;
    id2code.put(ac, this);
}

int areaCode(){
    return ac;
}

AreaCode area(int n){
     return id2code.get(n);

    }
}

}
AlexR
  • 114,158
  • 16
  • 130
  • 208
  • 1
    Although an answer was already accepted, I'd say this is the most efficient approach. You'll require a switch case for every enum constant, making for a lot of repetition. This will automatically prime the map, plus it has the best performance for reverse lookups. – G_H Oct 25 '11 at 11:54
  • 7
    Cannot refer to the static enum field AreaCode.id2code within an initializer – Thetsu Oct 25 '11 at 12:40
2

The reason it doesn't compile is that there's a missing return statement. You only return something for the cases that are recognized. I'd advise you to add a default case that returns something indicating the area code isn't known. Either an enum constant with name unknown or null could do the job.

G_H
  • 11,739
  • 3
  • 38
  • 82
2

You can use the next construction

public enum AreaCode {
  area1(7927),
  area2(7928),
  area3(7929);

  private static final Map<Integer, AreaCode> idMap = new HashMap<Integer, AreaCode>();

  static {
      for (AreaCode areaCode : AreaCode.values()) {
          idMap.put(areaCode.id, areaCode);
      }
  }

  private Integer id;
  private AreaCode(Integer id) {
      this.id = id;
  }

  public static AreaCode getById(Integer id) {
      return idMap.get(id);
  }
}
Scratte
  • 3,056
  • 6
  • 19
  • 26
PonomarevMM
  • 452
  • 3
  • 4
1

This way you make it clear on the method signature that you might or not find the enum.

 public static Optional<AreaCode> getAreaCode(int area_code){
       return Arrays.stream(AreaCode.values()).filter(e -> e.ac == area_code).findFirst();
  }
dreamcrash
  • 47,137
  • 25
  • 94
  • 117
1

The method should be static, and should return something in every case. Make it return null in the default case, or make it throw an IllegalArgumentException (or some other exception) : it's up to you.

Note: reading the compiler error message should guide you.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
0

You should include a default clause in your switch statement, because the compiler does not understand what to do when n is not 7927, 7928 or 7929.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Efthymis
  • 1,326
  • 11
  • 13