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是入门的好书。
翻译自: https://www.javacodegeeks.com/2013/08/pitfalls-of-java-comparable-interface.html