Рубрики
Без рубрики

Введение в автоматическое значение

Auto Value – это генератор исходного кода для объектов value; проще говоря, он автоматически генерирует объекты типа value с реализациями toString(), equals() и hashCode()

Автор оригинала: baeldung.

1. Обзор

Auto Value -это генератор исходного кода для Java, и, более конкретно, это библиотека для генерации исходного кода для объектов значений или объектов типа значений .

Для создания объекта типа значения все, что вам нужно сделать, это аннотировать абстрактный класс с помощью @AutoValue аннотации и скомпилировать свой класс. Генерируется объект value с методами доступа, параметризованным конструктором, правильно переопределенным toString(), equals(Object) и hashCode() методами.

Следующий фрагмент кода является кратким примером абстрактного класса, который при компиляции приведет к объекту значения с именем AutoValue_Person .

@AutoValue
abstract class Person {
    static Person create(String name, int age) {
        return new AutoValue_Person(name, age);
    }

    abstract String name();
    abstract int age();
}

Давайте продолжим и узнаем больше об объектах value, зачем они нам нужны и как AutoValue может помочь сделать задачу генерации и рефакторинга кода намного менее трудоемкой.

2. Настройка Maven

Чтобы использовать AutoValue в проектах Maven, необходимо включить следующую зависимость в pom.xml :


    com.google.auto.value
    auto-value
    1.2

Последнюю версию можно найти, перейдя по этой ссылке .

3. Объекты, типизированные по значениям

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

3.1. Что Такое Типы Значений?

Объекты типа значений-это объекты, равенство которых друг другу определяется не идентичностью, а скорее их внутренним состоянием. Это означает, что два экземпляра объекта с типизированным значением считаются равными, если они имеют одинаковые значения полей.

Как правило, типы значений неизменяемы . Их поля должны быть сделаны окончательными и у них не должно быть методов setter , так как это сделает их изменяемыми после создания экземпляра.

Они должны использовать все значения полей через конструктор или фабричный метод.

Типы значений не являются JavaBeans, потому что у них нет конструктора аргументов по умолчанию или нулевого аргумента, и у них также нет методов настройки, аналогично, они не являются объектами передачи данных или обычными старыми объектами Java .

Кроме того, типизированный по значению класс должен быть окончательным, чтобы они не были расширяемыми, по крайней мере, чтобы кто-то переопределял методы. JavaBeans, DTOS и POJOS не обязательно должны быть окончательными.

3.2. Создание типа значения

Предположим, что мы хотим создать тип значения Foo с полями text и number. Как бы мы это сделали?

Мы создадим последний класс и пометим все его поля как окончательные. Затем мы будем использовать IDE для создания конструктора, метода hashCode () , метода equals(Object) , методов getters в качестве обязательных методов и метода toString () , и у нас будет такой класс:

public final class Foo {
    private final String text;
    private final int number;
    
    public Foo(String text, int number) {
        this.text = text;
        this.number = number;
    }
    
    // standard getters
    
    @Override
    public int hashCode() {
        return Objects.hash(text, number);
    }
    @Override
    public String toString() {
        return "Foo [text=" + text + ", number=" + number + "]";
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Foo other = (Foo) obj;
        if (number != other.number) return false;
        if (text == null) {
            if (other.text != null) return false;
        } else if (!text.equals(other.text)) {
            return false;
        }
        return true;
    }
}

После создания экземпляра Foo мы ожидаем , что его внутреннее состояние останется неизменным в течение всего жизненного цикла.

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

Поэтому даже изменение поля одного и того же объекта приведет к изменению значения hashCode .

3.3. Как работают типы Значений

Причина, по которой типы значений должны быть неизменяемыми, заключается в том, чтобы предотвратить любое изменение их внутреннего состояния приложением после их создания.

Всякий раз, когда мы хотим сравнить любые два объекта типа значений, мы должны, следовательно, использовать метод equals(Object) класса Object .

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

Кроме того, для того, чтобы мы могли использовать наши объекты значений в коллекциях на основе хэша, таких как HashSet и HashMap s, не нарушая, мы должны правильно реализовать метод hashCode () .

3.4. Зачем Нам Нужны Типы Значений

Потребность в типах значений возникает довольно часто. Это случаи, когда мы хотели бы переопределить поведение по умолчанию исходного класса Object .

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

Предположим, что мы хотели бы создать денежный объект следующим образом:

public class MutableMoney {
    private long amount;
    private String currency;
    
    public MutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    // standard getters and setters
    
}

Мы можем запустить на нем следующий тест, чтобы проверить его равенство:

@Test
public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() {
    MutableMoney m1 = new MutableMoney(10000, "USD");
    MutableMoney m2 = new MutableMoney(10000, "USD");
    assertFalse(m1.equals(m2));
}

Обратите внимание на семантику теста.

Мы считаем, что он прошел, когда два денежных объекта не равны. Это связано с тем, что мы не переопределили метод equals , поэтому равенство измеряется путем сравнения ссылок на память объектов, которые, конечно, не будут отличаться, потому что это разные объекты, занимающие разные места в памяти.

Каждый объект представляет собой 10 000 долларов США, но Java говорит нам, что наши денежные объекты не равны . Мы хотим, чтобы эти два объекта тестировались неодинаково только в том случае, если суммы валют различны или типы валют различны.

Теперь давайте создадим эквивалентный объект value, и на этот раз мы позволим IDE генерировать большую часть кода:

public final class ImmutableMoney {
    private final long amount;
    private final String currency;
    
    public ImmutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (int) (amount ^ (amount >>> 32));
        result = prime * result + ((currency == null) ? 0 : currency.hashCode());
        return result;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        ImmutableMoney other = (ImmutableMoney) obj;
        if (amount != other.amount) return false;
        if (currency == null) {
            if (other.currency != null) return false;
        } else if (!currency.equals(other.currency))
            return false;
        return true;
    }
}

Единственное различие заключается в том, что мы отменили методы equals(Object) и hashCode () , теперь у нас есть контроль над тем, как мы хотим, чтобы Java сравнивала наши денежные объекты. Давайте проведем эквивалентный тест:

@Test
public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() {
    ImmutableMoney m1 = new ImmutableMoney(10000, "USD");
    ImmutableMoney m2 = new ImmutableMoney(10000, "USD");
    assertTrue(m1.equals(m2));
}

Обратите внимание на семантику этого теста, мы ожидаем, что он пройдет, когда оба денежных объекта будут равны с помощью метода equals .

4. Почему Автоматическое значение?

Теперь, когда мы полностью понимаем типы значений и почему они нам нужны, мы можем посмотреть на автоматическое значение и то, как оно входит в уравнение.

4.1. Проблемы С Ручным Кодированием

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

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

Переопределение хэш-кода() и равно(Объекту) методы требуют около 9 строк и 18 строк соответственно, а переопределение toString() метод добавляет еще пять строк.

Это означает, что хорошо отформатированная кодовая база для наших двух классов полей займет около 50 строк кода .

4.2 Идеи для Спасения?

Это легко сделать в среде IDE, такой как Eclipse или Intellij, и создать только один или два класса с типизированными значениями. Подумайте о множестве таких классов для создания, будет ли это так же просто, даже если IDE поможет нам?

Перенесемся вперед, через несколько месяцев, предположим, что нам придется пересмотреть наш код и внести изменения в наши классы Money и, возможно, преобразовать поле currency из типа String в другой тип значения, называемый Currency.

4.3 Идеи На Самом Деле Не так Полезны

IDE, подобная Eclipse, не может просто редактировать для нас наши методы доступа или методы toString () , hashCode() или equals(Object) .

Этот рефакторинг должен быть выполнен вручную . Редактирование кода увеличивает вероятность ошибок, и с каждым новым полем, которое мы добавляем в класс Money , количество строк увеличивается экспоненциально.

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

5. Пример автоматического значения

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

Мы рассмотрим тот же самый пример Money , но на этот раз с автоматическим значением. Мы будем называть этот класс Auto Value Money ради согласованности:

@AutoValue
public abstract class AutoValueMoney {
    public abstract String getCurrency();
    public abstract long getAmount();
    
    public static AutoValueMoney create(String currency, long amount) {
        return new AutoValue_AutoValueMoney(currency, amount);
    }
}

Что произошло, так это то, что мы пишем абстрактный класс, определяем для него абстрактные методы доступа, но не поля, мы аннотируем класс с помощью @AutoValue всего 8 строк кода, и javac генерирует для нас конкретный подкласс, который выглядит следующим образом:

public final class AutoValue_AutoValueMoney extends AutoValueMoney {
    private final String currency;
    private final long amount;
    
    AutoValue_AutoValueMoney(String currency, long amount) {
        if (currency == null) throw new NullPointerException(currency);
        this.currency = currency;
        this.amount = amount;
    }
    
    // standard getters
    
    @Override
    public int hashCode() {
        int h = 1;
        h *= 1000003;
        h ^= currency.hashCode();
        h *= 1000003;
        h ^= amount;
        return h;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o instanceof AutoValueMoney) {
            AutoValueMoney that = (AutoValueMoney) o;
            return (this.currency.equals(that.getCurrency()))
              && (this.amount == that.getAmount());
        }
        return false;
    }
}

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

Javac всегда будет восстанавливать обновленный код для нас .

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

Вот тест, который проверяет, правильно ли заданы наши поля:

@Test
public void givenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoney m = AutoValueMoney.create("USD", 10000);
    assertEquals(m.getAmount(), 10000);
    assertEquals(m.getCurrency(), "USD");
}

Тест для проверки того, что два Auto Value Money объекты с одинаковой валютой и одинаковой суммой теста равны следующим:

@Test
public void given2EqualValueTypesWithAutoValue_whenEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("USD", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertTrue(m1.equals(m2));
}

Когда мы меняем тип валюты одного денежного объекта на GBP, тест: 5000 GBP USD больше не верен:

@Test
public void given2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertFalse(m1.equals(m2));
}

6. Автоматическая оценка Со Строителями

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

Обратите внимание, что если бы все наши поля были Строками , было бы легко поменять их местами, когда мы передали их статическому фабричному методу, например, поместив сумму вместо валюты и наоборот.

Это особенно вероятно, если у нас много полей, и все они имеют тип String . Эта проблема усугубляется тем, что при использовании AutoValue все поля инициализируются через конструктор .

Чтобы решить эту проблему, мы должны использовать шаблон builder . К счастью. это может быть сгенерировано с помощью автоматического значения.

Наш класс AutoValue на самом деле не сильно меняется, за исключением того, что статический заводской метод заменяется построителем:

@AutoValue
public abstract class AutoValueMoneyWithBuilder {
    public abstract String getCurrency();
    public abstract long getAmount();
    static Builder builder() {
        return new AutoValue_AutoValueMoneyWithBuilder.Builder();
    }
    
    @AutoValue.Builder
    abstract static class Builder {
        abstract Builder setCurrency(String currency);
        abstract Builder setAmount(long amount);
        abstract AutoValueMoneyWithBuilder build();
    }
}

Сгенерированный класс точно такой же, как и первый, но создается конкретный внутренний класс для построителя, а также реализующий абстрактные методы в построителе:

static final class Builder extends AutoValueMoneyWithBuilder.Builder {
    private String currency;
    private long amount;
    Builder() {
    }
    Builder(AutoValueMoneyWithBuilder source) {
        this.currency = source.getCurrency();
        this.amount = source.getAmount();
    }
    
    @Override
    public AutoValueMoneyWithBuilder.Builder setCurrency(String currency) {
        this.currency = currency;
        return this;
    }
    
    @Override
    public AutoValueMoneyWithBuilder.Builder setAmount(long amount) {
        this.amount = amount;
        return this;
    }
    
    @Override
    public AutoValueMoneyWithBuilder build() {
        String missing = "";
        if (currency == null) {
            missing += " currency";
        }
        if (amount == 0) {
            missing += " amount";
        }
        if (!missing.isEmpty()) {
            throw new IllegalStateException("Missing required properties:" + missing);
        }
        return new AutoValue_AutoValueMoneyWithBuilder(this.currency,this.amount);
    }
}

Обратите также внимание, что результаты теста не меняются.

Если мы хотим знать, что значения полей на самом деле правильно заданы с помощью конструктора, мы можем выполнить этот тест:

@Test
public void givenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder().
      setAmount(5000).setCurrency("USD").build();
    assertEquals(m.getAmount(), 5000);
    assertEquals(m.getCurrency(), "USD");
}

Чтобы проверить, что равенство зависит от внутреннего состояния:

@Test
public void given2EqualValueTypesWithBuilder_whenEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    assertTrue(m1.equals(m2));
}

И когда значения полей отличаются:

@Test
public void given2DifferentValueTypesBuilder_whenNotEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("GBP").build();
    assertFalse(m1.equals(m2));
}

7. Заключение

В этом уроке мы познакомили вас с большинством основ библиотеки AutoValue от Google и с тем, как использовать ее для создания типов значений с очень небольшим количеством кода с нашей стороны.

Альтернативой автоматической оценке Google является проект Lombok – вы можете ознакомиться со вступительной статьей об использовании Lombok здесь .

Полную реализацию всех этих примеров и фрагментов кода можно найти в проекте AutoValue GitHub .