javacomparablecompareto

Problem with overriding compareTo method in subclass


I am creating subclass and have some class elements inside it. Now I want to override compareTo method from super class and use variables from subclass as argument in new compareTo method but I get error saying that the argument for compareTo method must be the same as in the superclass.

Is there any solution to this problem?

Thanks in advance!

Solutions that I tried are:

1)just simply to write new method in subclass (not to override method from super class) 2) create new compareTo method in superclass with arguments I want

this way program is working but I feel like that is not the correct way to fix this problem

Anyone can suggest something or give advice about this?


Solution

  • I am creating subclass and have some class elements inside it.

    Okay.

    Now I want to override compareTo method from super class and use variables from subclass

    Impossible.

    Comparable is a 'contract interface' - you can't just implement the methods; you aren't "done" when you have implemented every method and they compile and (appear to) run without throwing exceptions: The documentation states additional rules you need to adhere to that the compiler cannot check. The fact that the compiler cannot check that you adhere to these rules means your code compiles. However, your code is still broken. ("It compiles" does not mean "It is correct", after all).

    One of the rules in the contract is that comparison operations are commutative and associative: if a.compareTo(b) returns a negative number, then b.compareTo(a) must return a positive number, (and if one returns 0 so must the other) and vice versa. If you fail to adhere to the contract, crazy stuff happens. For example, you create a TreeSet and things end up out of order and this kind of code can start printing false:

    TreeSet<YourBrokenItem> set = new TreeSet<>();
    set.add(a);
    set.add(b);
    System.out.println(set.contains(a)); // prints false?
    

    However, adhering to the contract (the commutativity) rule is not possible when you subclass and want to override definitions. Let's make it practical:

    class Parent implements Comparable<Parent> {
      int x;
    
      /** Sort on x */
      @Override public int compareTo(Parent other) {
        return Integer.compare(x, other.x);
      }
    }
    
    class Child extends Parent implements Comparable<Child> {
      int y;
    
      /** Sort on x first; if those are equal, sort on y */
      @Override public int compareTo(Child other) {
        int c = Integer.compare(x, other.x);
        if (c != 0) return c;
        return Integer.compare(y, other.y);
      }
    }
    

    The above code doesn't work for two reasons: [A] it can't be made to compile, and [B] even if you voodoo magic that away with generics casting, the code breaks the contract. And always will, you can't fix [B].

    So instead of delving deep on generics and working around it (which you can), it is a moot point - B cannot be worked around, so what you want is impossible, therefore there is no point explaining how to hack around so that the compiler accepts it.

    The reason it's not possible is the commutativity rule. Let's consider 3 instances:

    Parent p = new Parent(10);
    Child c = new Child(10, 5);
    Child d = new Child(10, 20);
    

    Your intent is very clearly that this happens:

    System.out.println(c.compareTo(d)); // prints -1
    System.out.println(d.compareTo(c)); // prints +1
    

    However, what happens when p is involved? Remember, Child says that it is a subtype of Parent which means an instance of Child can do everything an instance of Parent can do, and more. One thing Parent can do, is compare itself to another instance of Parent. And instances of Child are also instances of Parent, so, you can compare a Parent to a Child and vice versa. Hence, p.compareTo(a) is valid. Similarly, you can make a new TreeSet<Parent>() and you can of course invoke .add(new Child()) on that. After all, instances of Child are definitely also instances of Parent, that's what subclassing means.

    Thus:

    p.compareTo(c); // This returns 0 - and you can't stop that from happening!
    p.compareTo(d); // so does this
    
    // thus, given the above, this:
    c.compareTo(d);
    // MUST, BY CONTRACT, return 0!
    

    Hence, what you want? Impossible. The contract of comparable forbids it.

    Effectively, with Comparable, you 'pick a level' and comparability is defined strictly on that level, and subclasses cannot change that. Whatever type in your type hierarchy decides to implements Comparable, that's the level. They set the baseline of the natural comparison for those things and subclasses cannot modify how that works at all without breaking the contract.

    What you probably want is to forget about natural order and use comparators instead. Every system that works on natural order (such as TreeSet, or list.sort(Comparator.naturalOrder()) or Collections.sort(list) or Arrays.binarySearch, and so on), also has an overloaded variant that accepts a Comparator<T>.

    You CAN make a custom comparator that compares, specifically, children. You can even make a custom comparator that compares children and parents if you define the comparison operation properly (meaning, there is a complete order). For example, by saying that given equal x values, any new Parent() instance will always sort before any new Child(), regardless of Child's y value:

    Comparator<Parent> myCustomComparator = (a, b) -> {
      int c = Integer.compare(a.x, b.x);
      if (c != 0) return c; // x isn't equal so it controls.
      if (a instanceof Child && !(b instanceof Child)) return +1;
      if (!(a instanceof Child) && b instanceof Child) return -1;
      if (!(a instanceof Child) && !(b instanceof Child)) return 0;
      int y1 = ((Child) a).y, y2 = ((Child) b).y;
      return Integer.compare(y1, y2);
    };
    
    TreeSet<Parent> myAwesomeSelfSortingSet = new TreeSet<Parent>(myCustomComparator);
    // and voila.