【问题标题】:Switch on EnumSet打开枚举集
【发布时间】:2012-12-13 23:22:51
【问题描述】:

老方法,如果我们想在一些复杂的位掩码上switch,我们可以很容易地这样做(我头脑中的一个随机示例只是为了演示这个问题):

private static final int   MAN = 0x00000001;
private static final int WOMAN = 0x00000002;
// ...alive, hungry, blind, etc.
private static final int  DEAD = 0xFF000000;

public void doStuff(int human) {
    switch (human) {
    case MAN | DEAD:
        // do something
        break;
    // more common cases
    }
}

现在,由于我们使用enumsEnumSets,我有时想做类似的事情:

enum Human {
    MAN, WOMAN, DEAD; // etc.
}

public void doStuff(EnumSet human) {
    switch (human) {
    case Human.MAN | Human.DEAD:
        // do something
        break;
    // more common cases
    }
}

这不起作用,因为我们只能在 intenumString 值上使用 switch。在这一点上,我意识到这是做不到的,即使 enum 值基本上只是隐藏的整数。但我喜欢四处挖掘,该功能看起来非常有用,所以:

private static final EnumSet<Human> DEAD_MAN = EnumSet.of(Human.MAN, Human.DEAD);

public void doStuff(EnumSet human) {
    switch (human) {
    case DEAD_MAN:
        // do something
        break;
    // more common cases
    }
}

仍然没有运气。 Knowing the trick for switch on Strings 并且 EnumSets 实际上是 64 位字段(或它们的数组),我也会尝试:

    switch (human.hashCode()) {
    case (Human.MAN.hashCode() | Human.DEAD.hashCode()):
        // do something
        break;
    // more common cases
    }

认为当 Human hashCode() 被正确实施以提供一致的结果时,它可以工作。没有:

java.lang.Error: 未解决的编译问题:case 表达式必须是常量表达式


现在,我想知道为什么不可能这样做。我一直认为 Java 中的 enumsEnumSets 是那些老式位域的合适替代品,但在这里似乎新方法无法处理更复杂的情况。

switch 的任何一种可能性相比,正确的 解决方案有点糟糕:

public void doStuff(EnumSet human) {
    if (human.contains(Human.MAN) && human.contains(Human.DEAD)) {
        // do something
    } else {
        // more common cases
    }
}

特别是自从Strings上引入switch之后,我相信switchEnumSets上至少有两种可能的实现方式:

  1. case (Human.MAN | Human.DEAD) 表达式中,简单地使用编译时类型检查和ordinal() 而不是枚举本身。
  2. 使用the same trick as for Strings
    • 在编译时,计算枚举值的hashCode() name(可能还有一些额外的东西 - 枚举中的值的数量,ordinal() 等 - 一切都是从编译时开始是静态和常量)。是的,这意味着将 hashCode() 更改为 EnumSet 类或 Enum 类。
    • 使用而不是枚举本身

现在,是否有任何我没有考虑到的严重障碍(我可以想出一些,都可以轻松克服),这会导致无法轻松实施?还是说这确实是可能的,但对于 Oracle 来说还不足以实现它,因为它不经常使用,这是对的吗?


另外,让我声明这是一个纯粹的学术问题可能没有一个好的答案(不知道,否则我不会问)。如果它被证明是无法回答的,我可能会将其设为社区 wiki。但是,我在任何地方都找不到答案(甚至任何人都在讨论它),所以就这样吧。

【问题讨论】:

  • 你试过在JDK中写一个类似于RegularEnumSet的类吗?不幸的是,RegularEnumSet 本身是包私有的,但它在内部将其值存储为 long。您可以编写一个将值存储为 int 的版本(仅限于具有
  • @radai 我想,编译器的改变是不会的。然而,我有时会把它放在我的待办事项上尝试一下,只是为了练习。
  • 是的,至少它不会很干净。您的新类(SmallEnumSet?)需要在 java.util 包中。但是暴露内部 int 存储可能会让你打开它。

标签: java enums switch-statement bit-fields enumset


【解决方案1】:

在 Java 和面向对象的世界中,您将在 Object 上拥有带有 setter 和 getter 的类,并且您会使用它们

public void doStuff(Human human) {
    if(human.isDead()) {
       if(human.isMale()) {
           // something
       } else if (human.isFemale()) {
           // something else
       } else {
           // neither
       }
    }
}

注意:switch 不是一个好主意,因为它只需要完全匹配。例如case MAN | DEAD: 不会匹配 MAN | HUNGRY | DEAD 除非你只想匹配那些在死前不饿的人。 ;)


我会看到你的“绝对足够”的基准,并为你提出另一个有缺陷的基准,它“表明”它需要一个时钟周期的一小部分(因为你想知道,这很难相信)

public static void main(String... args) {
    Human human = new Human();
    human.setMale(true);
    human.setDead(true);
    for(int i=0;i<5;i++) {
        long start = System.nanoTime();
        int runs = 100000000;
        for(int j=0;j< runs;j++)
            doStuff(human);
        long time = System.nanoTime() - start;
        System.out.printf("The average time to doStuff was %.3f ns%n", (double) time / runs);
    }
}

public static void doStuff(Human human) {
    if (human.isDead()) {
        if (human.isMale()) {
            // something
        } else if (human.isFemale()) {
            // something else
        } else {
            // neither
        }
    }
}

static class Human {
    private boolean dead;
    private boolean male;
    private boolean female;

    public boolean isDead() {
        return dead;
    }

    public boolean isMale() {
        return male;
    }

    public boolean isFemale() {
        return female;
    }

    public void setDead(boolean dead) {
        this.dead = dead;
    }

    public void setMale(boolean male) {
        this.male = male;
    }

    public void setFemale(boolean female) {
        this.female = female;
    }
}

打印

The average time to doStuff was 0.031 ns
The average time to doStuff was 0.026 ns
The average time to doStuff was 0.000 ns
The average time to doStuff was 0.000 ns
The average time to doStuff was 0.000 ns

在我的机器完全优化之前,这是 0.1 个时钟周期。

【讨论】:

  • 是的,这是绝对正确的,在我的实际用例中,我不得不用位域重写很多 1.5 之前的代码,我使用了它。然而,在某些情况下,与旧的基于int 的解决方案相比,它的速度慢得要命。对我来说幸运的是,旧版本中有更糟糕的性能错误,我用快速代码修复/替换,所以最后一切都很好。但仍然 - 我可以想象使用 enum 方式的开关。无论如何,谢谢!
  • @Slanec “但是,在某些情况下,与旧的基于 int 的解决方案相比,它的速度慢得要命。”那你就错了。它应该不会明显变慢。
  • @Slanec 我毫不怀疑这比使用带开关的 EnumSet 更快。如果您发现这与使用口罩之间存在显着差异,则说明您做错了。
  • @Slanec 如果绝对足够,那么您的微基准测试存在致命缺陷。我可以给你写一个有缺陷的基准,声称可以在一个时钟周期内运行上面的代码,而且你不会比这更快。 ;)
  • @NominSim 在 Windows XP 上运行时,您可能会得到负时间,因为操作系统无法纠正不同内核上计时器的差异。这意味着两次 nanoTime 调用之间的差异可能是负数。我已经看到它倒退了 4 毫秒。
【解决方案2】:

执行以下操作(根据您的示例):

enum Human {
    MAN, WOMAN, DEAD; // etc.
}

public void doStuff(Human human) {
    switch (human) {
        case MAN:
        case DEAD:
            // do something
            break;
        // more common cases
    }
}

如果你想要EnumSet,那么你不能使用switch,应该将它重构为if

public void doStuff(EnumSet<Human> human) {
    if( human.containsAll(EnumSet.<Human>of(Human.MAN, Human.DEAD) {
            // do something
    }
}

后一种变体将在内部进行按位比较。

【讨论】:

  • 在这篇文章中,这是他想要的一厢情愿的行为,而不是合法的 Java
【解决方案3】:

使用EnumSetSet方法怎么样。

private static final EnumSet<Human> DEAD_MAN = 
  EnumSet.of(Human.MAN, Human.DEAD);

public void doStuff(EnumSet human) {
    if ( human.containsAll( DEAD_MAN ) )
    {
            // do something
            break;
    }
    else
    {
        // more common cases
    }
}

实际上,EnumSet 对Set 接口方法的实现非常高效,下面是您正在寻找的位域比较。

【讨论】:

  • 实际上,这是一个很好的权衡,在实物期权和我的愿望上达到了一半。如果我需要比较很多复杂的枚举而没有机会重构,我可能会走这条路。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-10
  • 2015-09-23
  • 1970-01-01
相关资源
最近更新 更多