Search code examples
javavisitor-patterncomparetodouble-dispatch

How to use a Double Dispatch with compareTo in JAVA?


A beginner of Java... I'm a bit confused by how to apply double dispatch (and visitor pattern) together with the compareTo method in JAVA.

Example Scenario:
Let's say I have an Animal interface where I will implement 4 classes: dog, cat, mouse, and bunny.
I want to make sure that if I sort a list of these animals, mouse < bunny < cat < dog.
Wondering how I shall achieve it?
Right now the best I can think of is adding some if-elseif into the compareTo... but this is obviously not dispatchy... Thanks!

The Animal interface

package animals;

public interface Animal extends Comparable<Animal> {
  void makeSound();
}

The Dog class

package animals;

public class Dog implements Animal {
  @Override public void makeSound() {
    System.out.println("wow");
  }

  @Override public int compareTo(Animal o) {
    return 0; // ???? if (o instanceof Cat) {return 1}
  }
}


Solution

  • The Answer by Barskov is correct and wise. Probably the best solution for your scenario is to simply add a final static relativeSize field to each of the four classes.

    For fun, let’s look at an alternative approach using an upcoming Java feature.

    Sealed classes

    The new sealed classes feature being previewed in Java 16 makes this simpler.

    When all your allowed concrete types are known at compile-time, you can communicate that fact to the compiler. Mark the class or interface as sealed. Add permits with the names of implementing/extending classes. The compiler then enforces your claim, verifying that all the designated classes, and only the designated classes, extend/implement the sealed class/interface.

    We also use default methods. Modern Java allows an interface to have code, as a default method to be run if the implementing classes lack that method.

    Here is some example code.

    Notice the keywords: sealed, permits, and default.

    package work.basil.example.animalkingdom;
    
    import java.util.List;
    
    public sealed interface Animal                    // 🡄 sealed
            extends Comparable < Animal >
            permits Mouse, Bunny, Cat, Dog            // 🡄 permits
    {
        void makeSound ( );
    
        @Override
        default public int compareTo ( Animal o )     // 🡄 default
        {
            List < Class > animalClasses = List.of( Mouse.class , Bunny.class , Cat.class , Dog.class );
            return
                    Integer.compare
                            (
                                    animalClasses.indexOf( this.getClass() ) ,
                                    animalClasses.indexOf( o.getClass() )
                            )
                    ;
        }
    }
    

    The concrete classes look like the following. We use record here, where the compiler implicitly creates the constructor, getters, equals & hashCode, and toString. You could just as well use a conventional class.

    package work.basil.example.animalkingdom;
    
    final public record Mouse(String name) implements Animal
    {
        @Override
        public void makeSound ( )
        {
            System.out.println( "Squeak." );
        }
    }
    

    See that in action.

    package work.basil.example.animalkingdom;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class App
    {
        public static void main ( String[] args )
        {
            List < Animal > animals =
                    new ArrayList <>(
                            List.of(
                                    new Mouse( "Alice" ) ,
                                    new Bunny( "Bob" ) ,
                                    new Cat( "Carol" ) ,
                                    new Dog( "David" ) ,
                                    new Mouse( "Erin" ) ,
                                    new Bunny( "Frank" ) ,
                                    new Cat( "Grace" ) ,
                                    new Dog( "Heidi" )
                            )
                    );
            System.out.println( "animals = " + animals );
    
            Collections.sort( animals );
            System.out.println( "animals = " + animals );
        }
    }
    

    When run.

    animals = [Mouse[name=Alice], Bunny[name=Bob], Cat[name=Carol], Dog[name=David], Mouse[name=Erin], Bunny[name=Frank], Cat[name=Grace], Dog[name=Heidi]]
    animals = [Mouse[name=Alice], Mouse[name=Erin], Bunny[name=Bob], Bunny[name=Frank], Cat[name=Carol], Cat[name=Grace], Dog[name=David], Dog[name=Heidi]]
    

    Do not use mere class comparison as built-in compareTo

    The above code works, and was fun to try. But in real work you should not do this, not build-in such a compareTo implementation that merely looks at class only.

    Read the Comparable Javadoc. You will see a discussion of how the compareTo method should be consistent with equals.

    You would not consider two Bunny instances to be equal merely because they are both of the same class. You would also compare their content, the state of their contained data. In the case of a record, the default implementation of equals (and hashCode) examines each and every member field. In a conventional class, you would implement something similar, examining an id field or a name field or some such.

    When you do want to compare solely by class, use an external Comparator rather than implementing Comparable internally. Something like this:

    package work.basil.example.animalkingdom;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    
    public class App
    {
        public static void main ( String[] args )
        {
            List < Animal > animals =
                    new ArrayList <>(
                            List.of(
                                    new Mouse( "Alice" ) ,
                                    new Bunny( "Bob" ) ,
                                    new Cat( "Carol" ) ,
                                    new Dog( "David" ) ,
                                    new Mouse( "Erin" ) ,
                                    new Bunny( "Frank" ) ,
                                    new Cat( "Grace" ) ,
                                    new Dog( "Heidi" )
                            )
                    );
            System.out.println( "animals = " + animals );
    
            Collections.sort(
                    animals ,
                    new Comparator < Animal >()
                    {
                        @Override
                        public int compare ( Animal o1 , Animal o2 )
                        {
                            List < Class > animalClasses = List.of( Mouse.class , Bunny.class , Cat.class , Dog.class );
                            return
                                    Integer.compare
                                            (
                                                    animalClasses.indexOf( o1.getClass() ) ,
                                                    animalClasses.indexOf( o2.getClass() )
                                            )
                                    ;
                        }
                    } );
            System.out.println( "animals = " + animals );
        }
    }
    

    Or use the shorter lambda syntax.

            Collections.sort(
                    animals ,
                    ( o1 , o2 ) -> {
                        List < Class > animalClasses = List.of( Mouse.class , Bunny.class , Cat.class , Dog.class );
                        return
                                Integer.compare
                                        (
                                                animalClasses.indexOf( o1.getClass() ) ,
                                                animalClasses.indexOf( o2.getClass() )
                                        )
                                ;
                    } );