Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Switch on EnumSet

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.

like image 830
Petr Janeček Avatar asked Dec 28 '12 18:12

Petr Janeček


2 Answers

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.

like image 167
Peter Lawrey Avatar answered Nov 07 '22 08:11

Peter Lawrey


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.

like image 33
Alexander Pogrebnyak Avatar answered Nov 07 '22 09:11

Alexander Pogrebnyak