127

This question already has an answer here:

Does anyone know why java.lang.Number does not implement Comparable? This means that you cannot sort Numbers with Collections.sort which seems to me a little strange.

Post discussion update:

Thanks for all the helpful responses. I ended up doing some more research about this topic.

The simplest explanation for why java.lang.Number does not implement Comparable is rooted in mutability concerns.

For a bit of review, java.lang.Number is the abstract super-type of AtomicInteger, AtomicLong, BigDecimal, BigInteger, Byte, Double, Float, Integer, Long and Short. On that list, AtomicInteger and AtomicLong to do not implement Comparable.

Digging around, I discovered that it is not a good practice to implement Comparable on mutable types because the objects can change during or after comparison rendering the result of the comparison useless. Both AtomicLong and AtomicInteger are mutable. The API designers had the forethought to not have Number implement Comparable because it would have constrained implementation of future subtypes. Indeed, AtomicLong and AtomicInteger were added in Java 1.5 long after java.lang.Number was initially implemented.

Apart from mutability, there are probably other considerations here too. A compareTo implementation in Number would have to promote all numeric values to BigDecimal because it is capable of accommodating all the Number sub-types. The implication of that promotion in terms of mathematics and performance is a bit unclear to me, but my intuition finds that solution kludgy.


12 답변


63

It's worth mentioning that the following expression:

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

is always false, which tends to trip everyone up at some point or another. So not only can you not compare arbitrary Numbers but you can't even determine if they're equal or not.

Also, with the real primitive types (float, double), determining if two values are equal is tricky and has to be done within an acceptable margin of error. Try code like:

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

and you'll be left with some small difference.

So back to the issue of making Number Comparable. How would you implement it? Using something like doubleValue() wouldn't do it reliably. Remember the Number subtypes are:

  • Byte;
  • Short;
  • Integer;
  • Long;
  • AtomicInteger;
  • AtomicLong;
  • Float;
  • Double;
  • BigInteger; and
  • BigDecimal.

Could you code a reliable compareTo() method that doesn't devolve into a series of if instanceof statements? Number instances only have six methods available to them:

  • byteValue();
  • shortValue();
  • intValue();
  • longValue();
  • floatValue(); and
  • doubleValue().

So I guess Sun made the (reasonable) decision that Numbers were only Comparable to instances of themselves.


  • Thxs Cletus. IMHO there ought to be an intermediate numeric type like RealNumber (see above) encapsulating Float, Double, Long, Integer, Short). Otherwise, you have a lack of polymorphism ultimately resulting in code duplication for those types along with their corresponding unboxed primitive types. - Julien Chastang
  • Sun could have made Number abstract and included Comparable as an interface. Then all the particular subclasses could properly implement comparable for their type without any instanceof's. I think this is a shortcoming to not have Comparable on the Number type. - Jason
  • @Jason That would have been more confusing because then you'd expect to be able to compare numbers but it wouldn't always work. - mjaggard
  • It may be more confusing... or maybe not to the expert user that understands the system. It would also be simpler for the expert user. This same tradeoff is made in the Collections API. A number of abstract methods exist that do not make sense for certain underlying implementations. Instead of excluding the methods from the API and requiring programmers to check the type of the implementation, Sun chose to have abstract versions of the methods and recommend throwing an exception if the implementation does not support the method. See List.set, Map.put, Set.remove and dozens more. - Jason

42

For the answer, see Java bugparade bug 4414323. You can also find a discussion from comp.lang.java.programmer

To quote from the Sun response to the bug report from 2001:

All "numbers" are not comparable; comparable assumes a total ordering of numbers is possible. This is not even true of floating-point numbers; NaN (not a number) is neither less than, greater than, nor equal to any floating-point value, even itself. {Float, Double}.compare impose a total ordering different from the ordering of the floating-point "<" and "=" operators. Additionally, as currently implemented, the subclasses of Number are only comparable to other instances of the same class. There are other cases, like complex numbers, where no standard total ordering exists, although one could be defined. In short, whether or not a subclass of Number is comparable should be left as a decision for that subclass.


  • Eddie: Thanks for the link. I agree with the bug report that says that Java needs a RealNumber type that would be a super class of all the numeric types with a corresponding primitive numeric types (e.g. Float, etc.). Working with numbers in Java remains difficult without one. - Julien Chastang
  • Here is another (somewhat unsatisfying) explanation: forums.sun.com/thread.jspa?threadID=5306968 - Julien Chastang
  • Any chance we can edit this answer with a quote directly from the article? It's short enough that it should fit here with no problems. - Outlaw Programmer
  • Good idea. Done. - Eddie
  • Sun could have picked a reasonable default that programmers could override. This is no excuse. The same problem exists for Strings when it comes to empty strings or null references. Is a null reference greater than or less than the string "foo"? Sun picked an ordering that makes sense. - Jason

4

in order to implement comparable on number, you would have to write code for every subclass pair. Its easier instead to just allow subclasses to implement comparable.


3

Very probably because it would be rather inefficient to compare numbers - the only representation into which every Number can fit to allow such comparison would be BigDecimal.

Instead, non-atomic subclasses of Number implements Comparable itself.

Atomic ones are mutable, so can't implement an atomic comparison.


  • That's not it. It's that not all numbers are comparable to each other in a total ordering. - Eddie
  • Go on - all subclases of Number in java.lang are rationals; there is a total ordering of reals; what is the issue? - Pete Kirkham
  • NaN cannot be ordered with any other value. It is not less than and it is not greater than and it is not equal to any value, including itself. - Eddie
  • The existing implementations of comparable work around that already (IIRC by placing NaN below -Infinity). That would be an argument against Double or Float implementing Comparable too, but they do. - Pete Kirkham
  • Yeah it's a bit crazy to not allow comparing of every number ever just because of NaN. How cool would it be to be able to compare every Number, and how weird is it to people who use other languages that this is even an issue? :) - Robert Grant

3

You can use Transmorph to compare numbers using its NumberComparator class.

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);


3

To try to solve the original problem (sort a list of numbers), an option is to declare the list of a generic type extending Number and implementing Comparable.

Something like:

<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));
}


1

there is no stardard comparison for Numbers of different types. However you can write your own Comparator and use it to create a TreeMap<Number, Object>, TreeSet<Number> or Collections.sort(List<Number>, Comparator) or Arrays.sort(Number[], Comparator);


1

Write your own Comparator

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() + ", ");
 }
    }
}


  • This will not work correctly with some floating point numbers. Internal representation of floats may cause two floats A and B to be unequal by a very small margin when they should be equal. - christopheml

1

why this would have been bad idea? :

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
}

another option may have been to declare class Number implements Comparable, omit compareTo implementation, and implement it in some classes like Integer while throw UnsupportedException in others like AtomicInteger.


0

My guess is that by not implementing Comparable, it give more flexibility to implementing classes to implement it or not. All the common numbers (Integer, Long, Double, etc) do implement Comparable. You can still call Collections.sort as long as the elements themselves implement Comparable.


0

Looking at the class hierarchy. Wrapper classes like Long, Integer, etc, implement Comparable, i.e. an Integer is comparable to an integer, and a long is comparable to a long, but you can't mix them. At least with this generics paradigm. Which I guess answers your question 'why'.


0

byte (primitive) is a int (primitive). Primitives have only one value at a time.
Language design rules allows this.

int i = 255

// down cast primitive
(byte) i == -1

A Byte is not an Integer. Byte is a Number and an Integer is a Number. Number objects can have more than one value at the same time.

Integer iObject = new Integer(255);
System.out.println(iObject.intValue());   // 255
System.out.println(iObject.byteValue());  // -1

If a Byte is an Integer and an Integer is a Number, Which one value will you use in the compareTo(Number number1, Number number2) method?

Linked


Related

Latest