在使用枚举之前,请检查有效的枚举值

33

我想在一个枚举集合中查找,但通常会出现非匹配的情况导致抛出异常:为了避免异常,我想在执行查找操作之前检查值是否存在。我的枚举看起来像这样:

public enum Fruit {
    APPLE("apple"),
    ORANGE("orange");
    ;
    private final String fruitname;
    Fruit(String fruitname) {
        this.fruitname = fruitname;
    }
    public String fruitname() {return fruitname;}
}

我想在使用相关枚举之前检查,比如说,“banana”是否是我的枚举值之一。我可以遍历允许的值,将字符串与之进行比较。


Fruit.values()[i].fruitname

但我想要能够做类似这样的事情(伪代码):

if (Fruit.values().contains(myStringHere)) {...

这是否可能?我应该完全使用其他东西(数组?地图?)吗?

编辑:最终我采用了NawaMan的建议,但感谢所有人提供的有用输入。

13个回答

33

有一个Apache Commons Lang EnumUtils.isValidEnum()方法。不幸的是,在内部,它使用了try/catch逻辑并返回布尔值,但至少你的代码看起来很干净:

if(EnumUtils.isValidEnum(Fruit.class, fruitname)) { ....

您需要使用最新的commons-lang3库,因为commons-lang 2.x没有此功能。


检查空值也是很好的,谢谢您的评论很有帮助。 - Luis Ramirez-Monterosa
1
我不知道这个函数。如果你有依赖关系,这确实是最清晰和最简单的方法。 - Seega
3
它不检查值。例如,如果有APPLE(“apple”),并且调用是为了apple,则返回false。如果调用是为了APPLE,则返回true。 - Dejell
1
在2020年仍然使用,例如EnumUtils.isValidEnum(Fruit.class,someFood)? someFood:“”; - ASH
是的,像这样的一行代码是一个干净的解决方案:return EnumUtils.isValidEnum(Fruit.class, fruitname) ? Fruit.valueOf(fruitname) : null; 在此检查后,可以在同一行中使用valueOf方法。 - Kerim Oguzcan Yenidunya
参考文献,库为:import org.apache.commons.lang3.EnumUtils; - Shannon

32

我真的不知道有没有内置的解决方案。因此,您可能需要自己编写一个静态方法。

public enum Fruit {
   ...
   static public boolean isMember(String aName) {
       Fruit[] aFruits = Fruit.values();
       for (Fruit aFruit : aFruits)
           if (aFruit.fruitname.equals(aName))
               return true;
       return false;
   }
   ...
}

6
"values()"每次都会创建一个克隆数组,因此最好不要频繁调用它。仅调用一次并缓存结果,或使用"EnumSet.allOf(Fruit.class)"。 - dogbane
1
在JDK 1.7中已经修复了这个问题。在JDK 1.5中有一个注释来解决这个问题。不知道在JDK 1.6中发生了什么。 - alexsmail
1
请注意,这种解决方案对于许多值来说速度较慢。最好像https://dev59.com/tHI_5IYBdhLWcg3wJPdu#2546726那样做些改进。 - Ztyx

8
当我这样做时,我通常会将其嫁接到我的枚举类中。
public enum Fruit {
        APPLE("apple"),
        ORANGE("orange");

    // Order of initialisation might need adjusting, I haven't tested it.
    private static final Map<String, Fruit> lookup = new HashMap<String, Fruit>();
    private final String fruitname;
    Fruit(String fruitname) {
        this.fruitname = fruitname;
        lookup.put(fruitname, Fruit);
    }
    public String fruitname() {return fruitname;}

    public static Fruit fromFruitname(String fruitname) {
        return lookup.get(fruitname);
    }
}

但是:

  • 对于小的枚举,通过遍历列表可能更有效。

顺便说一下:

  • 在这种情况下,我会按照惯例使用name(),因为它与自定义名称相同,除了大小写(很容易修复)。
  • 当你需要查找的内容与name()的值完全不同时,这个解决方案更加有用。

我已经获得了修复示例的许可,将地图设置为静态。 - KLE
初始化的顺序没问题,不用担心。 - KLE
是的,缺少静态关键字是一个打字错误。 - Hakanai
2
我无法编译此示例,因为我收到“在初始化程序中无法引用静态枚举字段<myclass>.Fruit.lookup”的错误。如果我将映射变为非静态,则来自Fruitname的调用会出现“无法对非静态字段lookup进行静态引用”的投诉。我缺少什么? - davek
初始化顺序有误。构建地图需要移动到from*方法中,这似乎很奇怪,因为我的大多数自定义类型安全枚举都是在构造函数中完成的。枚举与自己编写的枚举类之间必须存在某些不同。 - Hakanai
查找映射表应在使用之前进行初始化...无论如何,lookup.put(fruitname, Fruit);是错误的... - Paraneetharan Saravanaperumal

8
这是我的解决方案。我创建了一个集合,这样你就不必指定构造函数。这还有一个额外的好处,被查找的值必须与枚举的大小写匹配。
public enum Fruit{
    Apple, 
    Orange;

    private final static Set<String> values = new HashSet<String>(Fruit.values().length);

    static{
        for(Fruit f: Fruit.values())
            values.add(f.name());
    }

    public static boolean contains( String value ){
        return values.contains(value);
    }

}

7
这是使用EnumSet.allOf来填充map的方法:
public enum Fruit {

    APPLE("apple"), 
    ORANGE("orange");

    private static final Map<String, Fruit> nameToValueMap = new HashMap<String, Fruit>();

    static {
        for (Fruit value : EnumSet.allOf(Fruit.class)) {
            nameToValueMap.put(value.name(), value);
        }
    }

    private final String fruitname;

    Fruit(String fruitname) {
        this.fruitname = fruitname;
    }

    public String fruitname() {
        return fruitname;
    }

    public static Fruit forName(String name) {
        return nameToValueMap.get(name);
    }
}

6
在Java8中,您可以这样做。
 public static boolean isValidFruit(final String fruit) {
    return Arrays.stream(Fruit.values())
        .map(Fruit::name)
        .collect(Collectors.toSet())
        .contains(fruit);
}

1
使用Java 8可以更加简单。在您的枚举中添加一个简单的方法boolean contains(String testedValue),并返回以下内容: Arrays.stream(values()).map(Enum::name).anyMatch(code -> code.equals(testedValue)); - Marcin Szałomski

5

我会持反对意见...我认为你的第一反应(抛出异常)是正确的。

如果你在业务逻辑中进行检查而不是在UI中,那么用户将无法在该级别上获得任何反馈。(如果您未在UI中进行检查,则有其他问题)。因此,处理它的正确方法是抛出异常。

当然,这并不意味着你必须让异常冒泡到UI级别,从而使你的逻辑被短路。我通常将枚举赋值放在自己的小try-catch中,并通过重新分配或任何其他优雅的解决方案来处理异常。

简而言之...你的第一个想法是正确的。跟随这个想法。只需稍微改变异常处理方式即可。


2

提及另一种可能性,让您的调用代码无需担心异常或条件检查,就是始终返回一个水果。例如,如果未找到字符串,则返回Fruit.UNKNOWN。

示例:

public enum Fruit {
   public Fruit getValueOf(String name) {
        for (Fruit fruit : Fruit.values()) {
           if (fruit.fruitname.equals(name))
               return fruit;
           }
        }
        return UNKNOWN;
   }
   ...
}

2

我同意你不希望创建任何异常。这对性能有好处(因为一个异常值得一千个指令,用于构建堆栈跟踪),而且当你说通常情况下没有找到时(因此它不是特殊情况)时,这是合乎逻辑的。


如果只有几个枚举值,我认为你提到的for循环是正确的。它可能是所有方法中性能最好的。但我理解你不想要这个。


你可以构建一个Map来查找你的枚举值,这将避免异常并同时返回适当的枚举值。

更新:Trejkaz已经发布了执行此操作的代码。


还要注意,有时,枚举没有实例匹配时,枚举会返回null作为返回类型,而是有一个专用实例(例如EMPTY或NOT_FOUND)。优点是所有调用代码都不必处理null,并且不会出现NullPointerException的风险。如果需要,可以有一个布尔方法说isFound()(除了该实例外都返回true)。那些确实需要区分这些值和其他值的代码仍然可以这样做,而那些不关心的代码则可以在不知道这种特殊情况的情况下传递实例。


2
很好。对于“不是异常情况”的判断,你说得很对。我一直认为异常应该被视为异常情况。如果它并不真的是一个异常情况,那么它就不应该被视为异常情况。加1,@KLE。 - Rap

2

也许你根本不应该使用Enum? 如果你经常处理在Enum中未定义的值,那么你应该使用像HashMap<String, Fruit>这样的东西。这样,你可以使用containsKey()方法来查找特定的键是否存在。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接