Статьи

Создание объектов значения с неизменяемыми

В ответ на мою недавнюю публикацию AutoValue: сгенерированные классы неизменных значений Брэндон предположил, что было бы интересно посмотреть, как AutoValue сравнивается с Project Lombok и Immutables, и Кевин поддержал это. Я согласен, что это хорошая идея, но я сначала публикую этот пост в качестве краткого обзора Immutables, потому что я уже предоставил аналогичные посты для Lombok и AutoValue .

Immutables 2.2.5 доступен в центральном репозитории Maven, и на странице его лицензии указано: «Набор инструментов Immutables и все необходимые зависимости включены в лицензию на программное обеспечение Apache версии 2.0 ». Начало работы! На странице указано, что «для работы процессора аннотаций Immutables требуется Java 7 или выше».

Immutables, как AutoValue, использует аннотации времени компиляции для генерации исходного кода для классов, которые определяют неизменяемые объекты. Поскольку оба они используют этот подход, оба вводят только зависимости времени компиляции, и их соответствующие JAR-файлы не нужны в пути к классам приложения. Другими словами, неизменяемые JAR-файлы должны находиться в пути к классам компилятора ( javac ), а не в пути к классам запуска Java ( java ).

Листинг кода для класса «шаблон» Person показан в следующем листинге кода ( Person.java ). Это выглядит очень похоже на Person.java я использовал в своей демонстрации AutoValue.

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
package dustin.examples.immutables;
 
import org.immutables.value.Value;
 
/**
 * Represents an individual as part of demonstration of
 * the Immutables project (http://immutables.github.io/).
 */
@Value.Immutable  // concrete extension will be generated by Immutables
abstract class Person
{
   /**
    * Provide Person's last name.
    *
    * @return Last name of person.
    */
   abstract String lastName();
 
   /**
    * Provide Person's first name.
    *
    * @return First name of person.
    */
   abstract String firstName();
 
   /**
    * Provide Person's birth year.
    *
    * @return Person's birth year.
    */
   abstract long birthYear();
}

Единственными различиями в этом классе «template» и классе «template», которые я перечислил в своем посте AutoValue, является имя пакета, комментарии Javadoc о том, какой продукт демонстрируется, и (что наиболее важно) аннотация, импортированная и примененная к класс. В примере AutoValue есть специальный метод «create», которого нет в примере Immutables, но это только потому, что я не продемонстрировал использование компоновщика AutoValue, что сделало бы метод «create» ненужным.

Когда я соответствующим образом указываю использование Immutables в моем пути к классам и использую javac для компиляции вышеуказанного исходного кода, вызывается процессор аннотаций и генерируется следующий исходный код Java:

ImmutablePerson.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
package dustin.examples.immutables;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.annotation.Generated;
 
/**
 * Immutable implementation of {@link Person}.
 * <p>
 * Use the builder to create immutable instances:
 * {@code ImmutablePerson.builder()}.
 */
@SuppressWarnings("all")
@Generated({"Immutables.generator", "Person"})
final class ImmutablePerson extends Person {
  private final String lastName;
  private final String firstName;
  private final long birthYear;
 
  private ImmutablePerson(String lastName, String firstName, long birthYear) {
    this.lastName = lastName;
    this.firstName = firstName;
    this.birthYear = birthYear;
  }
 
  /**
   * @return The value of the {@code lastName} attribute
   */
  @Override
  String lastName() {
    return lastName;
  }
 
  /**
   * @return The value of the {@code firstName} attribute
   */
  @Override
  String firstName() {
    return firstName;
  }
 
  /**
   * @return The value of the {@code birthYear} attribute
   */
  @Override
  long birthYear() {
    return birthYear;
  }
 
  /**
   * Copy the current immutable object by setting a value for the {@link Person#lastName() lastName} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param lastName A new value for lastName
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withLastName(String lastName) {
    if (this.lastName.equals(lastName)) return this;
    String newValue = Objects.requireNonNull(lastName, "lastName");
    return new ImmutablePerson(newValue, this.firstName, this.birthYear);
  }
 
  /**
   * Copy the current immutable object by setting a value for the {@link Person#firstName() firstName} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param firstName A new value for firstName
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withFirstName(String firstName) {
    if (this.firstName.equals(firstName)) return this;
    String newValue = Objects.requireNonNull(firstName, "firstName");
    return new ImmutablePerson(this.lastName, newValue, this.birthYear);
  }
 
  /**
   * Copy the current immutable object by setting a value for the {@link Person#birthYear() birthYear} attribute.
   * A value equality check is used to prevent copying of the same value by returning {@code this}.
   * @param birthYear A new value for birthYear
   * @return A modified copy of the {@code this} object
   */
  public final ImmutablePerson withBirthYear(long birthYear) {
    if (this.birthYear == birthYear) return this;
    return new ImmutablePerson(this.lastName, this.firstName, birthYear);
  }
 
  /**
   * This instance is equal to all instances of {@code ImmutablePerson} that have equal attribute values.
   * @return {@code true} if {@code this} is equal to {@code another} instance
   */
  @Override
  public boolean equals(Object another) {
    if (this == another) return true;
    return another instanceof ImmutablePerson
        && equalTo((ImmutablePerson) another);
  }
 
  private boolean equalTo(ImmutablePerson another) {
    return lastName.equals(another.lastName)
        && firstName.equals(another.firstName)
        && birthYear == another.birthYear;
  }
 
  /**
   * Computes a hash code from attributes: {@code lastName}, {@code firstName}, {@code birthYear}.
   * @return hashCode value
   */
  @Override
  public int hashCode() {
    int h = 31;
    h = h * 17 + lastName.hashCode();
    h = h * 17 + firstName.hashCode();
    h = h * 17 + Long.hashCode(birthYear);
    return h;
  }
 
  /**
   * Prints the immutable value {@code Person} with attribute values.
   * @return A string representation of the value
   */
  @Override
  public String toString() {
    return "Person{"
        + "lastName=" + lastName
        + ", firstName=" + firstName
        + ", birthYear=" + birthYear
        + "}";
  }
 
  /**
   * Creates an immutable copy of a {@link Person} value.
   * Uses accessors to get values to initialize the new immutable instance.
   * If an instance is already immutable, it is returned as is.
   * @param instance The instance to copy
   * @return A copied immutable Person instance
   */
  public static ImmutablePerson copyOf(Person instance) {
    if (instance instanceof ImmutablePerson) {
      return (ImmutablePerson) instance;
    }
    return ImmutablePerson.builder()
        .from(instance)
        .build();
  }
 
  /**
   * Creates a builder for {@link ImmutablePerson ImmutablePerson}.
   * @return A new ImmutablePerson builder
   */
  public static ImmutablePerson.Builder builder() {
    return new ImmutablePerson.Builder();
  }
 
  /**
   * Builds instances of type {@link ImmutablePerson ImmutablePerson}.
   * Initialize attributes and then invoke the {@link #build()} method to create an
   * immutable instance.
   * <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
   * but instead used immediately to create instances.</em>
   */
  static final class Builder {
    private static final long INIT_BIT_LAST_NAME = 0x1L;
    private static final long INIT_BIT_FIRST_NAME = 0x2L;
    private static final long INIT_BIT_BIRTH_YEAR = 0x4L;
    private long initBits = 0x7L;
 
    private String lastName;
    private String firstName;
    private long birthYear;
 
    private Builder() {
    }
 
    /**
     * Fill a builder with attribute values from the provided {@code Person} instance.
     * Regular attribute values will be replaced with those from the given instance.
     * Absent optional values will not replace present values.
     * @param instance The instance from which to copy values
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder from(Person instance) {
      Objects.requireNonNull(instance, "instance");
      lastName(instance.lastName());
      firstName(instance.firstName());
      birthYear(instance.birthYear());
      return this;
    }
 
    /**
     * Initializes the value for the {@link Person#lastName() lastName} attribute.
     * @param lastName The value for lastName
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder lastName(String lastName) {
      this.lastName = Objects.requireNonNull(lastName, "lastName");
      initBits &= ~INIT_BIT_LAST_NAME;
      return this;
    }
 
    /**
     * Initializes the value for the {@link Person#firstName() firstName} attribute.
     * @param firstName The value for firstName
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder firstName(String firstName) {
      this.firstName = Objects.requireNonNull(firstName, "firstName");
      initBits &= ~INIT_BIT_FIRST_NAME;
      return this;
    }
 
    /**
     * Initializes the value for the {@link Person#birthYear() birthYear} attribute.
     * @param birthYear The value for birthYear
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder birthYear(long birthYear) {
      this.birthYear = birthYear;
      initBits &= ~INIT_BIT_BIRTH_YEAR;
      return this;
    }
 
    /**
     * Builds a new {@link ImmutablePerson ImmutablePerson}.
     * @return An immutable instance of Person
     * @throws java.lang.IllegalStateException if any required attributes are missing
     */
    public ImmutablePerson build() {
      if (initBits != 0) {
        throw new IllegalStateException(formatRequiredAttributesMessage());
      }
      return new ImmutablePerson(lastName, firstName, birthYear);
    }
 
    private String formatRequiredAttributesMessage() {
      List<String> attributes = new ArrayList<String>();
      if ((initBits & INIT_BIT_LAST_NAME) != 0) attributes.add("lastName");
      if ((initBits & INIT_BIT_FIRST_NAME) != 0) attributes.add("firstName");
      if ((initBits & INIT_BIT_BIRTH_YEAR) != 0) attributes.add("birthYear");
      return "Cannot build Person, some of required attributes are not set " + attributes;
    }
  }
}

Из наблюдения сгенерированного кода можно сделать несколько наблюдений (и вы обнаружите, что они удивительно похожи на наблюдения, перечисленные для AutoValue в моем предыдущем посте ):

  • Сгенерированный класс расширяет (наследование реализации) абстрактный класс, написанный вручную, что позволяет потребляющему коду использовать API рукописного класса, не зная, что сгенерированный класс используется.
  • Поля были сгенерированы, хотя поля не были определены непосредственно в исходном классе; Immutables интерпретировал поля из предоставленных abstract методов доступа.
  • Сгенерированный класс не предоставляет методы «set» / mutator для полей (методы get / accessor). Это неудивительно, поскольку ключевая концепция объектов-значений заключается в том, что они неизменны, и даже название этого проекта (неизменяемые) подразумевает эту характеристику. Обратите внимание, что Immutables предоставляет некоторые возможности для изменяемых объектов с аннотацией @ Value.Modifiable .
  • Реализации equals (Object) , hashCode () и toString () автоматически генерируются соответствующим образом для каждого поля с учетом его типа.
  • Комментарии Javadoc к исходному классу и методы не воспроизводятся в сгенерированном классе расширения. Вместо этого более простые (и более общие) комментарии Javadoc предоставляются для методов сгенерированного класса, а более важные (но все же общие) комментарии Javadoc предоставляются для методов класса построителя.

Как я сказал в отношении AutoValue, одно из основных преимуществ использования такого подхода, как генерация Immutables, заключается в том, что разработчики могут сосредоточиться на более простых концепциях более высокого уровня, которые должен поддерживать конкретный класс, а генерация кода гарантирует, что детали более низкого уровня реализованы последовательно и правильно. Однако при использовании этого подхода необходимо помнить о некоторых вещах.

  • Immutables , скорее всего, будет полезен, когда разработчики достаточно дисциплинированы, чтобы просматривать и поддерживать абстрактный «исходный» класс Java вместо сгенерированного класса.
    • Изменения в сгенерированных классах будут перезаписаны в следующий раз, когда обработка аннотаций снова сгенерирует класс, или генерация этого класса должна быть остановлена, чтобы этого не произошло.
    • Абстрактный класс «template» содержит документацию и другие высокоуровневые элементы, на которых большинство разработчиков захотят сосредоточиться, а сгенерированный класс просто реализует мельчайшие детали.
  • Вы захотите настроить свою сборку / IDE так, чтобы сгенерированные классы считались «исходным кодом», чтобы абстрактный класс компилировался и компилировались все зависимости от сгенерированных классов.
  • Следует соблюдать особую осторожность при использовании изменяемых полей с Immutables, если кто-то хочет сохранить неизменность (что обычно имеет место при выборе использования Immutables или Value Objects в целом).

Вывод

Мой вывод может быть почти дословным, как и мой пост на AutoValue . Immutables позволяет разработчикам писать более сжатый код, который фокусируется на деталях высокого уровня и делегирует утомительную реализацию низкоуровневых (и часто подверженных ошибкам) ​​деталей для Immutables для автоматической генерации кода. Это похоже на то, что может генерировать исходный код IDE, но преимущество Immutables перед подходом IDE состоит в том, что Immutables может заново генерировать исходный код каждый раз, когда код компилируется, поддерживая сгенерированный код в актуальном состоянии. Это преимущество Immutables также является хорошим примером мощи обработки пользовательских аннотаций Java.