【问题标题】:What's the advantage of a Java enum versus a class with public static final fields?Java 枚举与具有公共静态最终字段的类相比有什么优势?
【发布时间】:2012-04-15 17:10:27
【问题描述】:

我非常熟悉 C#,但开始更多地使用 Java。我希望知道 Java 中的枚举基本上等同于 C# 中的枚举,但显然情况并非如此。最初,我很高兴得知 Java 枚举可以包含多条数据,这似乎非常有利 (http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html)。但是,从那时起,我发现 C# 中缺少许多微不足道的功能,例如轻松为枚举元素分配某个值的能力,以及因此无需大量努力即可将整数转换为枚举的能力(即Convert integer value to matching Java Enum)。

所以我的问题是:Java 枚举相对于具有一堆公共静态最终字段的类有什么好处吗?还是它只是提供更紧凑的语法?

编辑:让我更清楚。 Java 枚举与具有一堆相同类型的公共静态最终字段 的类相比有什么好处?例如,在第一个链接的行星示例中,枚举与具有这些公共常量的类相比有什么优势:

public static final Planet MERCURY = new Planet(3.303e+23, 2.4397e6);
public static final Planet VENUS = new Planet(4.869e+24, 6.0518e6);
public static final Planet EARTH = new Planet(5.976e+24, 6.37814e6);
public static final Planet MARS = new Planet(6.421e+23, 3.3972e6);
public static final Planet JUPITER = new Planet(1.9e+27, 7.1492e7);
public static final Planet SATURN = new Planet(5.688e+26, 6.0268e7);
public static final Planet URANUS = new Planet(8.686e+25, 2.5559e7);
public static final Planet NEPTUNE = new Planet(1.024e+26, 2.4746e7);

据我所知,卡萨布兰卡的答案是唯一能满足这一点的。

【问题讨论】:

  • @Bohemian:可能不是重复的,因为 OP 只提到了public static final 字段,这些字段可能是输入值,不一定是ints。
  • @Shahzeb 几乎没有。显然,使用枚举而不是字符串常量是一个非常好的主意,而且值得鼓励。类型安全,不需要静态实用程序函数等等。绝对没有理由使用字符串。
  • @Voo 是的,我知道会有分歧。这是 49 秒内的一个。枚举很棒(我喜欢它们并且经常使用它们)但是在声明常量或使用它时需要什么类型的安全性。每次需要为字符串文字声明一个常量时,创建枚举都是一种过度杀伤力。
  • @Shahzeb 如果您只有一个变量,请务必使用字符串,这里不会发生太多事情(单个值作为参数毫无意义)。但请注意,我们谈论的是常量S,所以现在我们谈论的是可能将它们传递给函数等。我们需要类型安全吗?好吧,不,但是大多数人认为“一切都是void*”的c 风格类型很好,它可以阻止错误(特别是如果传递多个枚举/字符串参数!)。它还将常量放入自己的命名空间等。与此相反,仅使用普通变量并没有真正的优势。
  • @Bohemian:我不明白怎么做。对于ints,没有类型安全,因为可以传递任何值。另一方面,类型化对象在类型安全方面与枚举没有什么不同。

标签: java enums


【解决方案1】:
  1. 类型安全和价值安全。
  2. 保证单身。
  3. 能够定义和覆盖方法。
  4. 能够在switch 语句case 语句中使用值而无需限定。
  5. 通过ordinal(). 内置的值序列化
  6. 按名称而不是按值进行序列化,这提供了一定程度的面向未来的能力。
  7. EnumSet 和 EnumMap 类。

【讨论】:

  • 说了这么多,每次我把代码放到枚举中我都后悔了。
  • 为什么后悔?我从来没有……
  • @glglgl 因为它把特定于应用程序的代码放到了一个我觉得它并不真正属于的地方,那实际上只是定义了一组值。如果让我再做一次,我会将它包含在众多 switch 语句之一中,而这正是使用 Enum 的最初动机。
【解决方案2】:

从技术上讲,确实可以将枚举视为具有一堆类型常量的类,这实际上就是枚举常量在内部实现的方式。但是,使用 enum 会为您提供有用的方法 (Enum javadoc),否则您必须自己实现这些方法,例如 Enum.valueOf。

【讨论】:

  • 还有.values()可以遍历值列表。
  • 这似乎是正确的答案,虽然它不是很令人满意。在我看来,Java 仅仅为了更紧凑的语法和对 Enum 方法的访问而添加对枚举的支持是不值得的。
  • @Craig 你的直觉是对的——这是一个非常糟糕的答案,因为它完全没有达到枚举的目的。请参阅问题下方的我的 cmets,了解部分原因。
  • @Bohemian:我没有“错过枚举的目的”——我一直在使用它们。请参阅我对您上述评论的回复。
  • Enum.valuesOf 方法如果调用两次是否返回相同的对象?
【解决方案3】:

没有人提到在switch 语句中使用它们的能力;我也会把它扔进去。

这允许以干净的方式使用任意复杂的枚举,而无需使用instanceof、可能会混淆if 序列或非字符串/int 切换值。典型的例子是状态机。

【讨论】:

  • 无论如何,您没有提到枚举与静态字段相比有什么好处,您可以在带有静态字段的 switch 语句中使用足够多的类型。 Op 需要真正的功能或性能差异
  • @Genaut 好处是枚举比字符串或整数具有更多的功能——问题是关于差异,我提供了。 OP 已经知道枚举是什么,当我 4.5 年前发布这个时,没有其他人提到 switch 语句,并且至少有几个人发现它提供了新信息¯_(ツ)_/¯
  • @Genaut 当Planet 不是enum 时如何切换Planet 实例?
【解决方案4】:

主要优点是类型安全。使用一组常量,可以使用相同内在类型的任何值,从而引入错误。对于枚举,只能使用适用的值。

例如

public static final int SIZE_SMALL  = 1;
public static final int SIZE_MEDIUM = 2;
public static final int SIZE_LARGE  = 3;

public void setSize(int newSize) { ... }

obj.setSize(15); // Compiles but likely to fail later

对

public enum Size { SMALL, MEDIUM, LARGE };

public void setSize(Size s) { ... }

obj.setSize( ? ); // Can't even express the above example with an enum

【讨论】:

  • 类也是类型安全的...:/(假设静态字段是容器类的类型)
  • 你仍然可以传递一个无效的值,但这将是一个更容易发现的编译时错误。
  • 您可以在第二个示例中调用 setSize(null),但它可能会比第一个示例的错误更快地失败。
【解决方案5】:

混乱更少。以Font 为例。它有一个构造函数,它采用您想要的Font 的名称、大小和样式(new Font(String, int, int))。直到今天,我都记不起风格或尺寸是第一位的。如果Font 对其所有不同的样式(PLAIN、BOLD、ITALIC、BOLD_ITALIC)都使用了enum,则其构造函数看起来像Font(String, Style, int),以防止任何混淆。不幸的是,在创建Font 类时,enums 并不存在,而且由于 Java 必须保持反向兼容性,所以我们总是会受到这种模糊性的困扰。

当然,这只是使用enum 而不是public static final 常量的一个论据。枚举也非常适合singletons 并实现默认行为,同时允许以后进行自定义(即strategy pattern)。后者的一个例子是java.nio.file 的OpenOption 和StandardOpenOption:如果开发人员想创建自己的非标准OpenOption,他可以。

【讨论】:

  • 如果要求两个相同的枚举,您的“字体”大小写仍然不明确。这个问题的典型答案是许多语言称之为Named Parameters。那个或更好的 IDE 方法签名支持。
  • @aaaaaa 我还没有看到很多情况,构造函数会使用两个相同的enum,而不使用varargs 或Set 来获取任意数量的它们。
  • @aaaaaa 命名参数的主要问题依赖于实现细节(参数的名称)。我可以做一个接口interface Foo { public void bar(String baz); }。有人创建了一个调用bar 的类:someFoo.bar(baz = "hello");。我将Foo::bar 的签名更改为public void bar(String foobar)。现在打电话给someFoo 的人需要修改他们的代码,如果他们仍然希望它工作。
  • 我也不记得见过连续的枚举类型,但认为常见的枚举类型可能是 DAY_OF_WEEK 或类似的东西。关于界面的好点 - 没有想到这一点。就我个人而言,由于未命名的参数引起的广泛的歧义,我会解决这个问题,因此需要更强大的 IDE 支持。不过,我知道这是一个判断要求,并且需要认真考虑破坏 API 更改。
【解决方案6】:

这里有很多好的答案,但没有人提到高度优化的实现 Collection API 类/接口专门用于枚举:

这些枚举特定类仅接受 Enum 实例(EnumMap 仅接受 Enums 作为键),并且只要有可能,它们在其实现中恢复为紧凑表示和位操作。

这是什么意思?

如果我们的 Enum 类型的元素不超过 64 个(大多数现实生活中的 Enum 示例都符合此条件),则实现将元素存储在单个 long 值中,每个 Enum 实例在问题将与这个 64 位长的long 相关联。向EnumSet 添加一个元素只是将正确的位设置为1,删除它只是将该位设置为0。测试一个元素是否在Set 中只是一个位掩码测试!现在你一定要为此爱上Enums!

【讨论】:

  • 我以前知道这两个,但我刚刚从您的What does this mean? 部分学到了很多东西。我知道大小为 64 的实现存在分歧,但我实际上并不知道为什么
  • 可能值得一提的是,EnumSet 对于包含超过 64 个常量的类型也有相同的优化。它只需要使用long[] 数组而不是单个long。虽然将add、remove 和contains 操作减少为单位操作非常棒,但真正有趣的是addAll、removeAll 和removeAll 和retainAll 这样的操作,它们不会一点一点地进行,而是一次处理 64 个元素(换句话说,通常是所有元素)。
【解决方案7】:

示例:

public class CurrencyDenom {
   public static final int PENNY = 1;
 public static final int NICKLE = 5;
 public static final int DIME = 10;
public static final int QUARTER = 25;}

java 常量的限制

1) 没有类型安全:首先它不是类型安全的;您可以将任何有效的 int 值分配给 int 例如99 虽然没有硬币可以代表这个值。

2) 无意义打印:打印任何这些常量的值将打印其数值而不是有意义的硬币名称,例如当您打印 NICKLE 时,它将打印“5”而不是“NICKLE”

3) 无命名空间:要访问 currencyDenom 常量,我们需要为类名添加前缀,例如CurrencyDenom.PENNY 而不是仅仅使用 PENNY,尽管这也可以通过在 JDK 1.5 中使用静态导入来实现

枚举的优点

1) Java 中的枚举是类型安全的,并且有自己的命名空间。这意味着您的枚举将在下面的示例中具有例如“货币”的类型,并且您不能分配除枚举常量中指定的任何值。

public enum Currency {PENNY, NICKLE, DIME, QUARTER};

Currency coin = Currency.PENNY; coin = 1; //compilation error

2) Java 中的 Enum 是类或接口等引用类型,您可以在 java Enum 中定义构造函数、方法和变量,这使其比 C 和 C++ 中的 Enum 更强大,如下一个 Java Enum 类型示例所示。

3) 您可以在创建时指定枚举常量的值,如下例所示: 公共枚举货币 {PENNY(1), NICKLE(5), DIME(10), QUARTER(25)}; 但要使其工作,您需要定义一个成员变量和一个构造函数,因为 PENNY (1) 实际上正在调用一个接受 int value 的构造函数,请参见下面的示例。

public enum Currency {
    PENNY(1), NICKLE(5), DIME(10), QUARTER(25);
    private int value;

    private Currency(int value) {
            this.value = value;
    }
}; 

参考:https://javarevisited.blogspot.com/2011/08/enum-in-java-example-tutorial.html

【讨论】:

    【解决方案8】:

    正如您已经注意到的,枚举的第一个好处是语法简单。但是枚举的主要目的是提供一组众所周知的常量,默认情况下,这些常量构成一个范围,并通过类型和值安全检查帮助执行更全面的代码分析。

    枚举的这些属性对程序员和编译器都有帮助。例如,假设您看到一个接受整数的函数。该整数可能意味着什么?你可以传递什么样的价值观?你真的不会马上知道。但是如果你看到一个接受枚举的函数,你就会非常清楚你可以传入的所有可能值。

    对于编译器,枚举有助于确定值的范围,除非您为枚举成员分配特殊值,否则它们的范围从 0 到更高。这有助于通过类型安全检查等自动跟踪代码中的错误。例如,编译器可能会警告您,您没有在 switch 语句中处理所有可能的枚举值(即,当您没有 default 大小写并且只处理 N 个枚举值中的一个时)。当您将任意整数转换为枚举时,它还会警告您,因为枚举的值范围小于整数,这反过来可能会触发函数中不真正接受整数的错误。此外,当值为 0 及以上时,为 switch 生成跳转表变得更容易。

    这不仅适用于 Java,对于其他具有严格类型检查的语言也是如此。 C、C++、D、C# 就是很好的例子。

    【讨论】:

      【解决方案9】:

      一个枚举是隐式final的,有一个私有构造函数,它的所有值都是相同类型或子类型,你可以使用values()获取它的所有值,获取它的name()或ordinal()值或您可以按编号或名称查找枚举。

      您还可以定义子类(即使名义上是最终的,但您无法以任何其他方式执行此操作)

      enum Runner implements Runnable {
          HI {
             public void run() {
                 System.out.println("Hello");
             }
          }, BYE {
             public void run() {
                 System.out.println("Sayonara");
             }
             public String toString() {
                 return "good-bye";
             }
          }
       }
      
       class MYRunner extends Runner // won't compile.
      

      【讨论】:

      • 要添加到这个答案,您还可以在接口中定义任何方法的“默认”实现。这样,并不是每个枚举都需要花括号和方法实现。您可以简单地为需要它的枚举值提供内联实现。
      【解决方案10】:

      枚举好处:

      1. 枚举是类型安全的,静态字段不是
      2. 值的数量是有限的(不可能传递不存在的枚举值。如果您有静态类字段,则可能会犯此错误)
      3. 每个枚举都可以分配多个属性(字段/getter) - 封装。还有一些简单的方法: YEAR.toSeconds() 或类似的。比较:Colors.RED.getHex() 与 Colors.toHex(Colors.RED)

      “例如能够轻松地为枚举元素分配某个值”

      enum EnumX{
        VAL_1(1),
        VAL_200(200);
        public final int certainValue;
        private X(int certainValue){this.certainValue = certainValue;}
      }
      

      “因此无需大量努力即可将整数转换为枚举” 添加一个将 int 转换为 enum 的方法。只需添加包含映射的静态 HashMap。

      如果您真的想将 ord=VAL_200.ordinal() 转换回 val_200,只需使用:EnumX.values()[ord]

      【讨论】:

        【解决方案11】:

        最大的优点是枚举单例易于编写和线程安全:

        public enum EasySingleton{
            INSTANCE;
        }
        

        和

        /**
        * Singleton pattern example with Double checked Locking
        */
        public class DoubleCheckedLockingSingleton{
             private volatile DoubleCheckedLockingSingleton INSTANCE;
        
             private DoubleCheckedLockingSingleton(){}
        
             public DoubleCheckedLockingSingleton getInstance(){
                 if(INSTANCE == null){
                    synchronized(DoubleCheckedLockingSingleton.class){
                        //double checking Singleton instance
                        if(INSTANCE == null){
                            INSTANCE = new DoubleCheckedLockingSingleton();
                        }
                    }
                 }
                 return INSTANCE;
             }
        }
        

        两者都是相似的,它通过实现自己处理序列化

        //readResolve to prevent another instance of Singleton
            private Object readResolve(){
                return INSTANCE;
            }
        

        more

        【讨论】:

          【解决方案12】:

          另一个重要的区别是 java 编译器将 primitive types 和 String 的 static final 字段视为文字。这意味着这些常量变为内联的。它类似于 C/C++ #define 预处理器。见this SO question。枚举不是这种情况。

          【讨论】:

            【解决方案13】:

            当您使用枚举时,您会在编译时检查有效值。看this question.

            【讨论】:

              【解决方案14】:

              枚举可以是本地的

              从Java 16 开始,可以在本地(在方法内)定义枚举。除了能够将枚举定义为嵌套类或单独的类之外,此范围还可以。

              这个新的本地定义范围与新的记录功能一起出现。有关详细信息,请参阅JEP 395: Records。枚举、接口和记录都可以在 Java 16+ 中本地定义。

              相比之下,public static final 字段始终具有全局范围。

              【讨论】:

              • 相同变化的一部分是本地类现在可以有static final 字段。它们的范围与本地 enum 的常量相同。然而,当你在一个实例方法中时,普通的本地类仍然有一个对周围this的隐式引用,这会阻止它们像enum一样被使用。
              【解决方案15】:

              我认为enum 不能是final,因为在后台编译器会为每个enum 条目生成子类。

              更多信息来自source

              【讨论】:

              • 在内部,它们不是最终的,因为——正如你所说——可以在内部被子类化。但是很遗憾,您不能自己对它们进行子类化,例如。 G。用自己的价值观扩展它。
              • 子类仅在您明确请求时生成。然后,子类将是final。否则,enum 类型本身将是 final。
              【解决方案16】:

              这里发布的枚举有很多优点,我现在正在按照问题中的要求创建这样的枚举。 但我有一个包含 5-6 个字段的枚举。

              enum Planet{
              EARTH(1000000, 312312321,31232131, "some text", "", 12),
              ....
              other planets
              ....
              

              在这种情况下,当枚举中有多个字段时,很难理解哪个值属于哪个字段,因为您需要查看构造函数和眼球。

              具有static final 常量的类并使用Builder 模式创建此类对象使其更具可读性。但是,如果需要,您将失去使用枚举的所有其他优势。 此类类的一个缺点是,您需要手动将 Planet 对象添加到 Planets. 的 list/set 中

              我仍然更喜欢枚举而不是此类,因为values() 派上用场,你永远不知道将来是否需要它们在switch 或EnumSet 或EnumMap 中使用:)

              【讨论】:

                【解决方案17】:

                主要原因:枚举可以帮助您编写结构良好的代码,其中参数的语义含义在编译时是清晰的并且是强类型的 - 出于其他答案给出的所有原因。

                Quid pro quo: 在 Java 中开箱即用,Enum 的成员数组是最终的。这通常很好,因为它有助于重视安全性和测试,但在某些情况下,它可能是一个缺点,例如,如果您可能从库中扩展现有的基本代码。相反,如果相同的数据位于具有静态字段的类中,您可以轻松地在运行时添加该类的新实例(您可能还需要编写代码将这些添加到该类的任何 Iterable 中)。但是 Enums 的这种行为可以改变:使用反射,您可以在运行时添加新成员或替换现有成员,尽管这可能只应在别无选择的特殊情况下完成:即,这是一个 hacky 解决方案,可能会产生意想不到的问题,在Can I add and remove elements of enumeration at runtime in Java 上查看我的回答。

                【讨论】:

                  【解决方案18】:

                  你可以这样做:

                  public enum Size { SMALL(1), MEDIUM(2), LARGE(3) };
                  
                  private int sizeValue;
                  
                  Size(sizeValue) {this.sizeValue = value; }
                  

                  因此,您可以获得像这样的大小值 SMALL.getSizeValue();

                  如果你想设置大小枚举不适合你,如果你只定义常量和固定值就可以了。

                  检查这个link也许可以帮助你

                  【讨论】:

                    猜你喜欢
                    • 2011-01-28
                    • 2012-05-02
                    • 2012-12-01
                    • 2012-11-08
                    • 1970-01-01
                    • 2013-12-31
                    • 2021-12-22
                    • 1970-01-01
                    • 2012-03-05
                    相关资源
                    最近更新 更多