如何防止改变变量的值

时间:2019-03-30 04:32:46

标签: java

我是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]。我该怎么办?

9 个答案:

答案 0 :(得分:19)

数组通过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));
    

但是,我更喜欢第一种方法。

答案 1 :(得分:8)

在Java中,没有不可变(不可更改)数组之类的东西。 Java语言不支持此功能。正如JLS 4.12.4所述:

  

如果final变量持有对对象的引用,则可以通过对该对象的操作来更改对象的状态,但是该变量将始终引用同一对象。 这也适用于数组,因为数组是对象;如果final变量持有对数组的引用,则可以通过对该数组进行操作来更改该数组的组件,但是该变量将始终引用同一数组。

JVM规范也不支持不可变数组类型。您无法在语言级别解决此问题。避免更改数组的唯一方法是不要与可能对其进行更改的其他代码共享对该数组的引用。

在您的示例中,您拥有一个leaky abstraction。您正在将数组传递给Person类,并且调用者保留对该数组的引用,以便可以对其进行更改。要解决此问题,您可以:

  • 复制数组,并将引用传递给副本,或者
  • 让构造函数(或array属性的setter)进行复制。

(有关示例代码,请参见答案https://stackoverflow.com/a/55428214/139985。)

从面向对象的角度来看,第二种选择是更可取的。 Person类应负责保持其内部状态不受干扰……如果这是您的设计要求。它不应该依赖调用者来执行此操作。 (即使呼叫方在技术上属于此课程的 部分,也是如此)。

答案 2 :(得分:1)

没有不可修改的数组,但是您可以创建一个不可修改的列表:

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

您将不得不更改代码以使用列表而不是数组,但这通常还是更可取的。


如果您已经具有非基本类型的数组,则可以将其包装在不可修改的列表中,如下所示:

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

但是,虽然不能直接更改列表,但是更改数组将更改列表。此外,这不适用于int[],而仅适用于Object[]的子类。

答案 3 :(得分:1)

  

在Java中,对象/数组通过引用变量进行操作

使用数组作为参数调用函数时,仅传递对该数组的引用。因此,当您对array数组进行突变时,arrayTest字段也会被突变,因为它们引用指向相同的地址

要覆盖此行为,您可以使用 Object.clone() 方法在构造函数中创建数组的副本,例如:

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

#来源:维基百科

答案 4 :(得分:0)

我不像其他人建议的那样将数组的副本传递给对象,我建议Person对象的构造函数应创建一个副本。而不是

this.arrayTest = arrayTest;

应该是

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

这将允许对象防御试图在构造函数构造和验证后修改数组的恶意代码。实际上,大多数IDE都具有分析工具,该工具会警告您不要保存数组引用。

答案 5 :(得分:0)

正如其他人已经指出的那样:数组作为引用传递给Person。因此,Person对象将看到以后对数组所做的更改。但这仅仅是问题的一半:您不仅将对数组的引用传递给Person的构造函数,而且还对返回进行了引用来自getArray方法。


一般而言,正如StephenC在其回答中所指出的那样:面向对象设计的一个重要方面是正确管理对象的状态空间。类的用户应该不可能将对象置于任何形式的“不一致状态”。

这对于纯原始数组很难。考虑以下伪代码,引用您发布的类:

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()getAgeOfChild(int i)左右...)

解决此问题的另一种方法是在数组上公开(不可修改的)List视图。例如,可以使用this answer中显示的asUnmodifiableList方法来完成此操作。

答案 6 :(得分:0)

由于您是Java的初学者,因此在构造函数中编写了以下代码,但最好使用克隆方法,因为marco13和rv 7已经说明 正如sourabh bhat所解释的,我们还可以使用Arrays类copyof

以上所有逻辑背后的思想很简单,即不传递当前对象的引用,而是创建对象的副本并传递该副本,或者仅复制对象的每个内容

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

            }
    }

答案 7 :(得分:0)

编辑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,它基于数组并且应具有最佳性能。您必须在构造函数中将数组从List转换为List,在getArray()中将List从数组转换为array。只要您没有编写任何可以修改List的方法,就没有必要使用Collections.unmodifiableList()(或Guavas ImmutableList <>),因为没有人可以访问List。
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() {}
}

答案 8 :(得分:0)

这是内存中发生的事情:

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,以便复制实际的Objects值。为此,我们可以:

[1]使用clone的{​​{1}}属性。

array

[2]或者我们可以创建自己的克隆。

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

无论哪种方式,都会在内存中创建一个新对象,这会阻止该对象被共享。