0

I have a long list of enumerations that are being referenced and selected via if-statements in my code, however, I want to use switch statements as I have a huge number of such statements in multiple places. This is a small subset of the enumerations that I have:

public enum MYENUM {
    FOO1("hello"),
    FOO2("goodbye"),
 
    private final String toString;

    MYENUM(String toString) {
        this.toString = toString;
    }

    @Override
    public String toString() {
        return this.toString;
    }
}

Previously I had:

String toCheck = "hello;"

if (toCheck.equals(MYENUM.FOO1.toString())) {
    //Do stuff
}

if (toCheck.equals(MYENUM.FOO2.toString())) {
    //Do stuff
}

But now I want:

String toCheck = "hello;"
switch(toCheck) {
    case MYENUM.FOO1.toString():
        //Do stuff
        break;

    case MYENUM.FOO2.toString():
        //Do stuff
        break;

}

Needless to say, this does not work, I can't even enter the toString() function of the Enum in the case, 'toString' has private access in 'MYENUM'. How can I modify my switch statement so that I can properly switch on the Strings?

J_code
  • 356
  • 1
  • 5
  • 17
  • Instead of a _switch_, try using a loop on _MYENUM.values_. Check each _toString_ from there. Use a _boolean_ outside of the loop, to indicate a match. – Reilas Aug 05 '23 at 21:03
  • Did you check [How to get an enum value from a string value in Java](https://stackoverflow.com/q/604424)? – Arvind Kumar Avinash Aug 06 '23 at 13:08

4 Answers4

3

You can't. The strings in a switch block have to be constants, and while MYENUM.FOO1.toString() feels constant, the JVM has no idea that it is (after all, your toString() method could roll some dice, ping a web server, who knows what it'll do). Hence, the JVM will not allow it and nothing you're going to do can make it work.

Instead, make a map that maps these string values to the right enum. This also gives you the performance benefit you might be looking for (O[1] lookup instead of O[n] lookup). Not that this will matter unless you have hundreds, maybe even thousands of enums. The enum itself can do this:

public enum MYENUM {
    FOO1("hello"),
    FOO2("goodbye"),
 
    private final String toString;

    MYENUM(String toString) {
        this.toString = toString;
    }

    @Override
    public String toString() {
        return this.toString;
    }

    private static final Map<String, MYENUM> stringToValue;
    static {
      var m = new HashMap<String, MYENUM>();
      for (MYENUM e : values()) m.put(e.toString(), e);
      stringToValue = Collections.unmodifiableMap(m);
    }

    public static MYENUM of(String v) {
      return stringToValue.get(v);
    }
}

Now replace your entire switch block with simply MYENUM.of(toCheck).

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • I like your use of a static initializer; I didn't know it could be used with enums. I'll have to change how I create similar enums (as shown in my answer). – Paul Aug 05 '23 at 21:23
  • What performance gains would using a static initializer in this context have, other than being thread safe I presume? – J_code Aug 06 '23 at 06:04
  • Vs. looping every time? The looping, obviously. Everytime a lookup ("Go from this string thing back to the enum") is done, it's an O(1) hashmap lookup. – rzwitserloot Aug 06 '23 at 11:30
0

In short, switch on an enum, not a String.

Here's how I construct and use enums like yours (i.e. using an alternate value):

public enum MyEnum {
    FOO1("hello"),
    FOO2("goodbye"),

    private static final Map<String, MyEnum> lookupMap = new TreeMap<>();
 
    private final String displayStr;

    MyEnum(String displayStr) {
        this.displayStr= displayStr;
    }

    public static MyEnum parse(String s)
    {
      if (s == null) throw new NullPointerException("Cannot parse null String");

      if (lookupMap.isEmpty())
      {
        for (MyEnum myEnum: values())
        {
           lookupMap.put(myEnum.name(), myEnum);
           lookupMap.put(myEnum.displayStr, myEnum);
        }
      }

      MyEnum result = lookupMap.get(s);
      if (result == null) throw new IllegalArgumentException("No MyEnum corresponds to " + s);
      return result;
    }

    @Override
    public String toString() {
        return displayStr;
    }
}

Your switch statement then becomes:

MyEnum toCheck = MyEnum.parse("hello");
if (toCheck == null)
{
  // In this block you must either return, throw an exception,
  // or set toCheck to a non-null default value.
}

switch(toCheck) {
    case MYENUM.FOO1:
        //Do stuff
        break;

    case MYENUM.FOO2:
        //Do stuff
        break;

// etc.
}
Paul
  • 19,704
  • 14
  • 78
  • 96
  • 1
    Note that your map setup has the benefit of not spending the cycles and RAM to build that map until you need it, but, [A] it's an enum so by definition the size of that map is limited; class files have limits, you can't have an enum with a million entries, and [B] this isn't thread safe at all. If 2 threads call `parse`, the JMM more or less states that your app is broken and probably in ways that are hard or even impossible to test (e.g. 'always works on my PC, but fails on the server' - that kind of untestable). – rzwitserloot Aug 05 '23 at 21:47
  • The odds that you run into that problem are quite small, but _if you do_, I guarantee you you'll be spending 20 person __HOURS__ finding it and fixing it. The impact of a bug is the odds that it will occur multiplied by the damage it'll do if it occurs, and in this case, tiny number times gargantuan number says: Still significant. I strongly suggest you do not use this style and just init immediately. That or add the appropriate safeguards. An easy way is with an inner private class that does this (as classes aren't initialized unless used). – rzwitserloot Aug 05 '23 at 21:49
  • 1
    @rzwitserloot Thank you for your comprehensive comments. I'm aware of the problems with my implementation and should have pointed them out in my answer...now I don't have to thanks to your comments. :) In our application there is no danger of multiple threads simultaneously calling `parse` the first time but it is dodgy code (especially as an answer) so we are going to convert them to use static initializers. As I mentioned in a comment to your answer I wasn't aware static initializers could be used with enums. Thanks again! – Paul Aug 09 '23 at 14:49
0

Unfortunately, you cannot directly use a switch statement with method calls in the case labels like MYENUM.FOO1.toString(). The case expressions in a switch statement must be compile-time constant expressions. The result of a method call like toString() is not known until runtime so it's not a constant expression. This is why you cannot directly switch on the result of toString().

However, you can create a static method that performs the logic of the switch statement using a Map.

import java.util.*;
import java.util.stream.*;

public enum MYENUM {
    FOO1("hello"),
    FOO2("goodbye");

    private final String toString;

    MYENUM(String toString) {
        this.toString = toString;
    }

    @Override
    public String toString() {
        return this.toString;
    }

    private static final Map<String, MYENUM> ENUM_MAP = Arrays.stream(MYENUM.values())
            .collect(Collectors.toUnmodifiableMap(MYENUM::toString, e -> e));

    public static MYENUM get(String name) {
        return ENUM_MAP.get(name);
    }
}

You would use the get() method in your switch statement like this:

String toCheck = "hello";

switch (MYENUM.get(toCheck)) {
    case FOO1 -> {
        // Do stuff
    }
    case FOO2 -> {
        // Do stuff
    }
    default -> {
        // Handle case where no match is found
    }
}
Akhilesh Pandey
  • 855
  • 5
  • 10
0

Instead of using the switch, you can utilize the Enum#values method and loop on each entry.

Enum#valueOf (Java SE 20 & JDK 20),

"... All the constants of an enum class can be obtained by calling the implicit public static T[] values() method of that class ..."

So, here is an example enum.

enum Value {
    A("ABC"), B("DEF"), C("GHI");

    public String string;

    Value(String string) {
        this.string = string;
    }
}

And, here is an example usage.

String string = "GHI";
Value value = null;
for (Value valueB : Value.values()) {
    if (valueB.string.equals(string)) {
        value = valueB;
        break;
    }
}
System.out.println("value = " + value);

Output

value = C

Additionally, you could just add this as a static method within the enum.

enum Value {
    A("ABC"), B("DEF"), C("GHI");

    public String string;

    Value(String string) {
        this.string = string;
    }

    /** @return null if not found */
    static Value parse(String string) {
        for (Value value : Value.values()) {
            if (value.string.equals(string)) return value;
        }
        return null;
    }
}

If you did want to utilize the switch structure, here is an example, where string is the toCheck value.

Value value = Value.parse(string);
if (value != null) {
    switch (value) {
        case A -> {
            //Do stuff
        }
        case B -> {
            //Do stuff
        }
        case C -> {

        }
    }
}
Reilas
  • 3,297
  • 2
  • 4
  • 17
  • For your example, why do you have ` case A -> ` instead of `case A: `? – J_code Aug 05 '23 at 22:04
  • @J_code, I'm using the newer _switch_ syntax. If I used the older switch _syntax_ I would need to add a _break_ keyword within each branch. _Java_ refers to these as _[switch expressions](https://docs.oracle.com/en/java/javase/20/language/switch-expressions.html)_. – Reilas Aug 05 '23 at 22:17
  • @J_code, also, I simplified the code for the _parse_ method; I removed the need for the local _value_ variable. – Reilas Aug 05 '23 at 22:26
  • Ok, I see. Your solution is promising and in my case, the null check would not be needed because it would be done at the point of input so at this level, it would be guaranteed to not be null. – J_code Aug 05 '23 at 22:54
  • @J_code, if the _null_ is a concern, then you'd just have to choose where to handle it. I find it easier to remove that component from the class, in terms of delegation; a lot of _Java_ classes follow this same design principle. – Reilas Aug 05 '23 at 22:59