反向查找最佳实践

我认为 在博客上建议的是在 Java 枚举中使用 getCode(int)进行“反向查找”的合理方法:

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;
}
}

这两种方法有没有明显的问题? 有没有推荐的方法来实现这种查找?

46803 次浏览

来自 Google 的 番石榴的 Maps.uniqueIndex 对于构建查找地图非常方便。

更新: 下面是一个使用 Maps.uniqueIndex和 Java8的示例:

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可能是有意义的

这两种方式都是完全有效的,而且它们在技术上有相同的 Big-Oh 运行时间。

但是,如果首先将所有值保存到一个 Map,那么每次查找时都可以节省迭代集所需的时间。因此,我认为静态映射和初始化程序是一种更好的方法。

显然,map 将提供常量时间查找,而循环不会。在一个值很少的典型枚举中,遍历查找没有问题。

这里有一个可能更快一点的替代方案:

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;
}
}

当然,如果您希望以后能够添加更多的常量,这是不可维护的。

下面是 Java8的一个替代方案(带有单元测试) :

// 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");


}
}

在 Java8中,我只需将下面的工厂方法添加到枚举中,并跳过查找映射。

public static Optional<Status> of(int value) {
return Arrays.stream(values()).filter(v -> value == v.getCode()).findFirst();
}
@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);
}
}