Pitfalls of Java Comparable interface

Java Comparable interface provides a way to do natural ordering for classes implementing the interface. Natural ordering makes sense for scalars and other quite simple objects, but when we come to more business oriented domain objects the natural ordering becomes much more complicated. A transaction object’s natural ordering from business manager’s point of view could be the value of the transaction, but from the system admin’s point of view the natural ordering could be the speed of the transaction. In most cases, there is no clear natural ordering for business domain objects.

Let’s assume that we have found a good natural ordering for a class like Company. We will use the company’s official name as the primary order field and the company id as the secondary. The Company class’ implementation could be as follows.

public class Company implements Comparable<Company> {
 
    private final String id;
    private final String officialName;
 
    public Company(final String id, final String officialName) {
        this.id = id;
        this.officialName = officialName;
    }
 
    public String getId() {
        return id;
    }
 
    public String getOfficialName() {
        return officialName;
    }
 
    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder(17, 29);
        builder.append(this.getId());
        builder.append(this.getOfficialName());
        return builder.toHashCode();
    }
 
    @Override
    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof Company)) {
            return false;
        }
        Company other = (Company) obj;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(this.getId(), other.getId());
        builder.append(this.getOfficialName(), other.getOfficialName());
        return builder.isEquals();
    }
 
    @Override
    public int compareTo(final Company obj) {
        CompareToBuilder builder = new CompareToBuilder();
        builder.append(this.getOfficialName(), obj.getOfficialName());
        builder.append(this.getId(), obj.getId());
        return builder.toComparison();
    }
}

The implementation looks fine and works properly. The Company class isn’t enough for some use cases so we extend it to a CompanyDetails class that provides more information about the company. Instances of these classes could be used for example in a data table showing details of companies.

public class CompanyDetails extends Company {
 
    private final String marketingName;
    private final Double marketValue;
 
    public CompanyDetails(final String id, final String officialName, final String marketingName, final Double marketValue) {
        super(id, officialName);
        this.marketingName = marketingName;
        this.marketValue = marketValue;
    }
 
    public String getMarketingName() {
        return marketingName;
    }
 
    public Double getMarketValue() {
        return marketValue;
    }
 
    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder(19, 31);
        builder.appendSuper(super.hashCode());
        builder.append(this.getMarketingName());
        return builder.toHashCode();
    }
 
    @Override
    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof CompanyDetails)) {
            return false;
        }
        CompanyDetails other = (CompanyDetails) obj;
        EqualsBuilder builder = new EqualsBuilder();
        builder.appendSuper(super.equals(obj));
        builder.append(this.getMarketingName(), other.getMarketingName());
        builder.append(this.getMarketValue(), other.getMarketValue());
        return builder.isEquals();
    }
}

Again the implementation looks fine at first glance, but actually it isn’t. We can create a small test case to indicate the issues of the implementation. The problem arises when we don’t know what the actual interface is doing with our class and we don’t pay enough attention to all details of the super class we are extending.

CompanyDetails c1 = new CompanyDetails("231412", "McDonalds Ltd", "McDonalds food factory", 120000.00);
CompanyDetails c2 = new CompanyDetails("231412", "McDonalds Ltd", "McDonalds restaurants", 60000.00);
 
Set<CompanyDetails> set1 = CompaniesFactory.createCompanies1();
set1.add(c1);
set1.add(c2);
 
Set<CompanyDetails> set2 = CompaniesFactory.createCompanies2();
set2.add(c1);
set2.add(c2);
 
Assert.assertEquals(set1.size(), set2.size());

We use two sets, but realize that they behave differently. Why is that? The other set is a HashSet that relies on the object’s hashCode() and equals() methods, but the other is TreeSet and relies only on the Comparable interface, which we didn’t implement for the subclass. This is quite common mistake when domain objects are extended, but more than that, this is about bad coding conventions. We used Apache Commons‘ builders to implement hashCode()equals() and compareTo() methods. The builders supply appendSuper() method that indicates that it should be used for the super class’ implementation for the method. If you have read the great book Effective Java by Joshua Bloch, you’d realize that this is not right. If we add fields in subclass, we cannot implement equals() or compareTo() methods properly without violating the symmetry rule. We should have used composition over inheritance. If we had used composition to create the CompanyDetails, there would have been no issue for the Comparable interface, because we don’t implement it automatically and allow misbehavior by default. And also we could satisfy the requirements of equals() and hashCode() properly.

The issues mentioned in this post are quite common but usually overlooked. The problems with Comparable interface actually arises from bad conventions and not understanding the requirements of the used interfaces. As a Java developer or architect, you should pay attention to things like this and obey good coding conventions and practices. The bigger the project, the more important it is to avoid errors created by the human factor. I tried to sum up a good best practices list for Comparable interface so the errors could be avoided.

Best practices for Java Comparable interface design and usage:

  • Understand the domain object you are creating and if there is no clear natural ordering for the object, do not implement Comparable interface.
  • Prefer Comparator implementations over Comparable. Comparator can be used more business oriented way depending on the use case.
  • If you need to create interfaces or libraries that rely on comparing objects, provide your own Comparator implementation if possible, otherwise create good documentation how the Comparator should be implemented for your interface.
  • Obey good coding conventions and practices. Effective Java is great book to start with.

 

Reference: Pitfalls of Java Comparable interface from our JCG partner Tapio Rautonen at the RAINBOW WORLDS blog.
Related Whitepaper:

Bulletproof Java Code: A Practical Strategy for Developing Functional, Reliable, and Secure Java Code

Use Java? If you do, you know that Java software can be used to drive application logic of Web services or Web applications. Perhaps you use it for desktop applications? Or, embedded devices? Whatever your use of Java code, functional errors are the enemy!

To combat this enemy, your team might already perform functional testing. Even so, you're taking significant risks if you have not yet implemented a comprehensive team-wide quality management strategy. Such a strategy alleviates reliability, security, and performance problems to ensure that your code is free of functionality errors.Read this article to learn about this simple four-step strategy that is proven to make Java code more reliable, more secure, and easier to maintain.

Get it Now!  

Leave a Reply


seven − = 3



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.

Sign up for our Newsletter

20,709 insiders are already enjoying weekly updates and complimentary whitepapers! Join them now to gain exclusive access to the latest news in the Java world, as well as insights about Android, Scala, Groovy and other related technologies.

As an extra bonus, by joining you will get our brand new e-books, published by Java Code Geeks and their JCG partners for your reading pleasure! Enter your info and stay on top of things,

  • Fresh trends
  • Cases and examples
  • Research and insights
  • Two complimentary e-books