比较 Java 枚举成员:== 或 equals()?

我知道 Java 枚举被编译为具有私有构造函数和一堆公共静态成员的类。比较给定枚举的两个成员时,我总是使用.equals() ,例如

public useEnums(SomeEnum a)
{
    if(a.equals(SomeEnum.SOME_ENUM_VALUE))
    {
        ...
    }
    ...
}

但是,我遇到了一些使用 equals 运算符==而不是. equals()的代码:

public useEnums2(SomeEnum a)
{
    if(a == SomeEnum.SOME_ENUM_VALUE)
    {
        ...
    }
    ...
}

我应该使用哪个运算符?

答案

两者在技术上都是正确的。如果您查看.equals()的源代码,则它仅.equals() ==

我使用== ,但是这将是 null 安全的。

可以在enum上使用==吗?

是的:枚举具有严格的实例控件,使您可以使用==比较实例。这是语言规范提供的保证(我强调):

JLS 8.9 枚举

枚举类型除了由其枚举常量定义的实例外,没有其他实例。

尝试显式实例化枚举类型是编译时错误。 Enumfinal clone方法可确保永远不会克隆enum常量,并且序列化机制的特殊处理可确保不会因反序列化而创建重复的实例。禁止枚举类型的反射实例化。总之,这四件事确保了enum类型的实例不存在超出enum常量定义的实例的情况。

因为只有一个每个实例enum常数,允许使用==操作者在适当位置的equals比较两个对象的引用时,如果已知它们中的至少一个是指方法enum常数 。 ( Enumequals方法是final方法,仅在其参数上调用super.equals并返回结果,从而执行身份比较。)

Josh Bloch 建议这种保证足够强大,如果您坚持使用单例模式,则实现它的最佳方法是使用单元素enum (请参阅: 有效的 Java 2nd Edition,第 3 项:使用私有构造函数或枚举类型 ;也包括 Singleton 中的线程安全性


==equals之间有什么区别?

提醒一下,通常必须说==并不是equals的可行选择。但是,当使用时(例如enum ),要考虑两个重要的区别:

==从不抛出NullPointerException

enum Color { BLACK, WHITE };

Color nothing = null;
if (nothing == Color.BLACK);      // runs fine
if (nothing.equals(Color.BLACK)); // throws NullPointerException

==在编译时接受类型兼容性检查

enum Color { BLACK, WHITE };
enum Chiral { LEFT, RIGHT };

if (Color.BLACK.equals(Chiral.LEFT)); // compiles fine
if (Color.BLACK == Chiral.LEFT);      // DOESN'T COMPILE!!! Incompatible types!

如果适用,应使用==

Bloch 特别提到对实例进行适当控制的不可变类可以向其客户保证==可用。具体enum来举例说明。

项目 1:考虑静态工厂方法而不是构造函数

[...] 它允许不可变类保证不存在两个相等的实例: a.equals(b)当且仅当a==b 。如果一个类保证了这一点,那么它的客户可以使用==运算符而不是equals(Object)方法,这可能会提高性能。枚举类型提供了这种保证。

总而言之,在enum上使用==的参数为:

  • 有用。
  • 它更快。
  • 在运行时更安全。
  • 在编译时更安全。

使用==比较两个枚举值是可行的,因为每个枚举常量只有一个对象。

附带说明一下,如果您像这样编写equals() ,则实际上无需使用==来编写空安全代码:

public useEnums(final SomeEnum a) {
    if (SomeEnum.SOME_ENUM_VALUE.equals(a)) {
        …
    }
    …
}

这是被称为 “ 从左边比较常量”的最佳实践,您绝对应该遵循。

正如其他人所说, ==.equals()在大多数情况下都有效。您没有比较其他人指出的完全不同类型的对象的编译时间确定性是有效和有益的,但是比较两种不同编译时间类型的对象的特殊类型的错误也可以通过 FindBugs 找到(并且可能通过 Eclipse / IntelliJ 编译时间检查),因此 Java 编译器发现它并没有增加太多额外的安全性。

然而:

  1. 这样的事实==不会抛出 NPE 在我心目中是一个缺点 == 。几乎不需要enum类型为null ,因为您可能希望通过null表示的任何额外状态都可以作为附加实例添加到enum中。如果它意外地为null ,那么我宁愿拥有一个 NPE 而不是==默默地评估为 false。因此,我不同意它在运行时的观点是更安全的 。最好养成永远不要让enum值为@Nullable的习惯。
  2. == 更快的说法也是假的。在大多数情况下,您将在编译时类型为 enum 类的变量上调用.equals() ,在这些情况下,编译器可以知道这与==相同(因为enumequals()方法可以(不会被覆盖)并可以优化函数调用。我不确定编译器当前是否这样做,但是如果没有这样做,结果证明这是 Java 整体的性能问题,那么我宁愿修复该编译器,也不愿让 100,000 个 Java 程序员更改其编程样式以适应特定编译器版本的性能特征。
  3. enums是对象。对于所有其他对象类型,标准比较是.equals() ,而不是== 。我认为对enums进行例外处理是危险的,因为您可能最终会意外地将 Object 与==而不是equals()进行比较,尤其是当您将enum重构为非枚举类时。在这种重构的情况下,从上面的工作原理是错误的。为了使自己确信==的使用是正确的,您需要检查所讨论的值是enum还是基元。如果它是一个非enum类,那将是错误的,但是很容易遗漏,因为代码仍然可以编译。使用.equals()错误的唯一情况是所讨论的值是否是基元;在这种情况下,代码将无法编译,因此更容易遗漏。因此, .equals()更容易被识别为正确的,并且对于将来的重构更安全。

我实际上认为 Java 语言应该在 Objects 上定义 ==,以在左侧值上调用. equals(),并为对象标识引入一个单独的运算符,但这不是 Java 的定义方式。

总之,我仍然认为参数支持对enum类型使用.equals()

我更喜欢使用==而不是equals

除了这里已经讨论的其他原因之外,其他原因是您可能会引入一个错误而没有意识到。假设您有一个完全相同的枚举,但是使用了单独的包(虽然不常见,但是可能会发生):

第一个枚举

package first.pckg

public enum Category {
    JAZZ,
    ROCK,
    POP,
    POP_ROCK
}

第二枚举:

package second.pckg

public enum Category {
    JAZZ,
    ROCK,
    POP,
    POP_ROCK
}

然后,假设你使用等号就像在下一item.category这是first.pckg.Category但在导入第二枚举( second.pckg.Category ),而不是第一个没有意识到这一点:

import second.pckg.Category;
...

Category.JAZZ.equals(item.getCategory())

因此,尽管您期望为 true,但是由于item.getCategory()JAZZ所以您将始终得到false因为它是一个不同的枚举。这可能很难看。

因此,如果改用运算符== ,则会出现编译错误:

运算符 == 不能应用于 “second.pckg.Category”,“first.pckg.Category”

import second.pckg.Category; 
...

Category.JAZZ == item.getCategory()

这是比较两者的粗略时序测试:

import java.util.Date;

public class EnumCompareSpeedTest {

    static enum TestEnum {ONE, TWO, THREE }

    public static void main(String [] args) {

        Date before = new Date();
        int c = 0;

        for(int y=0;y<5;++y) {
            for(int x=0;x<Integer.MAX_VALUE;++x) {
                if(TestEnum.ONE.equals(TestEnum.TWO)) {++c;}
                if(TestEnum.ONE == TestEnum.TWO){++c;}              
            }
        }

        System.out.println(new Date().getTime() - before.getTime());
    }   

}

一次注释掉 IF。这是上面两个反汇编后的字节码比较:

21  getstatic EnumCompareSpeedTest$TestEnum.ONE : EnumCompareSpeedTest.TestEnum [19]
 24  getstatic EnumCompareSpeedTest$TestEnum.TWO : EnumCompareSpeedTest.TestEnum [25]
 27  invokevirtual EnumCompareSpeedTest$TestEnum.equals(java.lang.Object) : boolean [28]
 30  ifeq 36

 36  getstatic EnumCompareSpeedTest$TestEnum.ONE : EnumCompareSpeedTest.TestEnum [19]
 39  getstatic EnumCompareSpeedTest$TestEnum.TWO : EnumCompareSpeedTest.TestEnum [25]
 42  if_acmpne 48

第一个(等于)执行虚拟调用并测试堆栈中的返回布尔值。第二个(==)直接从堆栈中比较对象地址。在第一种情况下,活动更多。

我一次使用两个 IF 多次运行了此测试。 “==” 的速度是如此之快。

如果枚举是正确的,那么正确!

tl; dr

另一个选项是Objects.equals实用程序方法。

Objects.equals( thisEnum , thatEnum )

用于 null 安全性的Objects.equals

等于运算符 == 而不是. equals()

我应该使用哪个运算符?

第三种选择是在Java 7和更高版本中添加Objects实用工具类上找到的 static equals方法。

这是使用Month枚举的示例。

boolean areEqual = Objects.equals( Month.FEBRUARY , Month.JUNE ) ;  // Returns `false`.

好处

我发现此方法有很多好处:

  • 零安全
  • 紧凑,可读

怎么运行的

Objects.equals使用什么逻辑?

OpenJDKJava 10 源代码中亲自了解一下:

return (a == b) || (a != null && a.equals(b));

使用==以外的任何东西来比较枚举常量都是无稽之谈。这就像class对象与equals对象进行比较 –不要这样做!

但是,在 Sun JDK 6u10 及更早版本中存在一个讨厌的错误( BugId 6277781 ),由于历史原因,这可能很有趣。该错误阻止在反序列化枚举上正确使用== ,尽管可以说这是一个极端的情况。

枚举是类,它为public static final field (不可变)声明的每个枚举常量返回一个实例(如单例),因此==运算符可用于检查其相等性,而不是使用equals()方法