如何防止在班级中修改私有字段?


165

想象一下我有这堂课:

public class Test
{
  private String[] arr = new String[]{"1","2"};    

  public String[] getArr() 
  {
    return arr;
  }
}

现在,我有另一个使用上述类的类:

Test test = new Test();
test.getArr()[0] ="some value!"; //!!!

这就是问题所在:我已经从外部访问了班级的私有领域!我该如何预防?我的意思是如何使该数组不可变?这是否意味着您可以使用每种获取方法来逐步访问私有字段?(我不需要像Guava这样的任何库。我只需要知道执行此操作的正确方法)。


10
实际上,使其final确实阻止了该字段的修改。但是,防止Object由字段引用的修改更复杂。
OldCurmudgeon

22
如果您认为能够修改将引用存储在私有字段中的数组等同于能够修改私有字段,则心理模型就会出现问题。
Joren

45
如果是私人的,为什么要先公开呢?
埃里克·雷彭

7
我花了一些时间才弄清楚这一点,但是当我确保所有数据结构都完全封装在其对象中时,它总是会改进我的代码-意味着没有办法“获取”“ arr”,而无论您必须在内部做什么该类或提供一个迭代器。
Bill K

8
其他人是否也感到惊讶,为什么这个问题得到了如此多的关注?
2013年

Answers:


163

您必须返回阵列的副本

public String[] getArr() {
  return arr == null ? null : Arrays.copyOf(arr, arr.length);
}

121
我想提醒大家,尽管这已被选为问题的正确答案,但实际上,如sp00m所言,对该问题最佳解决方案是返回a 。Unmodifiable List
OldCurmudgeon

48
如果您确实需要返回数组,则不需要。
Svish 2013年

8
实际上,所讨论问题最佳解决方案通常是@MikhailVladimirov所说的:-提供或返回数组或集合的视图
ChristofferHammarström,2013年

20
但请确保它是数据的深层副本,而不是浅层副本。对于字符串,这没有什么区别,对于其他类(如Date),可以修改实例的内容,这可以有所作为。
jwenting

16
@Svish我应该更加激进:如果您从API函数返回数组,那么您做错了。在库内部的私有函数中,可能是正确的。在API(防止可修改性起作用的API)中,从来没有
康拉德·鲁道夫

377

如果可以使用列表而不是数组,则Collections提供一个不可修改的列表

public List<String> getList() {
    return Collections.unmodifiableList(list);
}

添加了一个答案该答案Collections.unmodifiableList(list)用作对该字段的赋值,以确保列表不会被类本身更改。
Maarten Bodewes 2014年

只是想知道,如果您反编译此方法,则会得到很多新的关键字。当getList()获得大量访问权限时,这不会损害性能。例如在渲染代码中。
Thomas15v 2014年

2
请注意,如果数组的元素本身像String原来一样是不可变的,则可以这样做,但是如果它们是可变的,则可能必须执行某些操作以防止调用者更改它们。
Lii 2015年

45

修饰符private仅保护字段本身不被其他类访问,而不能保护此字段引用的对象。如果您需要保护引用的对象,请不要将其分发出去。更改

public String [] getArr ()
{
    return arr;
}

至:

public String [] getArr ()
{
    return arr.clone ();
}

或者

public int getArrLength ()
{
    return arr.length;
}

public String getArrElementAt (int index)
{
    return arr [index];
}

5
本文并不反对在数组上使用克隆,而只在可以子类化的对象上使用。
Lyn Headley

28

Collections.unmodifiableList已经提到-的Arrays.asList()奇怪不是!我的解决方案还将是从外部使用列表,并将数组包装如下:

String[] arr = new String[]{"1", "2"}; 
public List<String> getList() {
    return Collections.unmodifiableList(Arrays.asList(arr));
}

复制数组的问题是:如果每次访问代码时都这样做,并且数组很大,那么肯定会为垃圾收集器创建很多工作。因此,复制是一种简单但非常糟糕的方法-我会说“便宜”,但内存昂贵!特别是当您有两个以上元素时。

如果你看一下源代码Arrays.asListCollections.unmodifiableList有其实不多创建。第一个只包装数组而不复制它,第二个只包装列表,使对它的更改不可用。


您在这里假设每次都复制包含字符串的数组。不是,仅复制对不可变字符串的引用。只要阵列不大,开销就可以忽略不计。如果数组很大,请一直寻找列表immutableList
Maarten Bodewes 2014年

不,我没有做这个假设-在哪里?它与复制的引用有关-是字符串还是任何其他对象都没有关系。只是说,对于大型阵列,我不建议您复制阵列,对于大型阵列,Arrays.asListand Collections.unmodifiableList操作可能更便宜。也许有人可以计算出需要多少个元素,但是我已经在for循环中看到过代码,其中包含要复制的成千上万个元素的数组只是为了防止修改-太疯狂了!
michael_s 2014年

6

您也可以使用ImmutableList哪个应该比标准更好unmodifiableList。该类是Google创建的Guava库的一部分。

说明如下:

与Collections.unmodifiableList(java.util.List)(它是仍可以更改的单独集合的视图)不同,ImmutableList实例包含其自己的私有数据,并且永远不会更改

这是一个简单的用法示例:

public class Test
{
  private String[] arr = new String[]{"1","2"};    

  public ImmutableList<String> getArr() 
  {
    return ImmutableList.copyOf(arr);
  }
}

如果您不信任Test该类以使返回的列表保持不变,请使用此解决方案。您需要确保ImmutableList返回了该值,并且列表中的元素也是不可变的。否则我的解决方案也应该是安全的。
Maarten Bodewes 2014年

3

在这种情况下,您应该使用系统阵列副本:

public String[] getArr() {
   if (arr != null) {
      String[] arrcpy = new String[arr.length];
      System.arraycopy(arr, 0, arrcpy, 0, arr.length);
      return arrcpy;
   } else
      return null;
   }
}

-1,因为它不会在调用上做任何事情clone,并且不够简洁。
Maarten Bodewes 2014年

2

您可以返回数据的副本。选择更改数据的呼叫者只会更改副本

public class Test {
    private static String[] arr = new String[] { "1", "2" };

    public String[] getArr() {

        String[] b = new String[arr.length];

        System.arraycopy(arr, 0, b, 0, arr.length);

        return b;
    }
}

2

问题的根源在于您正在返回一个指向可变对象的指针。哎呀。您要么使对象变得不可变(不可修改的列表解决方案),要么返回对象的副本。

通常,如果对象是可变的,则对象的终结性不能保护它们不被更改。这两个问题是“亲吻表亲”。


Java有引用,而C有指针。说够了。此外,“最终”修饰符可以根据应用的位置而产生多种影响。在类成员上申请将强制您仅使用“ =(赋值)”运算符向成员完全赋值一次。这与不变性无关。如果将成员的引用替换为新成员的引用(同一类中的其他其他实例),则先前的实例将保持不变(但如果没有其他引用保存它们,则可以将其GC化)。
gyorgyabraham 2013年

1

返回一个不可修改的列表是一个好主意。但是,在调用getter方法期间无法修改的列表仍可以由该类或从该类派生的类来更改。

相反,您应该向扩展类的任何人明确表示不应该修改列表。

因此,在您的示例中,它可能导致以下代码:

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Test {
    public static final List<String> STRINGS =
        Collections.unmodifiableList(
            Arrays.asList("1", "2"));

    public final List<String> getStrings() {
        return STRINGS;
    }
}

在上面的示例中,我将 STRINGS字段公开,原则上您可以取消方法调用,因为这些值是已知的。

您也可以将字符串分配给private final List<String>在构造类实例期间不可修改的字段。使用(构造函数的)常量或实例化参数取决于类的设计。

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Test {
    private final List<String> strings;

    public Test(final String ... strings) {
        this.strings = Collections.unmodifiableList(Arrays
                .asList(strings));
    }

    public final List<String> getStrings() {
        return strings;
    }
}

0

是的,您应该返回数组的一个副本:

 public String[] getArr()
 {
    return Arrays.copyOf(arr);
 }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.