为什么Java中Enum的compareTo方法是final的?

105
在Java中,枚举类型实现了Comparable接口。很遗憾无法覆盖Comparable的compareTo方法,因为它被标记为final。Enum的默认自然顺序是按枚举列表中的顺序排列。
有人知道为什么Java中的枚举类型有这个限制吗?

在Effective Java - 3rd Edition的第10项中有一个非常好的解释(处理equals()),但在第14项中,他们告诉我们compareTo()的问题是相同的。简而言之:如果您扩展了一个可实例化的类(如枚举)并添加了一个值组件,则无法保留equals(或compareTo)合同。 - Christian H. Kuhn
@ChristianH.Kuhn 这听起来像是一个反对这种限制的论点!大多数人在添加值组件时都希望添加一个compareTo方法,正是因为他们想要保留equals/compareTo协议。 - Adam Burley
5个回答

127

为了保持一致性,当你看到一个enum类型时,你可以确定它的自然排序是按照常量声明的顺序。

要解决这个问题,你可以很容易地创建自己的Comparator<MyEnum>并在需要不同排序的时候使用它:

enum MyEnum
{
    DOG("woof"),
    CAT("meow");

    String sound;    
    MyEnum(String s) { sound = s; }
}

class MyEnumComparator implements Comparator<MyEnum>
{
    public int compare(MyEnum o1, MyEnum o2)
    {
        return -o1.compareTo(o2); // this flips the order
        return o1.sound.length() - o2.sound.length(); // this compares length
    }
}

您可以直接使用Comparator

MyEnumComparator comparator = new MyEnumComparator();
int order = comparator.compare(MyEnum.CAT, MyEnum.DOG);

或者将其用于集合或数组中:

NavigableSet<MyEnum> set = new TreeSet<MyEnum>(comparator);
MyEnum[] array = MyEnum.values();
Arrays.sort(array, comparator);    

更多信息:


自定义比较器只有在将枚举类型提供给集合时才真正有效。如果您想进行直接比较,它并没有太大帮助。 - Martin OConnor
7
是的,它会。通过新的MyEnumComparator.compare(enum1, enum2)方法即可完成。Et voilá。 - Bombe
@martinoconnor和Bombe:我已经将你们的评论纳入答案中。谢谢! - Zach Scrivena
由于 MyEnumComparator 没有状态,因此它应该只是一个单例,特别是如果您正在执行 @Bombe 建议的操作;相反,您可以执行类似 MyEnumComparator.INSTANCE.compare(enum1, enum2) 的操作,以避免不必要的对象创建。 - kbolino
2
@kbolino:比较器甚至可以是枚举类内部的嵌套类。它可以存储在枚举中的LENGTH_COMPARATOR静态字段中。这样,任何使用该枚举的人都可以轻松找到它。 - Lii
2
我对你的compare方法有些困惑,因为它有两个连续的return语句。请问你能解释一下吗? - Sam

41

提供一个使用源代码顺序的compareTo的默认实现是可以的;将其设置为final是Sun犯的一个错误。ordinal已经考虑了声明顺序。我同意,在大多数情况下,开发人员只需要逻辑地对他们的元素进行排序,但有时候人们希望按照一种使可读性和维护性最重要的方式组织源代码。例如:


  //===== SI BYTES (10^n) =====//

  /** 1,000 bytes. */ KILOBYTE (false, true,  3, "kB"),
  /** 10<sup>6</sup> bytes. */   MEGABYTE (false, true,  6, "MB"),
  /** 10<sup>9</sup> bytes. */   GIGABYTE (false, true,  9, "GB"),
  /** 10<sup>12</sup> bytes. */  TERABYTE (false, true, 12, "TB"),
  /** 10<sup>15</sup> bytes. */  PETABYTE (false, true, 15, "PB"),
  /** 10<sup>18</sup> bytes. */  EXABYTE  (false, true, 18, "EB"),
  /** 10<sup>21</sup> bytes. */  ZETTABYTE(false, true, 21, "ZB"),
  /** 10<sup>24</sup> bytes. */  YOTTABYTE(false, true, 24, "YB"),

  //===== IEC BYTES (2^n) =====//

  /** 1,024 bytes. */ KIBIBYTE(false, false, 10, "KiB"),
  /** 2<sup>20</sup> bytes. */   MEBIBYTE(false, false, 20, "MiB"),
  /** 2<sup>30</sup> bytes. */   GIBIBYTE(false, false, 30, "GiB"),
  /** 2<sup>40</sup> bytes. */   TEBIBYTE(false, false, 40, "TiB"),
  /** 2<sup>50</sup> bytes. */   PEBIBYTE(false, false, 50, "PiB"),
  /** 2<sup>60</sup> bytes. */   EXBIBYTE(false, false, 60, "EiB"),
  /** 2<sup>70</sup> bytes. */   ZEBIBYTE(false, false, 70, "ZiB"),
  /** 2<sup>80</sup> bytes. */   YOBIBYTE(false, false, 80, "YiB");

以上排序在源代码中看起来不错,但是作者认为compareTo应该按字节数排序。实现这种排序的源代码会降低代码组织性。

作为枚举类型的客户端,我并不关心作者如何组织他们的源代码。但我希望他们的比较算法有一定的意义。Sun公司让源代码编写者处于了困境中。


4
同意,我希望我的枚举类型可以拥有一种商业可比较的算法,而不是一个顺序,如果有人不注意可能会打破这个顺序。 - TheBakker

7

枚举值按照声明的顺序进行逻辑排序。这是Java语言规范的一部分。因此,只有在同一个枚举类型中的成员才能进行比较。规范进一步保证了compareTo()返回的可比较顺序与声明值的顺序相同。这就是枚举的定义。


正如托马斯·潘恩的例子所表明的那样,语言只能按照句法进行排序,而不能按照语义进行排序。你说,这些项目在逻辑上是“有序”的,但我理解enum的方式是,这些项目是通过逻辑手段封装起来的。 - Bondax

3

一个可能的解释是,compareTo 应该与 equals 保持一致。

而且枚举类型的 equals 应该与标识相等性 (==) 保持一致。

如果 compareTo 不是 final 的话,就有可能将其覆盖为与 equals 不一致的行为,这将非常违反直觉。


虽然建议compareTo()与equals()一致,但这并非必须的。 - Christian H. Kuhn

1

如果你想改变枚举元素的自然顺序,可以在源代码中改变它们的顺序。


是的,这就是我在原始记录中写的 :) - neu242
是的,但您没有真正解释为什么要覆盖compareTo()方法。因此,我的结论是您试图做一些不好的事情,我试图向您展示更正确的方法。 - Bombe
我不明白为什么我要手动排序条目,当计算机比我做得更好。 - neu242
这个问题出现的原因是我有一个包含{isocode,countryname}的枚举。它根据countryname手动排序,按预期运行。如果我现在决定根据isocode进行排序会怎样? - neu242
那么你可能不应该使用枚举。就像我说的那样。 - Bombe
显示剩余2条评论

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