Java的Comparable接口的一个陷阱

     Java的Comparable接口提供一个对实现了这个接口的对象列表进行排序的办法。原始的排序对于简单的对象来讲具备意义,可是当咱们面对复杂的面向对象的业务逻辑对象时,事情变得复杂的多。从业务经理的角度来看,一些交易对象的天然顺序多是按照交易的价值来排序的,可是从系统管理员的角度来看,这个排序的规则多是交易的速度。因此在大多数状况下,并无明确的业务领域对象的天然排序规则。
    假设咱们找到了一个须要排序的类,好比说Campany。咱们把公司的offical name做为主关键字,把id做为次要关键字。这个类的实现以下:
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();
    }
}


这个实现看起来没问题,假设如今这个类提供的信息不够使用,咱们又建立了这个类的一个子类CompanyDetail类用以扩展他。例如咱们想以一个表的形式显示公司的信息,咱们就能够用这个类。
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();
    }
}
这个类的实现看起来仍是没什么问题,可是事实上是有问题的,咱们能够写一个test指出问题在哪里。当咱们没有对父类的全部细节加以注意时,问题就来了。
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());
    咱们构造了两个set,可是结果是assert的结果是不相等。这是为何?其中一个set是一个HashSet,他依赖对象的hashCode()和equals()方法,可是另外一个是TreeSet,他只是依赖Comparable接口,而这个接口在子类中咱们并无实现。在领域对象被扩展的时候这是很常见的一个错误,可是更重要的是这是很差的编码约定形成的。咱们使用Apache Commons包中的builder来实现hashCode(),equals().和compareTo()方法。这些builder提供了appendSuper()方法,此方法指示了如何调用这些方法在父类中的实现。若是你看过Joshua Bloch 的Effective Java,你会发现这是错误的。若是咱们在子类中添加成员变量,在不违反对称规则的状况下,咱们就不能正确的实现equals()方法和compareTo()方法。咱们应该使用组合的方式而不是继承。若是咱们使用组合的方式构造CompanyDetails,对于Comparable接口来讲没有任何问题,由于咱们没有自动的实现,并且在默认的状况容许不一样的行为。并且咱们也能知足正确的equals()和hashCode()的需求。

    这篇文章提到的问题很是广泛,可是常常被忽视。Comparable接口的问题实际是因为很差的约定和对使用的接口需求的错误理解形成的。做为一个Java开发人员或架构师,你应该特别注意这样的事情,并遵照良好的编码习惯和作法。 越大的项目,这种问题就越显得重要。这里我总结了一个使用Comparable接口的最佳实践,能够避免这个错误。html

Java的Comparable接口的设计和使用的最佳实践:java

·了解你须要建立的领域对象,若是对象没有明确的排序规则,请不要实现Comparable接口。
·更多的使用Comparator而不是Comparable,Comparator在更多的业务使用方式时要显得更为实用。
·若是你须要建立依赖Comparable接口的接口或者库,若是可能的话你提供本身的Comparator实现,不然就写一个良好的文档指明在你的接口实现类中如何实现。
·遵照良好的编码习惯和作法。Effective Java是很好的推荐。 

原文连接/ OSChina.NET原创翻译
相关文章
相关标签/搜索