【问题标题】:Java enum reverse look-up best practiceJava枚举反向查找最佳实践
【发布时间】:2011-07-16 00:53:14
【问题描述】:

我看到suggested on a blog 认为以下是在 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;
    }
}

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

【问题讨论】:

  • 顺便说一句,对你来说地图构建循环你可以完成for(Status s : values()) lookup.put(s.code, s);
  • 使用Enum.valueOf()有什么问题吗?您无法存储字符串吗?
  • @Jonathan 您经常需要从二进制或数字输入中生成枚举。所以我想Enum.valueOf() 没有什么(尽管要注意大写),但通常你只是从一个字节或一个数字开始。并且请:如果不需要字符串,请忽略它,如果您想知道原因,请查找“字符串类型的编码恐怖”。基本上你应该不断地问自己:当我收到一个字符串时,我知道里面是什么吗?它包含比整数更多的状态,或者实际上是一个枚举,并且状态增加是不好的

标签: java enums static-initializer


【解决方案1】:

来自 Google 的GuavaMaps.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);
    }
}

【讨论】:

  • 这是一个很好的答案,它确实摆脱了 static 类初始化程序。有谁知道它是否比来自 Java 的特定 Map 有任何其他优势(仅仅摆脱针对特定字段初始化程序的静态初始化程序不足以让我将一个库包含到我的类路径中,即使该库是 Guava )。
  • 在我看来,这是一个 hacky 解决方案,它增加了对外部库的不必要依赖。可以在这里找到更好、更优雅的解决方案stackoverflow.com/questions/28762438/how-to-reverse-enum
  • 当然你不需要 Guava 的流:LOOKUP = stream(values()).collect(toMap(MyEnum::getStatus, x -&gt; x));.
  • 这对我有用:LOOKUP = Arrays.stream(values()) .collect(Collectors.toMap(MyEnum::getStatus, Function.identity()));
【解决方案2】:

虽然开销较高,但静态映射还是不错的,因为它提供了code 的恒定时间查找。您的实现的查找时间随着枚举中元素的数量线性增加。对于小的枚举,这根本不会有很大的贡献。

这两种实现的一个问题(可以说是一般的 Java 枚举)是Status 可以承担的真正隐藏的额外价值:null。根据业务逻辑的规则,当查找“失败”时,返回一个实际的枚举值或抛出一个Exception 可能是有意义的。

【讨论】:

  • @Matt,我相信这两种方式都是恒定时间查找,因为地图中有恒定数量的项目。
  • @jjnguy:枚举大小为O(n)。这都是迂腐的,因为枚举不太可能很大。
  • @jinguy,它们可能都是“常数”时间操作,但每个操作中的常数是不同的。一个是在哈希表中找到一个值的时间,另一个是循环遍历一个变量(但在运行时是常数)数组值所需的时间。如果你在这个枚举中有一百万个值(不实用,只是一个例子),那么你会更喜欢 map-lookup 选项。
  • @jjnguy:声明一个算法是O(n) 并不意味着n 可以在运行时改变。如果这对不可变(因此在运行时固定)列表执行类似的查找怎么办?那绝对是O(n) 算法,其中n 是列表的大小。
  • @jjnguy 看看rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation O(1) 是“无论输入数据集大小如何,始终在同一时间(或空间)执行的算法。”而 O(N) 是“一种算法,其性能将线性增长并与输入数据集的大小成正比”,所以在这种情况下,数据集的大小不会随着运行而改变(为什么我认为你认为它'常数'),算法的性能仍然基于输入数据集的大小(在这种情况下,枚举中的条目数)
【解决方案3】:

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

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

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

【讨论】:

  • 完全相同,switch 版本可以使用查找表直接跳转到正确的代码,而不是进行一系列测试:artima.com/underthehood/flowP.html
  • @jjnguy:不,编译器可以优化此开关以使用二进制搜索或查找表(取决于数字)。而且您不需要在之前创建和填充values() 数组(仅此一项就可以使这个变体O(n))。当然,现在方法更长了,所以加载时间也更长。
  • @Alison: case WAITING.code 是个好主意,但我担心这不是编译时常量。
  • @jinguy 如果他们不想使用表查找,我认为他们不会费心创建 tableswitch 指令。不知道lookupswitch指令有没有优化。
  • 但就干净的代码而言,这是一个绝对不能接受的解决方案。使用此解决方案,您可以在两个不同的位置将 id 映射到枚举值,因此当映射不同时可能会出现错误!
【解决方案4】:

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

【讨论】:

  • 对于少数枚举,在我看来这并不重要。
【解决方案5】:

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

    }
}

【讨论】:

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

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

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

【讨论】:

  • 对此的想法:只是一个 for 循环。不是超级可读。编写一个可重用的函数来执行此操作并使用 (value, Status::getCode) 可能会很有趣(就像 Maps.uniqueIndex 一样)
  • 这比上面提到的任何其他答案都慢。 1. 你每次都从values() 创建一个新数组(这本身很慢,所以如果做了很多,你可以通过缓存数组一次并重用它来加快速度),2. 你'正在使用流(在性能/基准测试中,通常测试比简单的 for 循环慢得多)。
【解决方案7】:
@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);
    }
}

【讨论】:

    【解决方案8】:

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

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

    【讨论】:

    • 由于枚举常量的数量是恒定的,所以一切都是O(1) :-)
    • 不,对于HashMap,线性查找在线性时间 O(n) 而不是 O(1) 中运行。另一方面,n 是 4...
    • @jjnguy:问题是,常量数量的增加是查找运行时间的线性增加,使得查找 O(N)。常量的数量在运行时不变是无关紧要的。
    • @jjnguy:那条评论毫无意义。 N 是项目的数量。期间。
    • 你真的应该更正答案。获取数组中的第 n 个元素是 O(n)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-11-09
    • 2012-11-01
    • 2014-07-17
    • 1970-01-01
    • 1970-01-01
    • 2020-05-09
    • 2010-10-19
    相关资源
    最近更新 更多