Статьи

Ответственность за чистоту — избавиться от равных, сравнить и toString

Вы когда-нибудь смотрели на javadoc Object-класса в Java? Вероятно. Вы склонны в конечном итоге оказаться там и сейчас, когда копаете свой путь вниз по дереву наследования. Одна вещь, которую вы, возможно, заметили, состоит в том, что в ней есть несколько методов, которые каждый класс должен наследовать. Вероятно, любимые методы реализации себя, а не использования оригинальных, — это .toString (), .equals () и .hashCode () (почему всегда следует реализовывать оба последних, хорошо описано Per-Åke Minborg в этом посте. ).

Но этих методов явно недостаточно. Многие люди используют дополнительные интерфейсы из стандартных библиотек, таких как Comparable и Serializable . Но действительно ли это мудро? Почему все так сильно хотят реализовать эти методы самостоятельно? Что ж, реализация ваших собственных методов .equals () и .hashCode (), вероятно, будет иметь смысл, если вы планируете хранить их в чем-то вроде HashMap и хотите контролировать коллизии хешей, но как насчет CompareTo () и toString ()?

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

Оригинальный код

Давайте начнем с рассмотрения следующего примера. У нас есть типичный класс Java с именем Person. В нашем приложении мы хотим распечатать каждого человека из набора в порядке его имени, за которым следует фамилия (в случае, если два человека имеют одно и то же имя).

Person.java

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
50
51
52
53
54
55
public class Person implements Comparable<Person> {
     
    private final String firstname;
    private final String lastname;
     
    public Person(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname  = lastname;
    }
 
    public String getFirstname() {
        return firstname;
    }
 
    public String getLastname() {
        return lastname;
    }
     
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 83 * hash + Objects.hashCode(this.firstname);
        hash = 83 * hash + Objects.hashCode(this.lastname);
        return hash;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        final Person other = (Person) obj;
        if (!Objects.equals(this.firstname, other.firstname)) {
            return false;
        }
        return Objects.equals(this.lastname, other.lastname);
    }
 
    @Override
    public int compareTo(Person that) {
        if (this == that) return 0;
        else if (that == null) return 1;
 
        int comparison = this.firstname.compareTo(that.firstname);
        if (comparison != 0) return comparison;
 
        comparison = this.lastname.compareTo(that.lastname);
        return comparison;
    }
 
    @Override
    public String toString() {
        return firstname + " " + lastname;
    }
}

Main.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class Main {
    public static void main(String... args) {
        final Set people = new HashSet<>();
         
        people.add(new Person("Adam", "Johnsson"));
        people.add(new Person("Adam", "Samuelsson"));
        people.add(new Person("Ben", "Carlsson"));
        people.add(new Person("Ben", "Carlsson"));
        people.add(new Person("Cecilia", "Adams"));
         
        people.stream()
            .sorted()
            .forEachOrdered(System.out::println);
    }
}

Выход

1
2
3
4
5
6
run:
Adam Johnsson
Adam Samuelsson
Ben Carlsson
Cecilia Adams
BUILD SUCCESSFUL (total time: 0 seconds)

Человек реализует несколько методов здесь, чтобы управлять выводом потока. Методы hashCode () и equals () обеспечивают невозможность добавления дубликатов в набор. Метод compareTo () используется отсортированным действием для получения желаемого порядка. Переопределенный метод toString (), наконец, контролирует, как должен выводиться каждый объект Person при вызове System.out.println (). Вы узнаете эту структуру? Вы можете найти его почти в каждом Java-проекте.

Альтернативный код

Вместо того, чтобы помещать всю функциональность в класс Person, мы можем постараться сохранить его как можно более чистым и использовать функциональные ссылки для обработки этих украшений. Мы удаляем весь шаблон с помощью equals, hashCode, compareTo и toString и вместо этого вводим две статические переменные, COMPARATOR и TO_STRING.

Person.java

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
public class Person {
     
    private final String firstname;
    private final String lastname;
     
    public Person(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname  = lastname;
    }
 
    public String getFirstname() {
        return firstname;
    }
 
    public String getLastname() {
        return lastname;
    }
     
    public final static Comparator<Person> COMPARATOR =
        Comparator.comparing(Person::getFirstname)
            .thenComparing(Person::getLastname);
     
    public final static Function<Person, String> TO_STRING =
        p -> p.getFirstname() + " " + p.getLastname();
}

Main.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class Main {
    public static void main(String... args) {
        final Set people = new TreeSet<>(Person.COMPARATOR);
         
        people.add(new Person("Adam", "Johnsson"));
        people.add(new Person("Adam", "Samuelsson"));
        people.add(new Person("Ben", "Carlsson"));
        people.add(new Person("Ben", "Carlsson"));
        people.add(new Person("Cecilia", "Adams"));
         
        people.stream()
            .map(Person.TO_STRING)
            .forEachOrdered(System.out::println);
    }
}

Выход

1
2
3
4
5
6
run:
Adam Johnsson
Adam Samuelsson
Ben Carlsson
Cecilia Adams
BUILD SUCCESSFUL (total time: 0 seconds)

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