Статьи

Подводные камни интерфейса Java Comparable

Java Comparable интерфейс предоставляет способ естественного упорядочения классов, реализующих интерфейс. Естественное упорядочение имеет смысл для скаляров и других довольно простых объектов, но когда мы подходим к более ориентированным на бизнес объектам предметной области, естественное упорядочение становится намного более сложным. Естественное упорядочение объекта транзакции с точки зрения бизнес-менеджера может быть значением транзакции, но с точки зрения системного администратора естественное упорядочение может быть скоростью транзакции. В большинстве случаев нет четкого естественного порядка для объектов бизнес-домена.

Давайте предположим, что мы нашли хороший естественный порядок для такого класса, как Company. Мы будем использовать официальное название компании в качестве основного поля заказа и идентификатор компании в качестве дополнительного. Реализация корпоративного класса может быть следующей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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, который предоставляет больше информации о компании. Экземпляры этих классов могут быть использованы, например, в таблице данных, показывающей детали компаний.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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();
    }
}

Снова реализация выглядит хорошо на первый взгляд, но на самом деле это не так. Мы можем создать небольшой контрольный пример, чтобы указать на проблемы реализации. Проблема возникает, когда мы не знаем, что делает настоящий интерфейс с нашим классом, и мы не уделяем достаточного внимания всем деталям суперкласса, который мы расширяем.

01
02
03
04
05
06
07
08
09
10
11
12
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 (), который указывает, что его следует использовать для реализации метода суперкласса. Если бы вы прочитали замечательную книгу Джошуа Блоха « Эффективная Java », вы бы поняли, что это неправильно. Если мы добавим поля в подкласс, мы не сможем правильно реализовать методы equals () или compareTo () без нарушения правила симметрии. Мы должны были использовать композицию по наследству. Если бы мы использовали композицию для создания CompanyDetails, не было бы проблемы для интерфейса Comparable, потому что мы не реализуем его автоматически и допускаем неправильное поведение по умолчанию. А также мы могли бы удовлетворить требования equals () и hashCode () должным образом.

Проблемы, упомянутые в этом посте, довольно распространены, но обычно упускаются из виду. Проблемы с интерфейсом Comparable на самом деле возникают из-за неправильных соглашений и непонимания требований используемых интерфейсов. Как Java-разработчик или архитектор, вы должны обращать внимание на подобные вещи и соблюдать хорошие правила и нормы кодирования. Чем больше проект, тем важнее избежать ошибок, созданных человеческим фактором. Я попытался подытожить список лучших рекомендаций для интерфейса Comparable, чтобы избежать ошибок.

Лучшие практики для разработки и использования Java Comparable интерфейса:

  • Изучите объект домена, который вы создаете, и если нет четкого естественного порядка для объекта, не реализуйте интерфейс Comparable.
  • Предпочитают реализации Comparator по сравнению с Comparable. Компаратор может использоваться более ориентированным на бизнес способом в зависимости от варианта использования.
  • Если вам нужно создать интерфейсы или библиотеки, которые полагаются на сравнение объектов, по возможности предоставьте собственную реализацию Comparator, иначе создайте хорошую документацию о том, как Comparator должен быть реализован для вашего интерфейса.
  • Соблюдайте правила и практику кодирования. Эффективная Java — отличная книга для начала.

Ссылка: Подводные камни интерфейса Java Comparable от нашего партнера по JCG Тапио Раутонена в блоге RAINBOW WORLDS .