Как предотвратить изменение значения переменной

Я новичок в Java. При разработке программы я создал объект с конструктором с переменными в качестве аргументов. Но когда я изменяю значение переменной после создания объекта, у моего объекта появляется второе значение вместо первого. Я не хочу, чтобы мой объект изменил значение. Что я делаю?

public class Person {

    public Person(int[] arrayTest) {
            this.arrayTest = arrayTest;
    }

    public int[] getArray() {
        return this.arrayTest;
    }

    public boolean canHaveAsArray(int[] arrayTest) {
            return true;
    }

    private int[] arrayTest = new int[2];

    public static void main(String[] args) {
        int[] array = new int[] {5, 10};
        Person obj1 = new Person(array);
        array[0] = 20;
        System.out.println(Arrays.toString(obj1.getArray()));
    }
}

Мой вывод должен быть [5, 10], но вместо этого я получаю [20,10]. Мне нужно получить [5,10], даже когда я изменяю элемент массива, как показано выше. Что я должен делать?

Ответ 1

Массив передается по ссылке в Java. Если вы передаете исходный массив конструктору Person, вы передаете ссылку на исходный массив. Таким образом, любое изменение в arrayTest внутри экземпляра Person будет отражаться в исходном массиве (массив int[] array) и наоборот.

Если вы не хотите изменять значение элементов исходного массива в экземпляре Person тогда у вас есть два варианта:

  • Вы можете изменить код в конструкторе Person чтобы создать копию исходного массива с помощью метода java.util.Arrays.copyOf а затем использовать эту копию:

    public Person(int[] arrayTest) {
        this.arrayTest = java.util.Arrays.copyOf(arrayTest, arrayTest.length);
    }
    
  • Не передавайте исходный массив конструктору, просто отправьте копию исходного массива:

    Person obj1 = new Person(java.util.Arrays.copyOf(array, array.length));
    

Однако я бы предпочел первый подход.

Ответ 2

В Java нет такого понятия, как неизменяемый (неизменяемый) массив. Язык Java не поддерживает это. В JLS 4.12.4 говорится:

Если final переменная содержит ссылку на объект, то состояние объекта может быть изменено операциями над объектом, но переменная всегда будет ссылаться на один и тот же объект. Это относится и к массивам, потому что массивы являются объектами; если final переменная содержит ссылку на массив, то компоненты массива могут быть изменены с помощью операций над массивом, но переменная всегда будет ссылаться на один и тот же массив.

Спецификация JVM также не поддерживает тип неизменяемого массива. Вы не можете решить это на уровне языка. Единственный способ избежать изменений в массиве - не делиться ссылкой на массив с другим кодом, который может его изменить.

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

  • скопировать массив и передать ссылку на копию, или
  • пусть конструктор (или установщик для атрибута массива) сделает копию.

(См. Пример ответа fooobar.com/questions/17399401/....)

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

Ответ 3

Не существует неизменяемого массива, но вы можете создать неизменяемый список:

List<Integer> list = List.of(5, 10);

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


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

List<Integer> list = Collections.unmodifiableList(Arrays.asList(array));

Однако, хотя вы не можете изменить список напрямую, изменение массива изменит список. Более того, это не сработает для int[], а только для подклассов Object[].

Ответ 4

В Java объекты/массивы обрабатываются через ссылочные переменные #

Когда функция вызывается с массивами в качестве аргументов, передается только ссылка на массив. Поэтому, когда вы изменяете array массива, поле arrayTest также видоизменяется, поскольку они ссылаются на один и тот же адрес.

Чтобы переопределить это поведение, вы можете создать копию массива в конструкторе, используя метод Object.clone(), например:

public Person(int[] arrayTest) {
    this.arrayTest = arrayTest.clone();
}

# Источник: Википедия

Ответ 5

Вместо того, чтобы передавать копию массива объекту, как предлагали другие, я бы порекомендовал, чтобы конструктор объекта Person создал копию. Что означает вместо

this.arrayTest = arrayTest;

Так должно быть

this.arrayTest = Arrays.copyOf(arrayTest, arrayTest.length);

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

Ответ 6

Как уже отмечали другие: массив передается как ссылка на Person. Таким образом, изменения, которые позже будут сделаны в массиве, будут видны объекту Person. Но это только одна половина проблемы: вы не только передаете ссылку на массив конструктору Person, вы также возвращаете ссылку из метода getArray.


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

И это сложно с простыми примитивными массивами. Рассмотрим следующий псевдокод, ссылающийся на класс, который вы опубликовали:

int originalArray[] = new int[2];
originalArray[0] = 12;
originalArray[1] = 34;

Person person = new Person(originalArray);
int arrayFromPerson[] = person.getArray();

originalArray[0] = -666;               // Modify the original array
System.out.println(arrayFromPerson[0]) // Prints -666 - this is unexpected!

arrayFromPerson[1] = 12345678;         // Modify the array from the person
System.out.println(originalArray[1])   // Prints 12345678 - this is unexpected!

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

Простые примитивные массивы в Java имеют свое оправдание. Но когда они появляются в интерфейсе класса (то есть в его public методах), их следует просматривать с тщательным анализом.

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

public Person(int[] arrayTest) {
    this.arrayTest = arrayTest.clone(); // Store a clone of the array
}
public int[] getArray() {
    return this.arrayTest.clone(); // Return a clone of the array
}

Но это может быть громоздким. Более объектно-ориентированным решением может быть предоставление "только для чтения" состояния, представленного в массиве. Например:

public Person(int[] arrayTest) {
    this.arrayTest = arrayTest.clone(); // Store a clone of the array
}
public int getArrayLength() {
    return this.arrayTest.length;
}
public int getArrayElement(int index) {
    return this.arrayTest[index];
}

(Конечно, на практике вы бы назвали методы соответствующим образом, в зависимости от того, что на самом деле представляет массив. Например, если это возраст детей, вы бы getNumChildren() методы getNumChildren() и getAgeOfChild(int i) или так...)

Другой вариант, как это может быть решено, состоит в том, чтобы выставить (неизменяемое) представление List в массиве. Это может быть сделано, например, с asUnmodifiableList метода asUnmodifiableList который показан в этом ответе.

Ответ 7

поскольку вы новичок в Java, вы пишете следующий код в конструкторе, но лучше использовать метод clone, как уже объяснили marco13 и rv 7 и как объяснил Сурабх Бхат, мы также можем использовать класс Arrays copyof

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

    public Person(int[] arrayTest) {
            for (int i = 0; i <this.arrayTest.length; i++) {
                this.arrayTest[i]=arrayTest[i];

            }
    }

Ответ 8

Изменить 5/5/19: Добавлен исходный код

Как указывает большинство ответов, в Java нет неизменного массива примитивов. Таким образом, вы должны сделать несколько трюков.

  1. Чистая Java: делайте защитные копии. Большинство ответов показывают, как хранить копию массива, полученного в качестве параметра в конструкторе. Но только в одном ответе упоминается, что вы также должны вернуть копию внутреннего массива с помощью getArray().
public class Person {
    final private int[] arrayTest;
    public Person(int[] arrayTest) {
            this.arrayTest = java.util.Arrays.copyOf(arrayTest, arrayTest.length);
    }

    public int[] getArray() {
        return java.util.Arrays.copyOf(arrayTest, arrayTest.length);;
    }
}
  1. Другое внутреннее представление: Храните массив как (изменяемый) ArrayList, который основан на массиве и должен иметь наилучшую производительность. Вы должны конвертировать из массива в список в конструкторе и из списка в массив в getArray(). Нет необходимости использовать Collections.unmodifiableList() (или Guavas ImmutableList<>), если вы не пишете метод, который мог бы изменить Список, поскольку никто не будет иметь доступа к списку.
public class Person {
    final private List<Integer> arrayTest;
    public Person(int[] arrayTest) {
            this.arrayTest = new ArrayList<>(Arrays.asList(arrayTest));
    }

    public int[] getArray() {
        return this.arrayTest.stream().mapToInt(Integer::valueOf).toArray;
    }
}
  1. Пусть другие люди делают работу. Google AutoValue автоматически создает неизменяемые классы. И предоставляет equals(), hashCode() и toString(). Легко использовать. Мое любимое решение.
import com.google.auto.value.AutoValue;
@AutoValue
public abstract class Person {
    public static create(int[] arrayTest) {
            return new AutoValue_Person(int[] arrayTest);
    }

    public abstract int[] getArray() {}
}

Ответ 9

Вот что происходит в памяти:

Program:                            Stack memory:                Heap memory:
int[] array = new int[] {5, 10};    array -> 0x77a89             0x77a89 {5, 10}
Person obj1 = new Person(array);    obj1.arrayTest -> 0x77a89    No change
array[0] = 20;                                                   0x77a89 {20, 10}

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

Чтобы это исправить, вам нужно создать new Object в памяти, чтобы фактическое значение объекта было скопировано. Для этого мы можем:

[1] Используйте свойство clone array.

public Person(int[] arrayTest) {
    this.arrayTest = arrayTest.clone();
}

[2] Или мы можем создать наш собственный клон.

public Person(int[] arrayTest){
    if (arrayTest == null){
        this.arrayTest = null;
    } else {
        int[] copyArray = new int[arrayTest.length];
        for(int i=0; i<arrayTest.length; i++) {
            copyArray[i] = arrayTest[i]
        }
        this.arrayTest = copyArray;
    }
}

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