Как сделать объект неизменным в java

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

  • Сделать класс окончательным - имеет смысл
  • Не разрешать мутаторы (сеттеры) для атрибутов - имеет смысл
  • Сделать атрибуты private - имеет смысл

Теперь я не понимаю, почему нам нужны ниже точки

  • Сделать конструктор закрытым и предоставить метод createInstance с теми же атрибутами, что и конструктор или метод factory? Как это помогает?
  • Сделать атрибуты final - пост сообщения не может объяснить эту точку, а некоторые, где я читаю, чтобы избежать модификации случайно. Как вы можете случайно изменить, когда нет мутаторов, и класс является окончательным? Помогает ли создание атрибута final?
  • Вместо шаблона factory можно ли использовать шаблон построителя?

Я добавляю свой класс и тестовый пример:

    public final class ImmutableUser {
    private final UUID id;
    private final String firstName;
    private final String lastName;

    public ImmutableUser(UUID id, String firstName, String lastName) {
        super();
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    /**
     * @return the id
     */
    public UUID getId() {
        return id;
    }
    /**
     * @return the firstName
     */
    public String getFirstName() {
        return firstName;
    }
    /**
     * @return the lastName
     */
    public String getLastName() {
        return lastName;
    }
}

Тестовая версия

public class ImmutableUserTest {

        @Test(expected = IllegalAccessException.class)
        public void reflectionFailure() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
            ImmutableUser user = new ImmutableUser(UUID.randomUUID(), "john", "liu");
            Field i =user.getClass().getDeclaredField("firstName");
            i.setAccessible(true);
            i.set(user, "cassandra");
            System.out.println("user " + user.getFirstName()); // prints cassandra
        }

    }

Этот тестовый пример выходит из строя и печатает кассандру.

Сообщите мне, если я что-то делаю неправильно.

Ответы

Ответ 1

  • Сделать конструктор закрытым и предоставить метод createInstance с теми же атрибутами, что и конструктор или метод factory? Как это помогает?

Ответ: создание частного конструктора и предоставление метода createInstance() (factory) само по себе не помогает: это одна из немногих вещей, которые вы должны сделать, чтобы пользователи могли фактически использовать класс и его экземпляры, пока вы все еще контролируете способ создания экземпляров.

  • Сделать атрибуты final - сообщение не может объяснить эту точку, и где-то я читал, чтобы избежать модификации случайно. Как вы можете случайно изменить, когда нет мутаторов, и класс является окончательным? Помогает ли создание атрибута final?

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

final public class SomeClass {
    final Integer i = 1;
}

из другого класса вы можете сделать следующее:

class AnotherClass {

    public static void main (String[] args) throws Exception {

        SomeClass p = new SomeClass();
        Field i =p.getClass().getDeclaredField("i");
        i.setAccessible(true);
        i.set(p, 5);
        System.out.println("p.i = " + p.i); // prints 5
    }
}
  • Может ли вместо factory использовать шаблон компоновщика?

Ответ: вы можете использовать шаблон построителя или любой шаблон, который поможет вам управлять созданием экземпляров класса.

Далее:
Если вы хотите убедиться, что ваш класс неизменен, убедитесь, что любой getter возвращает глубокую копию члена класса. Этот метод называется "защитная/защитная копия". Вы можете узнать больше об этом здесь

Ответ 2

Я бы начал с создания атрибутов final. Создание атрибута final гарантирует, что вы не можете изменить значение атрибута. Я думаю, это очевидно. (Я напишу дополнительный комментарий для изменения содержания ссылок на неизменяемые объекты позже).

Теперь, когда все ваши атрибуты final, они должны быть инициированы с помощью конструктора. Однако некоторые классы имеют много атрибутов, поэтому конструктор становится огромным. Кроме того, иногда некоторые атрибуты могут быть инициализированы значениями по умолчанию. Попытка поддержать это заставляет нас реализовать несколько конструкторов с почти случайной комбинацией аргументов. Однако шаблон Builder помогает нам. Но как заставить пользователя использовать Builder вместо прямого вызова конструктора? Ответ заключается в создании конструктора private и создании статического метода, который возвращает построитель:

public class Person {
    private final String firstName;
    private final String lastName;
    private final Person mother;
    private final Person father;

    private Person(String firstName, String lastName, Person mother, Person father) {
        // init the fields....
    }

    public static PersonBuilder builder() {
        return new PersonBuilder();
    }


    public static class PersonBuilder {
        // here fields are NOT final 
        private String firstName;
        private String lastName;
        private Person mother;
        private Person father;

        public PersonBuilder bornBy(Person mother) {
            this.mother = mother;
             return this;
        }

        public PersonBuilder conceivedBy(Person father) {
             this.father = father;
             return this;
        }

        public PersonBuilder named(String firstName) {
             this.firstName = firstName;
             return this;
        }

        public PersonBuilder fromFamily(String lastName) {
             this.lastName = lastName;
             return this;
        }

        Person build() {
              return new Person(name, lastName, mother, father);
        } 
    }
}

И вот типичный шаблон использования:

Person adam = Person.builder().named("Adam").build(); // no mother, father, family
Person eve = Person.builder().named("Eve").build(); // no mother, father, family
Person cain = Person.builder().named("Cain").conerivedBy(adam).bornBy(eve); // this one has parents

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

Я думаю, что вы пропустили одну точку в своем вопросе: ссылки на другие (изменяемые) объекты. Если, например, мы добавляем поле Collection<Person> children к нашему классу Person, нам нужно иметь в виду, что getChildren() возвращает либо Iterable, либо, по крайней мере, немодифицированный сбор.

Ответ 3

Создание конструктора частным и использование шаблона компоновщика не обязательны для неизменности. Однако, поскольку ваш класс не может обеспечить сеттеры и если у него много полей, использование конструктора с множеством параметров может отрицательно сказаться на удобочитаемости, поэтому возникает идея использовать шаблон компоновщика (для которого нужен конструктор pervade).

Другие ответы, похоже, упустили важный момент.

Использование полей final важно не только для гарантии того, что они не будут изменены, но и потому, что в противном случае вы потеряете некоторые важные гарантии безопасности потоков. Действительно, один из аспектов неизменности заключается в том, что он обеспечивает безопасность потоков. Если вы не сделаете поля окончательными, ваш класс станет практически неизменным. Смотрите, например, Должны ли все свойства неизменяемого объекта быть окончательными?