51

I have a problem in Java using Enums. I have read the documentation about assigning value parameters to Enums. But, my question is what about multiple values, is it possible?

This what I would like to achieve: I have an Enum for languages. Each language is represented by its name and some shorter aliases (not always, and not always the same number of aliases)

Here is an example:

public enum Language{
English("english", "eng", "en", "en_GB", "en_US"),
German("german", "de", "ge"),
Croatian("croatian", "hr", "cro"),
Russian("russian")
}

Can I just define an Enum like this and get the right enum values by calling Language.valueOf() ???

zolakt
  • 521
  • 1
  • 4
  • 4
  • I'd recommend you to see my articles: http://java.dzone.com/articles/enum-tricks-customized-valueof and http://java.dzone.com/articles/enum-tricks-hierarchical-data Although I am not sure I understand exactly what do you really want I believe that at least one of these articles contain answer to your question. – AlexR Nov 16 '10 at 19:36
  • @AlexR: thanks! A decade has passed and such problems still exist :) I suppose hierarchical data per your article is a suitable answer here, as long as the code which cares about the values would be content with custom `is()` lookups (e.g. `if (lang.is("English"))`) with all aliases being "sub-items" in hierarchy. Might even report the display-name (override `toString()`) by looking up until grandparent class is `null`. I think some benefits of native `enum` are lost however, e.g. `switch/case` support becomes messy. Feels unfortunate that they did not make an "enum with aliases" part of Java. – Jim Klimov Feb 10 '23 at 14:36

6 Answers6

72

This is probably similar to what you're trying to achieve.

public enum Language{
    English("english", "eng", "en", "en_GB", "en_US"),
    German("german", "de", "ge"),
    Croatian("croatian", "hr", "cro"),
    Russian("russian");

    private final List<String> values;

    Language(String ...values) {
        this.values = Arrays.asList(values);
    }

    public List<String> getValues() {
        return values;
    }
}

Remember enums are a class like the others; English("english", "eng", "en", "en_GB", "en_US") is calling the enum constructor.

You could then retrieve the enum value corresponding to a string through a search method (you can put it as a static method in the enum again).

public static Language find(String name) {
    for (Language lang : Language.values()) {
        if (lang.getValues().contains(name)) {
            return lang;
        }
    }
    return null;
}
Jonik
  • 80,077
  • 70
  • 264
  • 372
Flavio
  • 11,925
  • 3
  • 32
  • 36
  • 6
    I could be misreading this, but I think he wants to be able to do Language.valueOf("eng") or Language.valueOf("english"), and get the same enum instance back. – Andy Nov 16 '10 at 19:22
  • 1
    Ok that is not possible with the defaul valueOf method; I edited the answer to include a method to obtain that effect. – Flavio Nov 16 '10 at 19:25
  • Yeah, I was sorta confused too, really it's a clever idea I think, but not possible. – Andy Nov 16 '10 at 19:28
  • +1. Couple of things you may consider are changing your example so that it first tries to delegate to `valueOf` (so the typical case "English" is covered), and maybe using a `static Map` instead of an instance member `List` to make lookups easier. – Mark Peters Nov 16 '10 at 19:34
  • @Mark - `valueOf()` attempts to match the actual constant name (in this case, `ENGLISH`) rather than any value associated with the constant ("English"). Plus, it throws an exception if it doesn't match. – Anon Nov 16 '10 at 19:40
  • @Anon: In this case, the constant name is actually `English`, not `ENGLISH` (don't forget to actually look at the code and/or question before refuting someone). The `find(String)` method doesn't check for "English" since it's not included in the constructor parameters. Hence my comment and suggestion. The point about the exception is a good one, but not a deal breaker since exceptions can be caught if need be. An alternative would be to explicitly add the enum to the list (`values.add(getName())`). – Mark Peters Nov 16 '10 at 19:46
  • The other alternative is to move the `valueOf()` call delegation to the end instead of the beginning. I think my original point is irrelevant anyway though, since it looks like the OP is fine with only returning a result for the strings listed in the arguments of the constructor. After that it's just a matter of case sensitivity. – Mark Peters Nov 16 '10 at 19:54
  • @Flavio, if the enum values are associated with only a single value, like `ENGLISH("Eng")`, will it be possible to get `Languages.ENGLISH` through `Language.valueOf("Eng")`? – SexyBeast Feb 17 '13 at 16:58
  • 1
    No, the `valueOf` method only works with the Java name of the enum (i.e. `ENGLISH` in your example). You must use the `find` method also for those cases. Unless *all* have a single value, but then this is the wrong post :) – Flavio Feb 17 '13 at 22:58
18

Mapping a string to a enum value, is typically what the valueOf static method is doing for you. So if you want to accomplish this with the use of synonyms, you will have to develop something similar. Because we cannot override a static method, and I do not think we want to in this case, we will name it differently: fromString should be appropriate.

public enum Language { 
  ENGLISH("eng", "en", "en_GB", "en_US"),   
  GERMAN("de", "ge"),   
  CROATIAN("hr", "cro"),   
  RUSSIAN("ru"),
  BELGIAN("be",";-)");

  static final private Map<String,Language> ALIAS_MAP = new HashMap<String,Language>(); 
  static { 
    for (Language language:Language.values()) { 
      // ignoring the case by normalizing to uppercase
      ALIAS_MAP.put(language.name().toUpperCase(),language); 
      for (String alias:language.aliases)
        ALIAS_MAP.put(alias.toUpperCase(),language);
    } 
  } 

  static public boolean has(String value) { 
    // ignoring the case by normalizing to uppercase
    return ALIAS_MAP.containsKey(value.toUpperCase());
  } 

  static public Language fromString(String value) { 
    if (value == null) throw new NullPointerException("alias null"); 
    Language language = ALIAS_MAP.get(value.toUpperCase()); 
    if (language == null)
      throw new IllegalArgumentException("Not an alias: "+value); 
    return language; 
  } 

  private List<String> aliases; 
  private Language(String... aliases) { 
    this.aliases = Arrays.asList(aliases); 
  } 
} 

As a benefit of this type of implementation we can, as demonstrated, also easily implement the has static method to test if a given alias is part of the enum value set. At the same time, we applied some good naming conventions:

  • the enum values go in uppercase, to indicate that they are in actuality static finals (singleton instances).
  • at the same time, we also put all the other static finals all caps.

Note that we do not have to repeat the name of the enum value itself: we always consider it's own name automatically (gets added to the ALIAS_MAP), and on top we normalize everything to uppercase to make it case insensitive.

Seems big, but while using the enum, it looks pretty:

public void main() {
  Language myLanguage = Language.fromString("en_GB");
  if (myLanguage == Language.ENGLISH) {
    System.out.println("Yes, I know, you understand English!");
  }
} 

The backing container for the aliases is a Map, a HashMap to be more specific. The HashMap provides a fast access path to the aliases, and is also easy to grow. Whenever we think about 'indexing' something, likely a HashMap should be our first choice.

Note that for transparent use, we store both the name of the enum constant itself (retrieved through the name() method), and all the aliases. We could have implemented this differently by first attempting to do the lookup using the built-in valueOf static method. It is a design choice, but we would potentially have to deal with additional exceptions etc.

YoYo
  • 9,157
  • 8
  • 57
  • 74
3

In short, no.

The parameter for the valueOf() method must be only the String of the enum constant type. So it cannot vary, or lookup possible values. See the JavaDoc.

You need to write your own utility method to return the proper enum type for the given values.

Andy
  • 8,841
  • 8
  • 45
  • 68
2

Normalize the data:

public enum LanguageCode
{
  ENGLISH,
  GERMAN,
  CROATIAN,
  RUSSIAN,
  // ...
}
// (add whatever initialization you want to that

Then

public enum Language{
  english(ENGLISH),
  eng(ENGLISH),
  en(ENGLISH),
  en_GB(ENGLISH),
  // ...
}

etc.

user207421
  • 305,947
  • 44
  • 307
  • 483
  • 1
    Hmmm...I don't think that will work. LanguageEnum members would then be english, eng, en, en_GB... and not ENGLISH,GERMAN... – zolakt Nov 17 '10 at 11:26
1

Wow, really quick reply :) Thanks guys.

Andy is right. I want to call Language.valueOf("eng") or Language.valueOf("english") and get Language.English as return.

I already have a utility function that does this, but it's not very nice. Switch function that checks string values and return appropriate enum instance.

But there is a lot of code rewriting (I have cca 30-40 languages). And if I want to add a language, I have to add it to enum, and implement a new check in the utility class.

I'll try Flavios approach. Just one question. Your constructor, shouldn't it be?

Language(List<String> values) {
    this.values = Arrays.asList(values);
}
zolakt
  • 521
  • 1
  • 4
  • 4
  • If you use your kind of constructor (you must remove the `Arrays.asList` call to make it compile), the instances would have to be created with `English(Arrays.asList("english", "eng", "en", "en_GB", "en_US"))`. You can also store the values as a simple array instead of a List... the search would be slightly more awkward though. – Flavio Nov 17 '10 at 08:14
0

Alternatively add a method, so your enums and constructor can be more clean if the structure is more complicated:

public Language getLanguage(String code){
  switch(code){
    case "en":
    case "en_GB":
    ...
    case "eng":
      return ENGLISH;
    case "rus":
    case "russian":
      return RUSSIAN;
  }
}
CsBalazsHungary
  • 803
  • 14
  • 30