Cleaner Responsibilities – Get rid of equals, compareTo and toString
Have you ever looked at the javadoc of the Object-class in Java? Probably. You tend to end up there every now and then when digging your way down the inheritance tree. One thing you might have noticed is that it has quite a few methods that every class must inherit. The favorite methods to implement yourself rather than stick with the original ones are probably .toString(), .equals() and .hashCode() (why you should always implement both of the latter is described well by Per-Åke Minborg in this post).
But these methods are apparently not enough. Many people mix in additional interfaces from the standard libraries like Comparable and Serializable. But is that really wise? Why do everyone want to implement these methods on their own so badly? Well, implementing your own .equals() and .hashCode() methods will probably make sense if you are planning on storing them in something like a HashMap and want to control hash collisions, but what about compareTo() and toString()?
In this article I will present an approach to software design that we use on the Speedment open source project where methods that operate on objects are implemented as functional references stored in variables rather than overriding Javas built in methods. There are several advantages to this. Your POJOs will be shorter and more concise, common operations can be reused without inheritance and you can switch between different configurations in a flexible matter.
Original Code
Let us begin by looking at the following example. We have a typical Java class named Person. In our application we want to print out every person from a Set in the order of their firstname followed by lastname (in case two persons share the same firstname).
Person.java
public class Person implements Comparable<Person> { private final String firstname; private final String lastname; public Person(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname; } public String getFirstname() { return firstname; } public String getLastname() { return lastname; } @Override public int hashCode() { int hash = 7; hash = 83 * hash + Objects.hashCode(this.firstname); hash = 83 * hash + Objects.hashCode(this.lastname); return hash; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final Person other = (Person) obj; if (!Objects.equals(this.firstname, other.firstname)) { return false; } return Objects.equals(this.lastname, other.lastname); } @Override public int compareTo(Person that) { if (this == that) return 0; else if (that == null) return 1; int comparison = this.firstname.compareTo(that.firstname); if (comparison != 0) return comparison; comparison = this.lastname.compareTo(that.lastname); return comparison; } @Override public String toString() { return firstname + " " + lastname; } }
Main.java
public class Main { public static void main(String... args) { final Set people = new HashSet<>(); people.add(new Person("Adam", "Johnsson")); people.add(new Person("Adam", "Samuelsson")); people.add(new Person("Ben", "Carlsson")); people.add(new Person("Ben", "Carlsson")); people.add(new Person("Cecilia", "Adams")); people.stream() .sorted() .forEachOrdered(System.out::println); } }
Output
run: Adam Johnsson Adam Samuelsson Ben Carlsson Cecilia Adams BUILD SUCCESSFUL (total time: 0 seconds)
Person implements several methods here to control the output of the stream. The hashCode() and equals() method make sure that duplicate persons can’t be added to the set. The compareTo() method is used by the sorted action to produce the desired order. The overridden toString()-method is finally controlling how each Person should be printed when System.out.println() is called. Do you recognize this structure? You can find it in almost every java project out there.
Alternative Code
Instead of putting all functionality into the Person class, we can try and keep it as clean as possible and use functional references to handle these decorations. We remove all the boilerplate with equals, hashCode, compareTo and toString and instead we introduce two static variables, COMPARATOR and TO_STRING.
Person.java
public class Person { private final String firstname; private final String lastname; public Person(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname; } public String getFirstname() { return firstname; } public String getLastname() { return lastname; } public final static Comparator<Person> COMPARATOR = Comparator.comparing(Person::getFirstname) .thenComparing(Person::getLastname); public final static Function<Person, String> TO_STRING = p -> p.getFirstname() + " " + p.getLastname(); }
Main.java
public class Main { public static void main(String... args) { final Set people = new TreeSet<>(Person.COMPARATOR); people.add(new Person("Adam", "Johnsson")); people.add(new Person("Adam", "Samuelsson")); people.add(new Person("Ben", "Carlsson")); people.add(new Person("Ben", "Carlsson")); people.add(new Person("Cecilia", "Adams")); people.stream() .map(Person.TO_STRING) .forEachOrdered(System.out::println); } }
Output
run: Adam Johnsson Adam Samuelsson Ben Carlsson Cecilia Adams BUILD SUCCESSFUL (total time: 0 seconds)
The nice thing with this approach is that we can now replace the order and the formatting of the print without changing our Person class. This will make the code more maintainable and easier to reuse, not to say faster to write.
Reference: | Cleaner Responsibilities – Get rid of equals, compareTo and toString from our JCG partner Emil Forslund at the Age of Java blog. |
The idea of a strategy pattern (which is the 20+ year old name for the technique you’re describing in object oriented circles) is a most excellent one. It should probably be used instead of inheritance by all but the best developers (if you use it unwisely, the costs are pretty minimal, where using inheritance unwisely can be an annoyingly expensive mistake to fix, plus the inappropriateness of inheritance often fails to show up ’till quite a bit later). However, since you point out the need for equals and hashcode for types that might be used as keys in hash structures,… Read more »
Or… you just can use Lombok: https://projectlombok.org/
I do agree with you just a little bit (but minimally so), let me explain: I agree with you that hardcoding a class to implement Comparable is old school, as you clearly show in your code. With the advent of Java 8 an implementation of a comparator is better done with a Java 8 function. This goes for the toString as well. Where I disagree with you, and you are disagreeing with yourself as well, is that the code is a static part of Person. You yourself write “The nice thing with this approach is that we can now replace… Read more »
How come you left out the default toString() implementation in the second attempt?