8

The old way, if we wanted to switch on some complicated bitmask, we could easily do it like this (a random example from the top of my head just to demonstrate the issue):

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

Nowadays, since we use enums and EnumSets, I'd sometimes like to do a similar thing:

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

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

which doesn't work, because we can only switch on an int, enum or String value. At this point, I realized it can't be done, even though that enum values are basically just hidden integers. But I like to dig around and the feature looks very useful, so:

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

Still no luck. Knowing the trick for switch on Strings and that EnumSets are actually 64-bit fields (or arrays of them), I would also try:

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

thinking that when the Human hashCode() would be properly implemented to give consistent results, it could work. Nope:

java.lang.Error: Unresolved compilation problem: case expressions must be constant expressions


Now, I wonder why there's no possibility to do this. I always thought of enums and EnumSets in Java like a proper replacement for those old-school bitfields, but here it seems that the new ways can't handle more complicated cases.

The right solution kind of sucks compared to any of the switch possibilities:

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

In particular, since the introduction of switch on Strings, I believe there are at least two possible implementations of switch on EnumSets:

  1. In the case (Human.MAN | Human.DEAD) expressions, simple use a compile-time type check and ordinal() instead of the enums themselves.
  2. Using the same trick as for Strings.
    • At compile time, compute the hashCode() of the name of the enum values (and possibly something additional - the number of values in enum, the ordinal() etc. - everything is static and constant from the compile time on). Yes, this would mean to change the hashCode() either of the EnumSet class or the Enum class.
    • use instead of the enums themselves

Now, is there any serious obstacle I didn't take into count (I can come up with a few, all can be easily overcame) that would render this impossible to implement easily? Or am I right that this would indeed be possible, but not desirable enough for Oracle to implement it, because it is not used so often?


Also, let me state that this is a purely academic question possibly without a good answer (don't know, I wouldn't ask otherwise). I might make it community wiki if it proves to be unanswerable. However, I couldn't find an answer (or even anyone discussing it) anywhere, so here it goes.

Petr Janeček
  • 37,768
  • 12
  • 121
  • 145
  • have you tried writing a class similar to RegularEnumSet in the JDK? unfortunately RegularEnumSet itself is package private, but it stores its value internally as a long. you could write a version that stores the value as an int (limited to enums with <=32 possible values and then you might be able to switch on it – radai Dec 28 '12 at 18:18
  • @radai Not without a change to the compiler, I think. Yet, I'm putting this on my todo to try it sometimes, just for the practise. – Petr Janeček Dec 28 '12 at 18:34
  • yeah, at the very least it wont be clean. your new class (SmallEnumSet?) would need to be in the java.util package. but exposing the internal int storage might allow you to switch on it. – radai Dec 28 '12 at 18:35

3 Answers3

6

In Java & Object Oriented world you would have class with setters and getters on an Object and you would use those

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

Note: switch is not a good idea because it only takes exact matches. e.g. case MAN | DEAD: will not match MAN | HUNGRY | DEAD unless you only want to match those who were not hungry before they died. ;)


I will see your "absolutely sufficient" benchmark and raise you another flawed benchmark which "shows" it takes a fraction of a clock cycle (in cause you are wondering, that is hard to believe)

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

prints

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

Thats 0.1 clock cycles on my machine, before it is optimised away completely.

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • Yes, this is absolutely true and in my real life use-case, where I had to rewrite a lot of pre-1.5 code with bitfields, I used it. However, it turned out to be slow as hell compared to the old `int` based solution for some cases. Luckily for me, there were even worse performace bugs in the old version which I fixed / replaced with fast code, so in the end, it was all good. But still - I could imagine using the switch on enum way. Thanks, anyway! – Petr Janeček Dec 28 '12 at 18:22
  • 1
    @Slanec "However, it turned out to be slow as hell compared to the old int based solution for some cases." You're doing it wrong then. It should not be close to noticeably slower. – NominSim Dec 28 '12 at 18:24
  • 1
    @Slanec I have no doubt this is faster than using an EnumSet with switch. If you find a significant difference between this and using a mask, you are doing something wrong. – Peter Lawrey Dec 28 '12 at 18:29
  • @NominSim I see your point and have to agree. But still, creating _a lot_ of Objects will be always noticeably slower than setting an `int` array. Anyway - the speed did show up as absolutely sufficient. – Petr Janeček Dec 28 '12 at 18:29
  • 1
    @Slanec if it was absolutely sufficient, then your micro-benchmark was fatally flawed. I can write you a flawed benchmark which claims to run the code above in under a clock cycle, and you won't get faster than that. ;) – Peter Lawrey Dec 28 '12 at 18:30
  • @PeterLawrey Ohh, I think I misunderstood your answer. Do you suggest the `isDead()` method to be just a shortcut for `return set.contains(Human.DEAD)`? If yes, then aha :). If no, then a POJO Human with many fields was a bad way to go in the time being :). – Petr Janeček Dec 28 '12 at 18:38
  • @PeterLawrey What about one that runs in negative clock cycles? My benchmark finished running tomorrows test two days ago :) – NominSim Dec 28 '12 at 18:39
  • @Slanec while it might be a bad way to go your code right now, using plain setters/getters is a) pretty standard b) as fast as anything you can come up with. "Performance reasons" so often used as an excuse for weird and wonderful coding when it isn't needed. Using an EnumSet has some overhead of its own which makes it slower, but used int he right place can be the best option. – Peter Lawrey Dec 28 '12 at 18:45
  • 1
    @NominSim Running on Windows XP you can get negative times because the OS doesn't correct for the difference in the timer on difference cores. This means the difference between two nanoTime calls can be negative. I have seen it go backwards up to 4 ms. – Peter Lawrey Dec 28 '12 at 18:48
  • Okay, let's cut it short. I agree that this method should be encouraged and I used it in the real assignment. Creation of an Object is slower than setting an int. And yes, it could have been written so that performance didn't matter. Back to the original academic question, though. Would there be a serious obstacle when implementing the imaginary switch on EnumSets? – Petr Janeček Dec 28 '12 at 19:05
5

How about using Set methods of EnumSet.

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

Acutally EnumSet's implementation of Set interface methods is very efficient and underneath is the bitfield comparison that you are looking for.

Alexander Pogrebnyak
  • 44,836
  • 10
  • 105
  • 121
  • This is, actually, a good trade-off that meets halfway on the real options and my wish. If I'll ever need to compare a lot of complicated enums without a chance to refactor, I'll probably go this way. – Petr Janeček Dec 28 '12 at 18:33
-5

Do the following (based on your example):

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

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

If you want EnumSet's then you can't use switch and should refactor it to if

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

latter variant will do bitwise comparison internally.

Dims
  • 47,675
  • 117
  • 331
  • 600