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}
}
}
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.
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]]
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() )
)
;
} );