2

In Java, what is the best approach to provide natural ordering for all implementations of an interface?

I have an interface, for which I want to ensure/provide natural ordering between all implementations by extending the Comparable interface:

public interface MyInterface extends Comparable<MyInterface> {

}

There will be several implementations of this interface, each of which can define a natural ordering for its own instances, but which may not know how to order itself against other implementations.

One approach, which I have used, is to introduce recursive generics and split the natural order comparison by implementation and by instance:

public interface MyInterface<X extends MyInterface<X>> extends Comparable<MyInterface> {

  @Override
  default int compareTo(MyInterface o) {
    // the interface defines how to compare between implementations, say...
    int comp = this.getClass().getSimpleName().compareTo(o.getClass().getSimpleName());
    if (comp == 0) {
      // but delegates to compare between instances of the same implementation
      comp = compare((X) o);
    }
    return comp;
  }

  int compare(X other);
}

This means that the implementations of MyInterface only have to compare between their own instances:

public class MyClass implements MyInterface<MyClass> {

  public int compare(MyClass other) {
    return 0; // ... or something more useful... 
  }
}

But, recursive generics can become very difficult to maintain.

Is there a better way?

amaidment
  • 6,942
  • 5
  • 52
  • 88
  • 2
    What does your custom `compare()` do differently than `compareTo()` from `Comparable`? – Felk Apr 18 '19 at 15:56
  • @Felk - it means that implementations of the interface only need to compare within their own instances, which has already been checked / cast to the right type. I've added an example to the question. – amaidment Apr 18 '19 at 16:03
  • 1
    Although, my question isn't to really an invitation to pick holes in this solution, but to ask for a better solution. – amaidment Apr 18 '19 at 16:04
  • it looks like your interface right now does the same as simply doing `MyClass implements Comparable` (signature-wise, the comparison implementation differs) – Felk Apr 18 '19 at 16:05
  • What does it mean to "compare between implementations"? How would an Integer be comparable to a String? What you are doing now is comparing class names which seems to be an arbitrary ordering. You seem to be trying to put different implementations of your interface into a the same list and sort that list but different implementations are not comparable to each other -- so you compare class names. Anyway, I don't think there is anything better than what you are doing. Perhaps you should ask about whatever trouble you are having with "recursive generics". – K.Nicholas Apr 18 '19 at 16:38
  • @Felk - no, MyClass implements Comparable, so I can compare all instances of all implementations of MyInterface – amaidment Apr 19 '19 at 11:22
  • @K.Nicholas - I’m not trying to compare Strings and Integers, but all instances of all implementations of MyInterface. Comparing on the simple class name is just a SSCCE - there are other ways of doing this, with an enum Type or something else, but that’s orthogonal to this problem. Is it arbitrary - yes... but it’s up to me to define what is the ‘natural’ ordering of my classes! Is there a better way... you don’t think so - thanks, that’s a genuinely helpful perspective. Do I want to ask about recursive genetics - no, I’m quite happy with the question I have asked. – amaidment Apr 19 '19 at 11:29
  • The X type in your generics could be anything, an Integer or a String. All the "Integer" entries will come first, ordered, then all the "String" entries because I comes before S. I don't care if you want to do something like this but you say you want a better solution because "recursive generics can become very difficult to maintain." This post is simply a statement of what you have done with this final statement. "Is there a better way" is overbroad. No real question here. – K.Nicholas Apr 19 '19 at 15:46
  • @K.Nicolas - no, X cannot be anything. As the codes clearly shows, X must implement MyInterface, recursively. Nevertheless, thank you for your thoughts on this matter. – amaidment Apr 20 '19 at 20:05
  • Is it possible to assign a hierarchical sort key to all implementations of your interface, to be used for comparisons? So for example `Impl1` with an id of `X` could return `Impl1:X`, while `Impl2` would return `Impl2:X`, so the "natural order" would be the order of those keys. If you can't define such a sort key, I'd probably question the sense of having everything comparable to everything else. – biziclop Apr 25 '19 at 13:14

2 Answers2

2

You could move this casting compare((X) o); from interface's default method to the implementations and therefore you don't need generic <X extends MyInterface<X>> at all.

public interface MyInterface extends Comparable<MyInterface> {
    @Override
    default int compareTo(MyInterface o) {
        ...
        comp = compare(o);
        ...
    }
    int compare(MyInterface other);
}

In this case implementations could look like:

public class MyClass implements MyInterface {
    private Integer property;
    public int compare(MyInterface other) {
        return Integer.compare(this.property, ((MyClass) other).property);
    }
}
Ruslan
  • 6,090
  • 1
  • 21
  • 36
  • That’s not a bad idea - it circumvents the need for recursive generics. However, my concern is that we’re then exposing a public method on the interface, such that I could pass an instance of one implementation to the compare method of another implementation. Rather than getting a compile time error, I would get a ClassCastException at runtime, which isn’t as good. So, it’s a bit of a trade off... – amaidment Apr 19 '19 at 11:20
1

So, this is the best I have come up with so far, which kind of blends my original approach with Ruslan's answer and tries to manage the trade offs:

We define the interface without recursive generics:

public interface MyInterface extends Comparable<MyInterface> {

  @Override // as per the Comparable interface (not needed, but included here for clarity)
  int compareTo(MyInterface o);
}

Then we create an abstract class that defines the comparison between implementations, and delegates to the implementations to compare between instances of that implementation. This improves on having this functionality in the interface, as we limit the scope of the compare() method to protected.

public abstract class MyAbstractClass implements MyInterface {

  @Override
  public int compareTo(MyInterface o) {
    // the interface defines how to compare between implementations, say...
    int comp = this.getClass().getSimpleName().compareTo(o.getClass().getSimpleName());
    if (comp == 0) {
      // but delegates to compare between instances of the same implementation
      comp = compare(o);
    }
    return comp;
  }

  protected abstract int compare(MyInterface other);
}

Then in each implementation, we check/cast to that implementation. This should never be called with an implementation other than itself, but just to be safe, we throw an IllegalArgumentException if that were to happen.

public class MyClass implements MyInterface {

  public int compare(MyClass o) {
    if (o instanceof MyClass) {
      return 0; // ... or something more useful... 
    } else {
      throw new IllegalArgumentException("Cannot compare " + this.getClass() + " with " + o.getClass());
    }
  }
}
amaidment
  • 6,942
  • 5
  • 52
  • 88