Лучшие практики обратного просмотра Java enum
Я увидел, что в блоге было высказано предположение, что следующий способ является "разумным" для обратного просмотра с использованием getCode(int)
в перечислении 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);
}
}
Для меня статическая карта и статический инициализатор выглядят как плохая идея, и моей первой мыслью было бы написать код поиска следующим образом:
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;
}
}
Есть ли какие-либо очевидные проблемы с любым методом, и есть ли рекомендуемый способ для реализации такого поиска?
8 ответов
Maps.uniqueIndex из Google Guava довольно удобен для создания поисковых карт.
Обновление: вот пример использования Maps.uniqueIndex
с 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);
}
}
Несмотря на более высокие издержки, статическая карта хороша тем, что предлагает постоянный поиск по code
, Время поиска вашей реализации увеличивается линейно с количеством элементов в перечислении. Для небольших перечислений это просто не будет значительным.
Одна проблема с обеими реализациями (и, возможно, с перечислениями Java в целом) заключается в том, что на самом деле есть скрытое дополнительное значение, которое Status
может взять на себя: null
, В зависимости от правил бизнес-логики может иметь смысл возвращать фактическое значение перечисления или бросать Exception
, когда поиск "не удается".
Вот альтернатива, которая может быть даже немного быстрее:
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;
}
}
Конечно, это не совсем поддерживаемо, если вы хотите иметь возможность добавлять больше констант позже.
Очевидно, что карта будет обеспечивать постоянный поиск по времени, а цикл - нет. В типичном перечислении с несколькими значениями я не вижу проблемы с поиском обхода.
Вот альтернатива Java 8 (с модульным тестом):
// 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");
}
}
@AllArgsConstructor
@Getter
public enum MyEnum {
A(0),
B(1),
C(2);
private static final Map<Integer, MyEnum> LOOKUP =
Arrays.stream(MyEnum.values()).collect(Collectors.toMap(MyEnum::getStatus, Function.identity()));
private final int status;
@Nullable
public static MyEnum fromStatus(int status) {
return LOOKUP.get(status);
}
}
В Java 8 я просто добавил бы следующий метод фабрики к вашему enum и пропустил поиск Map.
public static Optional<Status> of(int value) {
return Arrays.stream(values()).filter(v -> value == v.getCode()).findFirst();
}
Оба способа совершенно верны. И у них технически одно и то же время Big-Oh.
Однако, если вы сначала сохраните все значения на карте, вы сэкономите время, необходимое для перебора набора каждый раз, когда вы хотите выполнить поиск. Итак, я думаю, что статическая карта и инициализатор - немного лучший путь.