为什么java.lang.Number没有实现Comparable接口?

152

有人知道为什么 java.lang.Number 没有实现 Comparable 吗?这意味着您无法使用 Collections.sortNumber 进行排序,这似乎有点奇怪。

帖子讨论更新:

感谢所有有用的回复。最终我又做了一些关于这个问题的研究

java.lang.NumberAtomicIntegerAtomicLongBigDecimalBigIntegerByteDoubleFloatIntegerLong 以及 Short 的抽象超类型。在该列表中,AtomicIntegerAtomicLong 都没有实现 Comparable 接口。

通过调查,我发现将可变类型实现 Comparable 不是一个好的实践,因为对象可以在比较期间或之后发生更改,使比较结果无效。 AtomicLongAtomicInteger 都是可变的。API 设计人员有预见性,没有让 Number 实现 Comparable 接口,因为这将限制未来子类型的实现。事实上,在最初实现 java.lang.Number 之后很久,AtomicLongAtomicInteger 才被添加到 Java 1.5 中。

除了可变性之外,这里可能还有其他考虑因素。在Number中实现compareTo需要将所有数字值提升为BigDecimal,因为它能够容纳所有Number子类型。在数学和性能方面,这种升级的含义有点不清楚,但我的直觉认为这种解决方案很笨拙。

1
只是提一下:比较两个任意数字可能是一个无限的操作。请参见https://dev59.com/WWcs5IYBdhLWcg3w0HIa#12561805 - SpaceTrucker
12个回答

75

值得一提的是,以下表达式:

new Long(10).equals(new Integer(10))

这个值始终为false,这往往会在某个时候让每个人都感到困扰。因此,你不仅无法比较任意的Number,甚至无法确定它们是否相等。

另外,对于真实的原始类型(floatdouble),确定两个值是否相等是棘手的,并且必须在可接受的误差范围内完成。请尝试使用以下代码:

double d1 = 1.0d;
double d2 = 0.0d;
for (int i=0; i<10; i++) {
  d2 += 0.1d;
}
System.out.println(d2 - d1);

如果你比较两个数字,剩下的差异可能很小。

因此,回到让Number可比较的问题。你该如何实现?使用类似于doubleValue()这样的方法不能保证可靠性。请记住,Number子类型包括:

  • Byte
  • Short
  • Integer
  • Long
  • AtomicInteger
  • AtomicLong
  • Float
  • Double
  • BigInteger
  • BigDecimal

你能编写一个可靠的compareTo()方法而不会堕落为一系列if instanceof语句吗?Number实例只有六种可用方法:

  • byteValue()
  • shortValue()
  • intValue()
  • longValue()
  • floatValue()
  • doubleValue()

因此,我猜Sun做出了(合理的)决定,即Number仅与其自身的实例相比较。


14
谢谢,Cletus。我认为应该有一种中间数字类型,比如 RealNumber(如上所述),封装了 Float、Double、Long、Integer 和 Short。否则,你会缺乏多态性,最终导致这些类型及其对应的非封装原始类型在代码中出现冗余。 - Julien Chastang
1
Sun公司本可以将Number定义为抽象类,并将Comparable作为一个接口包含其中。这样,所有特定的子类都可以适当地实现其类型的可比较性,而无需使用instanceof。我认为不在Number类型上具有Comparable是一个缺点。 - Jason
@Jason 那样做会更加混乱,因为你会期望能够比较数字,但并不总是有效。 - mjaggard
2
对于了解系统的专业用户来说,这可能更加令人困惑......或者也可能不是。对于专业用户来说,这也会更简单。在集合API中也存在同样的权衡。许多抽象方法对于某些底层实现来说是没有意义的。Sun选择拥有方法的抽象版本,并建议如果实现不支持该方法,则抛出异常,而不是将方法从API中排除并要求程序员检查实现的类型。请参见List.setMap.putSet.remove和其他几十个方法。 - Jason

46
有关答案,请参见Java bugparade bug 4414323。您还可以在comp.lang.java.programmer中找到讨论。
引用2001年Sun对错误报告的回应:
“所有的‘数字’都是不可比较的; 可比较假设数字的完全排序是可能的。 即使对于浮点数也不是如此; NaN(不是一个数字)既不小于、也不大于、也不等于任何浮点数值,甚至不等于自身。 {Float, Double}.compare施加了一个与浮点数‘<’和‘=’运算符不同的完全排序。 此外,目前实现的Number的子类只能与同类的其他实例进行比较。 还有其他情况,比如复数,不存在标准的完全排序,尽管可以定义一个。 简而言之,是否可比较Number的子类应该由该子类自行决定。”

2
Eddie:谢谢你提供的链接。我同意这个错误报告,它说Java需要一个RealNumber类型,它将是所有数字类型的超类,并具有相应的原始数字类型(例如Float等)。如果没有这个类型,在Java中处理数字仍然很困难。 - Julien Chastang
这里有另一个(有些不尽如人意的)解释:http://forums.sun.com/thread.jspa?threadID=5306968 - Julien Chastang
有没有可能我们可以编辑这个答案,并直接引用文章中的一句话?它很短,应该没有问题。 - Tim Frey
2
Sun可以选择一个合理的默认值,程序员可以覆盖它。这不是借口。当涉及到空字符串或null引用时,字符串也存在同样的问题。null引用是否大于或小于字符串“foo”?Sun选择了一个有意义的排序方式。 - Jason

5
为了在数字上实现可比性,您需要为每个子类对编写代码。相反,更容易的方法是允许子类实现可比性。

5
尝试解决原始问题(对数字列表进行排序)的一个选择是声明将数字列表作为通用类型,其扩展了Number并实现了Comparable接口。

像这样:

<N extends Number & Comparable<N>> void processNumbers(List<N> numbers) {
    System.out.println("Unsorted: " + numbers);
    Collections.sort(numbers);
    System.out.println("  Sorted: " + numbers);
    // ...
}

void processIntegers() {
    processNumbers(Arrays.asList(7, 2, 5));
}

void processDoubles() {
    processNumbers(Arrays.asList(7.1, 2.4, 5.2));
}

3
很可能是因为比较数字相对效率较低 - 每个数字都可以适合比较的唯一表示方式是BigDecimal。
相反,非原子子类的Number本身实现了Comparable。
原子类是可变的,因此无法实现原子比较。

不是这样的。问题在于并非所有数字都可以在总序中相互比较。 - Eddie
继续吧 - 在java.lang中,所有Number的子类都是有理数;实数有一个完全的排序;问题在哪里? - Pete Kirkham
NaN 不能与任何其他值进行比较。它既不小于,也不大于,也不等于任何值,包括它本身。 - Eddie
3
现有的类似工作的实现方式已经考虑到这点了(如果我没记错的话是通过将NaN置于负无穷之下)。这可以成为Double或Float不应实现Comparable接口的一个理由,但它们确实实现了。 - Pete Kirkham
1
是的,因为 NaN 的缘故而不允许比较每个数字有点疯狂。如果能够比较每个数字将会多么酷,对于使用其他语言的人来说,这甚至是一个问题吗? :) - Rob Grant
任何可以通过“Number”接口区分的数字都可以通过“double”和“long”的组合唯一标识,因此只有三种情况需要考虑:double-double、double-long和long-long。如果对“double”值进行比较,则会正确地排列它们,如果不相等。否则,它们应根据其“long”值进行排名。 - supercat

3
你可以使用 Transmorph 来比较数字,使用其 NumberComparator 类。
NumberComparator numberComparator = new NumberComparator();
assertTrue(numberComparator.compare(12, 24) < 0);
assertTrue(numberComparator.compare((byte) 12, (long) 24) < 0);
assertTrue(numberComparator.compare((byte) 12, 24.0) < 0);
assertTrue(numberComparator.compare(25.0, 24.0) > 0);
assertTrue(numberComparator.compare((double) 25.0, (float) 24.0) > 0);
assertTrue(numberComparator.compare(new BigDecimal(25.0), (float) 24.0) > 0);

2

编写自己的比较器

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

public class NumberComparator implements Comparator {
    @SuppressWarnings("unchecked")
    @Override
    public int compare(Number number1, Number number2) {
 if (((Object) number2).getClass().equals(((Object) number1).getClass())) {
     // both numbers are instances of the same type!
     if (number1 instanceof Comparable) {
  // and they implement the Comparable interface
  return ((Comparable) number1).compareTo(number2);
     }
 }
 // for all different Number types, let's check there double values
 if (number1.doubleValue() < number2.doubleValue())
     return -1;
 if (number1.doubleValue() > number2.doubleValue())
     return 1;
 return 0;
    }

    /**
     * DEMO: How to compare apples and oranges.
     */
    public static void main(String[] args) {
 ArrayList listToSort = new ArrayList();
 listToSort.add(new Long(10));
 listToSort.add(new Integer(1));
 listToSort.add(new Short((short) 14));
 listToSort.add(new Byte((byte) 10));
 listToSort.add(new Long(9));
 listToSort.add(new AtomicLong(2));
 listToSort.add(new Double(9.5));
 listToSort.add(new Double(9.0));
 listToSort.add(new Double(8.5));
 listToSort.add(new AtomicInteger(2));
 listToSort.add(new Long(11));
 listToSort.add(new Float(9));
 listToSort.add(new BigDecimal(3));
 listToSort.add(new BigInteger("12"));
 listToSort.add(new Long(8));
 System.out.println("unsorted: " + listToSort);
 Collections.sort(listToSort, new NumberComparator());
 System.out.println("sorted:   " + listToSort);
 System.out.print("Classes:  ");
 for (Number number : listToSort) {
     System.out.print(number.getClass().getSimpleName() + ", ");
 }
    }
}

1
这段代码在一些浮点数上可能无法正常工作。浮点数的内部表示可能导致两个浮点数 A 和 B 在应该相等时由于微小差距而不相等。 - christopheml

1

不同类型的数字之间没有标准比较方法。 但是,您可以编写自己的比较器,并使用它来创建TreeMap<Number, Object>、TreeSet<Number>或Collections.sort(List<Number>, Comparator)或Arrays.sort(Number[], Comparator);


1

为什么这个想法不好呢?

abstract class ImmutableNumber extends Number implements Comparable {
    // do NOT implement compareTo method; allowed because class is abstract
}
class Integer extends ImmutableNumber {
    // implement compareTo here
}
class Long extends ImmutableNumber {
    // implement compareTo here
}

另一种选择可能是声明类 Number 实现 Comparable 接口,省略 compareTo 方法的实现,并在某些类中实现它,比如 Integer,而在像 AtomicInteger 这样的其他类中抛出 UnsupportedException 异常。

0

我的猜想是,如果不实现 Comparable 接口,那么就给了更多实现类选择的灵活性。所有常见的数字类型(Integer、Long、Double 等)都实现了 Comparable 接口。只要元素本身实现了 Comparable 接口,你仍然可以调用 Collections.sort 方法。


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