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接口的最佳實踐,可以避免這個錯誤。
Java的Comparable接口的設計和使用的最佳實踐:
- 了解你需要創建的領域對象,如果對象沒有明確的排序規則,請不要實現Comparable接口。
- 更多的使用Comparator而不是Comparable,Comparator在更多的業務使用方式時要顯得更為實用。
- 如果你需要創建依賴Comparable接口的接口或者庫,如果可能的話你提供自己的Comparator實現,否則就寫一個良好的文檔指明在你的接口實現類中如何實現。
- 遵守良好的編碼習慣和做法。Effective Java是很好的推薦。
原文鏈接:http://my.oschina.net/jack230230/blog/56339
【編輯推薦】