javaclassoopcomparable

Why can't I extend an instantiable class with a new value component while preserving the compareTo() contract?


Per Effective Java by Joshua Blotch:

There is no way to extend an instantiable class with a new value component while preserving the compareTo() contract, unless you are willing to forgo the benefits of object-oriented abstraction

Can you, please, explain the above with examples and the challenges? Can you also explain what Joshua means by "Value Component" and what other types of components are available?

This frees you to implement whatever compareTo method you like on the second class, while allowing its client to view an instance of the second class as an instance of the first class when needed.

Can you also explain what Joshua means by "second class as an instance of the first class"?


Solution

  • Can you please explain the above with examples and the challenges?

    Sure. Consider two classes like this - I've left out all the getters, setters etc, but hopefully you get the drift:

    class NamedThing {
        String name;
    }
    
    class Person extends NamedThing {
        Date dateOfBirth;
    }
    

    Ignore whether this is a good example of inheritance - it's a simple one.

    It would be natural for NamedThing to implement a comparison based on name, in alphabetical order.

    It would also be natural for Person to implement a comparison which first compares the name (so stays consistent in that respect) but then also checks for one date of birth being earlier than another.

    Now imagine this:

    NamedThing person1 = new Person("Jon", date1);
    NamedThing person2 = new Person("Jon", date2);
    NamedThing thing = new NamedThing("Jon");
    
    int comparison1 = person1.compareTo(thing);
    int comparison2 = person2.compareTo(thing);
    int comparison3 = person1.compareTo(person2);
    int comparison4 = thing.compareTo(person1);
    

    What would you want all these results to be? If Person.compareTo was smart enough to only apply its date processing to instances of Person, then you might expect comparison1 and comparison2 to be 0, but comparison3 to be non-zero.

    Presumably comparison4 would have to be 0, as it's using NamedThing.compareTo, which only compares names.

    Fundamentally, there's a problem trying to compare instances of different types. It ends up being cleaner to have an external definition of a comparison, which defines what comparison it will use. You could thus have a Comparator<Person> which only accepted Person references and used both name and date, and a Comparator<NamedThing> which only compared by name. The behaviour would have symmetry and clarity.

    Can you also explain what Joshua means by second class as an instance of the first class?

    You've taken it out of context. It's: "view an instance of the second class as an instance of the first class" - e.g.

    // First class = Animal
    // Second class = Zebra
    Animal person = new Zebra();