Java Comparable接口的陷阱

Java Comparable接口提供了一种对实现该接口的类进行自然排序的方法。 自然顺序对标量和其他非常简单的对象有意义,但是当我们使用面向业务的领域对象时,自然顺序就变得更加复杂。 从业务经理的角度来看,交易对象的自然顺序可以是交易的价值,但是从系统管理员的角度来看,自然顺序可以是交易的速度。 在大多数情况下,业务域对象没有明确的自然顺序。

假设我们已经为诸如Company这样的类找到了良好的自然排序。 我们将使用公司的正式名称作为主要订单字段,并使用公司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();
    }
}

该实现看起来不错并且可以正常工作。 对于某些用例,Company类是不够的,因此我们将其扩展到CompanyDetails类,该类提供有关公司的更多信息。 例如,可以在显示公司详细信息的数据表中使用这些类的实例。

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

乍一看,该实现看起来还不错,但实际上并非如此。 我们可以创建一个小的测试用例来指示实现问题。 当我们不知道类的实际接口在做什么,并且我们对扩展的超类的所有细节没有给予足够的关注时,就会出现问题。

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());

我们使用两个集合,但要意识到它们的行为有所不同。 这是为什么? 另一个集是HashSet,它依赖于对象的hashCode()equals()方法,而另一个是TreeSet,并且仅依赖于Comparable接口,而我们并未为子类实现该接口。 扩展域对象时,这是一个很常见的错误,但更重要的是,这与错误的编码约定有关。 我们使用Apache Commons的构建器来实现hashCode()equals()compareTo()方法。 建设者提供了appendSuper()方法,该方法指示应将其用于该方法的超类的实现。 如果您读过Joshua Bloch撰写的精彩著作《 Effective Java》 ,您将意识到这是不对的。 如果在子类中添加字段,则在不违反对称规则的情况下,我们无法正确实现equals()compareTo()方法。 我们应该使用组合而不是继承。 如果我们使用组合来创建CompanyDetails,那么Comparable接口就不会有问题,因为我们不会自动实现它,并且默认情况下允许行为异常。 而且我们也可以适当地满足equals()hashCode()的要求。

这篇文章中提到的问题很普遍,但通常被忽略。 可比接口的问题实际上是由不正确的约定引起的,并且不了解所用接口的要求。 作为Java开发人员或架构师,您应该注意诸如此类的事情,并遵守良好的编码约定和实践。 项目越大,避免人为因素造成的错误就越重要。 我试图为可比接口总结一个良好的最佳实践列表,以便可以避免错误。

Java可比接口设计和用法的最佳实践:

  • 了解您正在创建的域对象,如果该对象没有明确的自然顺序,则不要实现Comparable接口。
  • 比Comparable更喜欢Comparator实现。 比较器可以根据用例以更面向业务的方式使用。
  • 如果您需要创建依赖于比较对象的接口或库,请尽可能提供自己的Comparator实现,否则创建良好的文档,说明如何为您的接口实现Comparator。
  • 遵守良好的编码约定和惯例。 有效的Java是入门的好书。

参考:来自RAINBOW WORLDS博客的JCG合作伙伴 Tapio Rautonen 的Java Comparable接口的陷阱

翻译自: https://www.javacodegeeks.com/2013/08/pitfalls-of-java-comparable-interface.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值