【问题标题】:Best practice to look up Java Enum查找 Java 枚举的最佳实践
【发布时间】:2010-03-10 16:44:10
【问题描述】:

我们有一个 REST API,客户端可以在其中提供表示在 Java 枚举中定义在服务器上的值的参数。

所以我们可以提供描述性错误,我们将这个 lookup 方法添加到每个 Enum。好像我们只是在复制代码(不好)。有更好的做法吗?

public enum MyEnum {
    A, B, C, D;

    public static MyEnum lookup(String id) {
        try {
            return MyEnum.valueOf(id);
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("Invalid value for my enum blah blah: " + id);
        }
    }
}

更新valueOf(..) 提供的默认错误消息为No enum const class a.b.c.MyEnum.BadValue。我想从 API 中提供一个更具描述性的错误。

【问题讨论】:

  • 好吧,IllegalArgumentException 没有什么问题,它已经是一个 RuntimeException。您想在这里改进什么?
  • 添加比仅使用valueOf(和IllegalArgumentException)时提供的更具描述性的消息

标签: java enums


【解决方案1】:

也许你可以实现通用静态lookup 方法。

像这样

public class LookupUtil {
   public static <E extends Enum<E>> E lookup(Class<E> e, String id) {   
      try {          
         E result = Enum.valueOf(e, id);
      } catch (IllegalArgumentException e) {
         // log error or something here

         throw new RuntimeException(
           "Invalid value for enum " + e.getSimpleName() + ": " + id);
      }

      return result;
   }
}

那你就可以了

public enum MyEnum {
   static public MyEnum lookup(String id) {
       return LookupUtil.lookup(MyEnum.class, id);
   }
}

或显式调用实用程序类查找方法。

【讨论】:

  • 你不能创建一个基础枚举来扩展子枚举,因为“所有枚举都隐式扩展 java.lang.Enum。由于 Java 不支持多重继承,所以枚举不能扩展其他任何东西”,所以没有地方放这段代码,除非你想把它放在某种静态实用程序类中并使用枚举类参数调用它。
  • 与仅调用 Enum.valueOf 相比,您的实用程序方法有什么优势?
  • 发布和预处理操作以及错误处理。据我了解,这是跨不同枚举类型复制和粘贴的错误处理。
  • 还是不明白你为什么要这么做。您将IllegalArgumentException 包装在实用程序类抛出的运行时异常中......为了什么收获?由 Enum 中的 valueOf() 实现抛出的默认异常就足够了:它给出了无效类型,即作为异常消息的一部分输入。
  • 您也可以直接调用MyEnum.valueOf(id),而不是调用Enum.valueOf(MyEnum.class, id)。因为IllegalArgumentExceptionRuntimeException 更合适,所以根本不需要那个实用方法。
【解决方案2】:

看起来你在这里有一个不好的做法,但不是你想的。

捕获IllegalArgumentException 以重新抛出另一个带有更清晰消息的RuntimeException 可能看起来是个好主意,但事实并非如此。因为这意味着您关心异常中的消息。

如果您关心异常中的消息,那么这意味着您的用户以某种方式看到了您的异常。这很糟糕。

如果您想向您的用户提供明确的错误消息,您应该在解析用户输入时检查枚举值的有效性,如果用户输入不正确,则在响应中发送相应的错误消息。

类似:

// This code uses pure fantasy, you are warned!
class MyApi
{
    // Return the 24-hour from a 12-hour and AM/PM

    void getHour24(Request request, Response response)
    {
        // validate user input
        int nTime12 = 1;
        try
        {
            nTime12 = Integer.parseInt(request.getParam("hour12"));
            if( nTime12 <= 0 || nTime12 > 12 )
            {
                throw new NumberFormatException();
            }
        }
        catch( NumberFormatException e )
        {
            response.setCode(400); // Bad request
            response.setContent("time12 must be an integer between 1 and 12");
            return;
        }

        AMPM pm = null;
        try
        {
            pm = AMPM.lookup(request.getParam("pm"));
        }
        catch( IllegalArgumentException e )
        {
            response.setCode(400); // Bad request
            response.setContent("pm must be one of " + AMPM.values());
            return;
        }

        response.setCode(200);
        switch( pm )
        {
            case AM:
                response.setContent(nTime12);
                break;
            case PM:
                response.setContent(nTime12 + 12);
                break;
        }
        return;
    }
}

【讨论】:

  • 我认为本质上我们会按照您的建议行事。我们抛出异常,然后堆栈上更高的类捕获它,然后通过 REST API(在定义的错误响应中)仅将异常消息返回给客户端
  • 我的建议是,你不应该捕获泛型异常来返回它们的消息,而是返回意味着解释为什么捕获异常的消息。根据您的异常进行通用错误响应是内部架构向客户端的泄漏,并且是一种不好的做法。您的 API 的错误消息不应依赖于实现的错误报告架构(在本例中为 Java 异常)。
【解决方案3】:

当涉及到 Rest/Json 等时,我们所有的枚举都是这样的。 它的优点是错误是人类可读的,并且还为您提供了可接受的值列表。我们正在使用自定义方法 MyEnum.fromString 而不是 MyEnum.valueOf,希望对您有所帮助。

public enum MyEnum {

    A, B, C, D;

    private static final Map<String, MyEnum> NAME_MAP = Stream.of(values())
            .collect(Collectors.toMap(MyEnum::toString, Function.identity()));

    public static MyEnum fromString(final String name) {
        MyEnum myEnum = NAME_MAP.get(name);
        if (null == myEnum) {
            throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s", name, Arrays.asList(values())));
        }
        return myEnum;
    }
}

例如,如果你打电话

MyEnum value = MyEnum.fromString("X");

您将收到带有以下消息的 IllegalArgumentException:

'X' 没有对应的值。接受的值:[A、B、C、D]

您可以将 IllegalArgumentException 更改为自定义。

【讨论】:

    【解决方案4】:

    如果您希望查找不区分大小写,您可以遍历值使其更友好一点:

     public enum MyEnum {
       A, B, C, D;
    
          public static MyEnum lookup(final String id) {
            for(MyEnum enumValue: values()){
               if(enumValue.name().equalsIgnoreCase(id)){
                  return enumValue;
               }
            }  
            throw new RuntimeException("Invalid value for my enum: " + id);
           }
    }
    

    【讨论】:

    • 好建议,不是我真正想要的,而是一个好主意。请注意,在您的 lookup 中,您实际上并没有从该方法返回任何值。
    • 好点。应该是: if(enum.toString().equalsIgnoreCase(id)) return enum;然后,您可以消除布尔值并在通过循环时抛出异常。
    • 我建议使用enum.name() 而不是enum.toString(),因为toString 方法可能会被覆盖并返回某种人类可读的描述而不是枚举常量的名称。
    【解决方案5】:

    Guava 还提供了这样的功能,如果找不到枚举,它将返回 Optional

    Enums.getIfPresent(MyEnum.class, id).toJavaUtil()
                .orElseThrow(()-> new RuntimeException("Invalid enum blah blah blah.....")))
    

    【讨论】:

      【解决方案6】:

      为什么我们要写那 5 行代码?

      public class EnumTest {
      public enum MyEnum {
          A, B, C, D;
      }
      
      @Test
      public void test() throws Exception {
          MyEnum.valueOf("A"); //gives you A
          //this throws ILlegalargument without having to do any lookup
          MyEnum.valueOf("RADD"); 
      }
      }
      

      【讨论】:

      • 思路/要求是重新抛出特殊异常
      • 没错。有关“默认”错误与更具描述性的错误的更多描述,请参阅更新后的帖子。
      • 在这种情况下,@"Mykola Golubyev" 的答案应该已经解决了您的问题,即静态查找实用程序类,您可以在 catch 块中抛出您想要的任何消息/异常?
      【解决方案7】:

      IllegalArgumentException 中的错误消息已经足够描述了。

      您的方法从一个特定的例外中产生了一个通用例外,并简单地改写了相同的消息。开发人员更喜欢特定的异常类型,并且可以适当地处理这种情况,而不是尝试处理 RuntimeException。

      如果目的是让消息对用户更友好,那么对枚举值的引用无论如何都与它们无关。让 UI 代码决定应该向用户显示什么,UI 开发人员最好使用 IllegalArgumentException。

      【讨论】:

      • “默认”错误消息为:No enum const class a.b.c.MyEnum.BadValue。我希望从 REST API 返回更相关的错误消息。创建前端的开发人员不会真正知道如何处理默认错误,而只是显示它 - 这是我想避免的。
      • 我看不到您的示例消息如何更好。两者都不适合用户,因此应处理异常并显示正确的面向用户的消息。这与将消息放在异常中不同,这应该是针对开发人员的。
      【解决方案8】:

      更新:正如 GreenTurtle 正确评论的那样,以下是错误的


      我只想写

      boolean result = Arrays.asList(FooEnum.values()).contains("Foo");
      

      这可能不如捕捉运行时异常的性能,但可以使代码更简洁。捕获这样的异常总是一个坏主意,因为它很容易被误诊。 当检索比较值本身导致 IllegalArgumentException 时会发生什么?然后,这将被视为枚举数的不匹配值。

      【讨论】:

      • 除非您在 FooEnum 类中实现可以处理 String 对象的 equals 方法,否则此代码将永远不会返回 true。发生这种情况是因为生成的 ArrayList 包含 FooEnum 类型的对象,并且如果使用 String 对象请求包含,则始终返回 false。
      • @GreenTurtle 是的,您显然是对的,忘记了 Enum#equals 仅适用于对象比较。对于枚举,您甚至不应该能够实现自己的等号。我仍然会尽量避免捕获通用运行时异常,只是遍历它,进行字符串比较。
      • 布尔结果 = Arrays.stream(FooEnum.values()).map(Enum::name).collect(Collectors.toList()).contains("Foo");
      【解决方案9】:

      Apache Commons Lang 3 包含类 EnumUtils。如果您没有在项目中使用 Apache Commons,那么您做错了。你在重新发明轮子!

      我们可以使用十几种很酷的方法而不会引发异常。例如:

      获取类的枚举,如果找不到则返回 null。

      此方法与 Enum.valueOf 的不同之处在于它不会抛出 无效枚举名称的异常并执行不区分大小写 名称匹配。

      EnumUtils.getEnumIgnoreCase(SeasonEnum.class, season);
      

      【讨论】:

        【解决方案10】:

        您可以使用静态查找映射来避免异常并返回 null,然后随意抛出:

        public enum Mammal {
            COW,
            MOUSE,
            OPOSSUM;
        
            private static Map<String, Mammal> lookup = 
                    Arrays.stream(values())
                          .collect(Collectors.toMap(Enum::name, Function.identity()));
        
            public static Mammal getByName(String name) {
                return lookup.get(name);
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2011-07-16
          • 1970-01-01
          • 2013-11-09
          • 2012-11-01
          • 1970-01-01
          • 2020-05-09
          • 1970-01-01
          • 2010-10-19
          • 2010-10-03
          相关资源
          最近更新 更多