Java枚举反向查找最佳实践


76

在博客上看到它的建议是,以下是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;
    }
}

两种方法都存在明显的问题,并且有推荐的方法来实现这种查找吗?


顺便说一句,为您完成地图构建循环for(Status s : values()) lookup.put(s.code, s);
Peter Lawrey

4
使用有什么问题Enum.valueOf()吗?您无法存储字符串吗?
乔纳森

1
@Jonathan经常,您需要从二进制或数字输入中产生枚举。所以,我想没有什么Enum.valueOf()(记市值虽然),但很多时候你拿到一个字节或一个数字开始。并且请:如果不需要字符串,请忽略它,如果您想知道原因,请查找“字符串编码的恐怖”。基本上,您应该不断问自己:当我收到一个字符串时,我知道其中包含什么吗?它包含比整数更多的状态,或者实际上,枚举和状态增加是不好的
Maarten Bodewes

Answers:


30

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

更新:这是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);
    }
}

2
这是一个很好的答案,它确实摆脱了static类初始化程序。有谁知道它相对于MapJava而言是否具有其他优势(仅针对特定于字段的初始化器摆脱静态初始化器是不足以让我在类路径中包括一个库,即使该库是Guava)。
Maarten Bodewes,

在我看来,这是一个骇人听闻的解决方案,增加了对外部库的不必要依赖。可以在这里找到一个更好,更优雅的解决方案stackoverflow.com/questions/28762438/how-to-reverse-enum
波西米亚风

2
当然,与流你不需要番石榴:LOOKUP = stream(values()).collect(toMap(MyEnum::getStatus, x -> x));
基因(Gene)

20

尽管开销较大,但静态映射还是不错的,因为它可以通过 code。实现的查找时间随枚举中元素的数量线性增加。对于小的枚举,这根本不会起到很大的作用。

两种实现方式(以及一般而言Java枚举)的一个问题是,a确实具有一个隐藏的额外价值Statusnull。根据业务逻辑的规则Exception,当查找“失败”时,返回实际的枚举值或抛出,可能是有意义的。


2
@Matt,我相信这两种方式都是固定时间查找,因为Map中的项目数量恒定。
jjnguy 2011年

3
@jjnguy:它O(n)是枚举的大小。这全都是学问,因为枚举不太可能很大。
马特·鲍尔,

6
@jinguy,它们都可能是“常量”时间操作,但是每个操作中的常量是不同的。一个是在哈希表中查找值的时间,另一个是循环遍历变量数组(但在运行时为常数)所需的时间。如果您在此枚举中具有一百万个值(不实际,仅是一个示例),那么您将更喜欢map-lookup选项。
马特b

5
@jjnguy:声明算法O(n)不是暗示n可以在运行时更改。如果这在不可变(因此固定于运行时)列表上执行了类似的查找,该怎么办?那绝对是一种O(n)算法,n列表的大小在哪里。
马特·鲍尔,

11
@jjnguy看看rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation O(1)是“无论何时都将在同一时间(或空间)执行的算法输入数据集的大小。” 而O(N)是“一种性能会线性增长且与输入数据集大小成正比的算法”,因此在这种情况下,数据集的大小不会因运行而改变(为什么我认为您认为它是“恒定的”),算法的性能仍然取决于输入数据集的大小(在这种情况下,枚举中的条目数)
digitaljoel 2011年

7

这是一个可能更快的替代方法:

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

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


9
并非完全相同,切换版本可以使用查找表直接跳至正确的代码,而无需进行一系列测试:artima.com/underthehood/flowP.html
Dan Berindei 2011年

12
@jjnguy:否,编译器可以优化此开关以使用二进制搜索或查找表(取决于数字)。而且您无需先创建和填充values()数组(仅此一个就可以创建此变量O(n))。当然,现在该方法更长,因此加载时间更长。
圣保罗Ebermann

1
@Alison:case WAITING.code是个好主意,但我担心这不是编译时常量。
圣保罗Ebermann

1
@jinguy如果他们不想使用表查找,我认为他们不会费心创建tableswitch指令。不知道lookupswitch指令是否有任何优化。
Dan Berindei 2011年

15
但是,就干净代码而言,这绝对是不可接受的解决方案。使用此解决方案,您可以在两个不同的地方将id映射到枚举值,因此当映射不同时可能会出现错误!
Zordid

6

显然,地图将提供恒定的时间查找,而循环则不会。在一个没有值的典型枚举中,遍历查找没有问题。


对于少数几个枚举,我认为这并不重要。
无尽的

3

这是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");

    }
}

1
您能否解释一下您的代码,而不只是提供代码转储?它看起来像是一个不错的实现(实际上,我自己刚刚编写了一个类似的解决方案),但是如果不列出其属性,人们将不得不根据代码对其进行评估,而且这种情况不太可能发生。
Maarten Bodewes,

0

在Java 8中,我只需将以下工厂方法添加到您的枚举中,然后跳过查找映射。

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

关于此的想法:只是一个for循环在幕后。不是超级可读。编写一个可重用的函数来执行此操作并使它取值(值,Status :: getCode)(如Maps.uniqueIndex一样)可能会很有趣
Barett

1
这比上面提到的任何其他答案都要慢。1.您每次都从创建一个新数组values()(这本身非常慢,因此,如果做很多,您可以通过一次缓存该数组并重新使用它来大大提高速度),2.您正在使用流(在性能/基准测试中,其测试通常比简单的for循环要慢得多)。
Shadow Man

0
@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);
    }
}

-2

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

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


3
作为枚举常量的数量和不断的,一切都O(1):-)
保罗Ebermann

12
不,线性查找在线性时间O(n)而不是O(1)中运行HashMap。另一方面,n是4 ...
汤姆·霍顿-大头钉

6
@jjnguy:事实是,常量数量的增加是查询运行时间的线性增加,从而使查询为O(N)。常量的数量在运行时不会改变是无关紧要的。
ColinD 2011年

4
@jjnguy:该评论毫无意义。N是项目数。期。
user207421'3

2
您应该真正纠正答案。获得数组中的第n个元素是O(n)。
user1803551
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.