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

Неизменяемые структуры данных в Java

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

В рамках некоторых интервью по программированию, которые я проводил в последнее время, регулярно поднимается тема неизменяемых структур данных. Я сам не слишком догматичен в этом, но всякий раз, когда нет необходимости в изменяемом состоянии, я пытаюсь избавиться от кода, который делает код изменяемым, что часто наиболее заметно в структурах данных. Однако, похоже, существует некоторое недопонимание концепции неизменяемости, когда разработчики часто считают, что наличия ссылки final или val в Kotlin или Scala достаточно, чтобы сделать объект неизменяемым. Этот пост в блоге немного углубляется в неизменяемые ссылки и неизменяемые структуры данных.

Преимущества неизменяемых структур данных

Неизменяемые структуры данных обладают некоторыми существенными преимуществами, такими как:

  • Нет недопустимого состояния
  • Безопасность резьбы
  • Более легкий для понимания код
  • Легче тестировать
  • Может использоваться для типов значений

Нет недопустимого состояния

Когда объект неизменяем, трудно иметь объект в недопустимом состоянии. Объект может быть создан только через его конструктор, который будет обеспечивать действительность объектов. Таким образом, могут быть применены требуемые параметры для допустимого состояния. Пример:

Address address = new Address();
address.setCity("Sydney");
// address is in invalid state now, since the country hasn't been set.

Address address = new Address("Sydney", "Australia");
// Address is valid and doesn't have setters, so the address object is always valid.

Безопасность резьбы

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

Более легкий для понимания код

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

Легче тестировать

Поскольку объекты более предсказуемы, нет необходимости проверять все перестановки методов инициализатора; т.е. при вызове конструктора класса объект является либо допустимым, либо недопустимым. Другие части кода, использующие эти классы, становятся более предсказуемыми, имея меньше шансов на NullPointerExceptions. Иногда при передаче объектов по кругу существуют методы, которые потенциально изменяют состояние объекта. Например:

public boolean isOverseas(Address address) {
    if(address.getCountry().equals("Australia") == false) {
        address.setOverseas(true); // address has now been mutated!
        return true;
    } else {
        return false;
    }
}

Приведенный выше код, в общем, является плохой практикой. Он возвращает логическое значение, а также потенциально изменяет состояние объекта. Это затрудняет понимание и тестирование кода. Лучшим решением было бы удалить установщик из класса Address и вернуть логическое значение, проверив название страны. Еще лучшим способом было бы перенести эту логику в сам класс Address (address.is За границей()). Когда состояние действительно необходимо установить, создайте копию исходного объекта без изменения входных данных.

Может использоваться для типов значений

Представьте себе денежную сумму, скажем, 10 долларов. 10 долларов всегда будут 10 долларами. В коде это может выглядеть как государственные деньги (конечная сумма BigInteger, конечная валюта Currency). Как вы можете видеть в этом коде, невозможно изменить значение 10 долларов на что-либо другое, кроме этого, и, таким образом, вышеизложенное можно безопасно использовать для типов значений.

Окончательные ссылки не делают объекты неизменяемыми

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

Окончательная ссылка не делает ваши объекты неизменяемыми!

Другими словами, следующий код не делает ваши объекты неизменяемыми:

final Person person = new Person("John");

Почему нет? Ну, в то время как `person` является конечным полем и не может быть переназначен, класс Person может иметь метод setter или другие методы-мутаторы, выполняющие действие, подобное:

person.setName("Cindy");

Довольно простая вещь, которую можно сделать, независимо от конечного модификатора. В качестве альтернативы класс Person может предоставить список адресов, подобных этому. Доступ к этому списку позволяет вам добавить в него адрес и, следовательно, изменяет объект person следующим образом:

person.getAddresses().add(new Address("Sydney"));

Наша последняя ссылка снова не помогла нам остановить нас от изменения объекта person.

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

  • Не раскрывайте внутреннее состояние изменяемым способом
  • Не меняйте состояние внутренне
  • Убедитесь, что подклассы не переопределяют описанное выше поведение

Руководствуясь следующими рекомендациями, давайте разработаем улучшенную версию нашего класса Person.

public final class Person {  // final class, can't be overridden by subclasses
    private final String name;     // final for safe publication in multithreaded applications
    private final List
addresses; public Person(String name, List
addresses) { this.name = name; this.addresses = List.copyOf(addresses); // makes a copy of the list to protect from outside mutations (Java 10+). // Otherwise, use Collections.unmodifiableList(new ArrayList<>(addresses)); } public String getName() { return this.name; // String is immutable, okay to expose } public List
getAddresses() { return addresses; // Address list is immutable } } public final class Address { // final class, can't be overridden by subclasses private final String city; // only immutable classes private final String country; public Address(String city, String country) { this.city = city; this.country = country; } public String getCity() { return city; } public String getCountry() { return country; } }

Теперь следующий код можно использовать следующим образом:

import java.util.List;
final Person person = new Person("John", List.of(new Address("Sydney", "Australia"));

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

Обновление: Как упоминали некоторые люди , приведенный выше код все еще был изменяемым, потому что я не делал копию списка адресов в конструкторе. Итак, без вызова new ArrayList() в конструкторе все еще можно выполнить следующее:

final List
addresses = new ArrayList<>(); addresses.add(new Address("Sydney", "Australia")); final Person person = new Person("John", addressList); addresses.clear();

Однако, поскольку теперь в конструкторе создается копия, приведенный выше код больше не будет влиять на скопированную ссылку на список адресов в классе Person, что делает код безопасным. Спасибо всем за то, что заметили!

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

Еще раз большое спасибо моему коллеге Уинстону за то, что он нашел время вычитать и просмотреть этот пост в блоге!

Оригинал: “https://dev.to/bodiam/immutable-data-structures-in-java-34a1”