Java的Comparable接口的一个陷阱

转载自:http://my.oschina.net/jack230230/blog/56339


Java的Comparable接口提供一个对实现了这个接口的对象列表进行排序的办法。原始的排序对于简单的对象来说具有意义,但是当我们面对复杂的面向对象的业务逻辑对象时,事情变得复杂的多。从业务经理的角度来看,一些交易对象的自然顺序可能是按照交易的价值来排序的,但是从系统管理员的角度来看,这个排序的规则可能是交易的速度。所以在大多数情况下,并没有明确的业务领域对象的自然排序规则。
    假设我们找到了一个需要排序的类,比如说Campany。我们把公司的offical name作为主关键字,把id作为次要关键字。这个类的实现如下:
01 public class Company implements Comparable<Company> {
02   
03     private final String id;
04     private final String officialName;
05   
06     public Company(final String id, final String officialName) {
07         this.id = id;
08         this.officialName = officialName;
09     }
10   
11     public String getId() {
12         return id;
13     }
14   
15     public String getOfficialName() {
16         return officialName;
17     }
18   
19     @Override
20     public int hashCode() {
21         HashCodeBuilder builder = new HashCodeBuilder(1729);
22         builder.append(this.getId());
23         builder.append(this.getOfficialName());
24         return builder.toHashCode();
25     }
26   
27     @Override
28     public boolean equals(final Object obj) {
29         if (obj == this) {
30             return true;
31         }
32         if (!(obj instanceof Company)) {
33             return false;
34         }
35         Company other = (Company) obj;
36         EqualsBuilder builder = new EqualsBuilder();
37         builder.append(this.getId(), other.getId());
38         builder.append(this.getOfficialName(), other.getOfficialName());
39         return builder.isEquals();
40     }
41   
42     @Override
43     public int compareTo(final Company obj) {
44         CompareToBuilder builder = new CompareToBuilder();
45         builder.append(this.getOfficialName(), obj.getOfficialName());
46         builder.append(this.getId(), obj.getId());
47         return builder.toComparison();
48     }
49 }


这个实现看起来没问题,假设现在这个类提供的信息不够使用,我们又创建了这个类的一个子类CompanyDetail类用以扩展他。例如我们想以一个表的形式显示公司的信息,我们就可以用这个类。
01 public class CompanyDetails extends Company {
02   
03     private final String marketingName;
04     private final Double marketValue;
05   
06     public CompanyDetails(final String id, final String officialName, final String marketingName, final Double marketValue) {
07         super(id, officialName);
08         this.marketingName = marketingName;
09         this.marketValue = marketValue;
10     }
11   
12     public String getMarketingName() {
13         return marketingName;
14     }
15   
16     public Double getMarketValue() {
17         return marketValue;
18     }
19   
20     @Override
21     public int hashCode() {
22         HashCodeBuilder builder = new HashCodeBuilder(1931);
23         builder.appendSuper(super.hashCode());
24         builder.append(this.getMarketingName());
25         return builder.toHashCode();
26     }
27   
28     @Override
29     public boolean equals(final Object obj) {
30         if (obj == this) {
31             return true;
32         }
33         if (!(obj instanceof CompanyDetails)) {
34             return false;
35         }
36         CompanyDetails other = (CompanyDetails) obj;
37         EqualsBuilder builder = new EqualsBuilder();
38         builder.appendSuper(super.equals(obj));
39         builder.append(this.getMarketingName(), other.getMarketingName());
40         builder.append(this.getMarketValue(), other.getMarketValue());
41         return builder.isEquals();
42     }
43 }
这个类的实现看起来还是没什么问题,但是事实上是有问题的,我们可以写一个test指出问题在哪里。当我们没有对父类的所有细节加以注意时,问题就来了。
01 CompanyDetails c1 = new CompanyDetails("231412""McDonalds Ltd""McDonalds food factory"120000.00);
02 CompanyDetails c2 = new CompanyDetails("231412""McDonalds Ltd""McDonalds restaurants"60000.00);
03   
04 Set<CompanyDetails> set1 = CompaniesFactory.createCompanies1();
05 set1.add(c1);
06 set1.add(c2);
07   
08 Set<CompanyDetails> set2 = CompaniesFactory.createCompanies2();
09 set2.add(c1);
10 set2.add(c2);
11   
12 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接口的最佳实践,可以避免这个错误。

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

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

posted @ 2014-02-20 16:00  奋斗中的毛毛虫  Views(389)  Comments(0Edit  收藏  举报