valueof - Práctica recomendada de búsqueda inversa de Java enum
java enum get by name (7)
Lo vi sugerido en un blog que la siguiente era una forma razonable de hacer una "búsqueda inversa" usando getCode(int)
en una enumeración Java:
public enum Status {
WAITING(0),
READY(1),
SKIPPED(-1),
COMPLETED(5);
private static final Map<Integer,Status> lookup
= new HashMap<Integer,Status>();
static {
for(Status s : EnumSet.allOf(Status.class))
lookup.put(s.getCode(), s);
}
private int code;
private Status(int code) {
this.code = code;
}
public int getCode() { return code; }
public static Status get(int code) {
return lookup.get(code);
}
}
Para mí, el mapa estático y el inicializador estático parecen una mala idea, y mi primer pensamiento sería codificar la búsqueda de esa manera:
public enum Status {
WAITING(0),
READY(1),
SKIPPED(-1),
COMPLETED(5);
private int code;
private Status(int code) {
this.code = code;
}
public int getCode() { return code; }
public static Status get(int code) {
for(Status s : values()) {
if(s.code == code) return s;
}
return null;
}
}
¿Hay algún problema obvio con cualquiera de los métodos? ¿Existe alguna forma recomendada de implementar este tipo de búsqueda?
Ambas formas son perfectamente válidas. Y tienen técnicamente el mismo tiempo de ejecución Big-Oh.
Sin embargo, si guarda todos los valores en un mapa primero, ahorrará el tiempo que le lleva iterar en el conjunto cada vez que quiera hacer una búsqueda. Entonces, creo que el mapa estático y el inicializador son una manera un poco mejor de ir.
Aquí hay una alternativa de Java 8 (con prueba unitaria):
// DictionarySupport.java :
import org.apache.commons.collections4.Factory;
import org.apache.commons.collections4.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
public interface DictionarySupport<T extends Enum<T>> {
@SuppressWarnings("unchecked")
Map<Class<?>, Map<String, Object>> byCodeMap = LazyMap.lazyMap(new HashMap(), (Factory) HashMap::new);
@SuppressWarnings("unchecked")
Map<Class<?>, Map<Object, String>> byEnumMap = LazyMap.lazyMap(new HashMap(), (Factory) HashMap::new);
default void init(String code) {
byCodeMap.get(this.getClass()).put(code, this);
byEnumMap.get(this.getClass()).put(this, code) ;
}
static <T extends Enum<T>> T getByCode(Class<T> clazz, String code) {
clazz.getEnumConstants();
return (T) byCodeMap.get(clazz).get(code);
}
default <T extends Enum<T>> String getCode() {
return byEnumMap.get(this.getClass()).get(this);
}
}
// Dictionary 1:
public enum Dictionary1 implements DictionarySupport<Dictionary1> {
VALUE1("code1"),
VALUE2("code2");
private Dictionary1(String code) {
init(code);
}
}
// Dictionary 2:
public enum Dictionary2 implements DictionarySupport<Dictionary2> {
VALUE1("code1"),
VALUE2("code2");
private Dictionary2(String code) {
init(code);
}
}
// DictionarySupportTest.java:
import org.testng.annotations.Test;
import static org.fest.assertions.api.Assertions.assertThat;
public class DictionarySupportTest {
@Test
public void teetSlownikSupport() {
assertThat(getByCode(Dictionary1.class, "code1")).isEqualTo(Dictionary1.VALUE1);
assertThat(Dictionary1.VALUE1.getCode()).isEqualTo("code1");
assertThat(getByCode(Dictionary1.class, "code2")).isEqualTo(Dictionary1.VALUE2);
assertThat(Dictionary1.VALUE2.getCode()).isEqualTo("code2");
assertThat(getByCode(Dictionary2.class, "code1")).isEqualTo(Dictionary2.VALUE1);
assertThat(Dictionary2.VALUE1.getCode()).isEqualTo("code1");
assertThat(getByCode(Dictionary2.class, "code2")).isEqualTo(Dictionary2.VALUE2);
assertThat(Dictionary2.VALUE2.getCode()).isEqualTo("code2");
}
}
Aquí hay una alternativa que puede ser incluso más rápida:
public enum Status {
WAITING(0),
READY(1),
SKIPPED(-1),
COMPLETED(5);
private int code;
private Status(int code) {
this.code = code;
}
public int getCode() { return code; }
public static Status get(int code) {
switch(code) {
case 0: return WAITING;
case 1: return READY;
case -1: return SKIPPED;
case 5: return COMPLETED;
}
return null;
}
}
Por supuesto, esto no es realmente mantenible si desea poder agregar más constantes más adelante.
Aunque tiene una sobrecarga superior, el mapa estático es bueno porque ofrece búsqueda de tiempo constante por code
. El tiempo de búsqueda de su implementación aumenta linealmente con la cantidad de elementos en la enumeración. Para enumeraciones pequeñas, esto simplemente no contribuirá significativamente.
Un problema con ambas implementaciones (y, posiblemente, con las enumeraciones de Java en general) es que realmente hay un valor oculto adicional que un Status
puede asumir: null
. Dependiendo de las reglas de la lógica comercial, puede tener sentido devolver un valor de enum real o lanzar una Exception
cuando la búsqueda "falla".
En Java 8 simplemente agregaría el siguiente método de fábrica a su enumeración y omitiría el mapa de búsqueda.
public static Optional<Status> of(int value) {
return Arrays.stream(values()).filter(v -> value == v.getCode()).findFirst();
}
Obviamente, el mapa proporcionará una búsqueda de tiempo constante mientras que el bucle no lo hará. En una enumeración típica con pocos valores, no veo un problema con la búsqueda transversal.
Maps.uniqueIndex de Google''s Guava es bastante útil para construir mapas de búsqueda.
Actualización: Aquí hay un ejemplo usando Maps.uniqueIndex
con Java 8:
public enum MyEnum {
A(0), B(1), C(2);
private static final Map<Integer, MyEnum> LOOKUP = Maps.uniqueIndex(
Arrays.asList(MyEnum.values()),
MyEnum::getStatus
);
private final int status;
MyEnum(int status) {
this.status = status;
}
public int getStatus() {
return status;
}
@Nullable
public static MyEnum fromStatus(int status) {
return LOOKUP.get(status);
}
}