如何防止修改类中的私有字段?

Posted

技术标签:

【中文标题】如何防止修改类中的私有字段?【英文标题】:How do I prevent the modification of a private field in a class? 【发布时间】:2013-01-26 09:16:42 【问题描述】:

想象一下我有这门课:

public class Test

  private String[] arr = new String[]"1","2";    

  public String[] getArr() 
  
    return arr;
  

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

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

所以这就是问题所在:我从外部访问了一个类的私有字段! 我怎样才能防止这种情况?我的意思是如何使这个数组不可变?这是否意味着您可以通过每个 getter 方法逐步访问私有字段? (我不想要任何像 Guava 这样的库。我只需要知道正确的方法)。

【问题讨论】:

实际上,将其设为final 确实防止修改字段。但是,防止修改字段引用的Object 比较复杂。 如果您认为能够修改存储在私有字段中的引用的数组与能够修改私有字段相同,那么您的心理模型就有问题。 如果它是私有的,为什么要首先公开它? 我花了一段时间才弄清楚这一点,但当我确保所有数据结构都完全封装在它们的对象中时,它总是会改进我的代码——这意味着没有办法“获取”你的“arr” ",而是在你必须在类中执行或提供迭代器时执行。 还有人惊讶于这个问题为什么会受到如此多的关注吗? 【参考方案1】:

如果你可以使用 List 而不是数组,Collections provides an unmodifiable list:

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

【讨论】:

Added an answer 使用 Collections.unmodifiableList(list) 作为字段的赋值,以确保列表不会被类本身更改。 只是想知道,如果你反编译这个方法,你会得到很多新的关键字。当 getList() 获得很多访问权限时,这不会损害性能。例如在渲染代码中。 请注意,如果数组的元素本身是不可变的,就像 String 一样,这有效,但如果它们是可变的,则可能需要做一些事情来防止调用者更改它们。【参考方案2】:

您必须返回数组的副本

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

【讨论】:

我想提醒大家,虽然这已被投票选为问题的正确答案,但问题的最佳解决方案实际上正如sp00m 所说——返回一个Unmodifiable List 如果您确实需要返回一个数组,则不需要。 实际上,所讨论的问题的最佳解决方案通常如@MikhailVladimirov 所说:- 提供或返回数组或集合的视图 . 但请确保它是数据的深层副本,而不是浅层副本。对于字符串来说没有区别,对于像 Date 这样可以修改实例内容的其他类,它可以有所作为。 @Svish 我应该更加激烈:如果您从 API 函数返回一个数组,那么您做错了。在库中的私有函数中,它可能是正确的。在 API 中(防止可修改性发挥作用),它从不【参考方案3】:

修饰符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];

【讨论】:

本文不反对在数组上使用克隆,只在可以子类化的对象上使用。【参考方案4】:

Collections.unmodifiableList 已经被提及 - Arrays.asList() 奇怪的是没有!我的解决方案也是使用外部列表并将数组包装如下:

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

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

如果您查看Arrays.asListCollections.unmodifiableList 的源代码,实际上创建的并不多。第一个只是包装数组而不复制它,第二个只是包装列表,使其无法更改。

【讨论】:

您在这里假设数组包括字符串 每次都被复制。不是,只复制了对不可变字符串的引用。只要您的阵列不是很大,开销就可以忽略不计。如果数组很大,请一直使用列表和immutableList 不,我没有做出这样的假设——在哪里?它是关于被复制的引用——不管它是一个字符串还是任何其他对象。只是说对于大数组,我不建议复制数组,Arrays.asListCollections.unmodifiableList 操作对于更大的数组可能更便宜。也许有人可以计算出需要多少元素,但我已经在 for 循环中看到了代码,其中包含数千个元素的数组被复制只是为了防止修改 - 这太疯狂了!【参考方案5】:

您也可以使用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,并且列表的元素也是不可变的。否则my solution 也应该是安全的。【参考方案6】:

此时你应该使用系统数组拷贝:

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 没有任何作用,而且不够简洁。【参考方案7】:

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

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;
    

【讨论】:

【参考方案8】:

问题的关键在于您正在返回一个指向可变对象的指针。哎呀。要么将对象呈现为不可变(不可修改的列表解决方案),要么返回对象的副本。

一般来说,如果对象是可变的,对象的最终确定性并不能保护对象不被更改。这两个问题就是“亲表亲”。

【讨论】:

Java 有引用,而 C 有指针。说够了。此外,“最终”修饰符可能会产生多种影响,具体取决于其应用位置。应用于类成员将强制您使用“=”(赋值)运算符只为成员赋值一次。这与不变性无关。如果将成员的引用替换为新的引用(来自同一类的其他实例),则前一个实例将保持不变(但如果没有其他引用持有它们,则可能会被 GC)。【参考方案9】:

返回一个不可修改的列表是个好主意。但是,在调用 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&lt;String&gt; 字段,该字段在类实例的构造过程中不可修改。使用常量或实例化参数(构造函数的)取决于类的设计。

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;
    

【讨论】:

【参考方案10】:

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

 public String[] getArr()
 
    return Arrays.copyOf(arr);
 

【讨论】:

【参考方案11】:

从 Java 9 开始,也可以从静态工厂方法 List.of() 构造不可变列表,从而减少导入和代码:

当可以修改原始users 字段时,从getUsers() 返回别名:

class Computer 
    private String[] users = new String[] "user1", "user2", "user3";
    public String[] getUsers;

    String[] getUsers() 
        return this.users;
    


Computer c = new Computer();
c.getUsers()[0] = "me";
for (String user: c.getUsers()) 
    System.out.println(user);

输出:

me
user2
user3

使用不可变列表:

import java.util.List;

class Computer 
    private String[] users = new String[] "user1", "user2", "user3";
    public List<String> getUsers;

    List<String> getUsers() 
        return List.of(this.users);
    


Computer c = new Computer();
c.getUsers().set(0, "me");
for (String user: c.getUsers()) 
    System.out.println(user);

输出:

user1
user2
user3

【讨论】:

以上是关于如何防止修改类中的私有字段?的主要内容,如果未能解决你的问题,请参考以下文章

在仅拿到头文件的情况下,如何修改类中的私有成员值?

Delphi:写入后代类中的私有祖先字段

访问类中的私有字段[重复]

如何使一个类中的私有哈希图对其他类可见[关闭]

用私有属性替换类中的每个字段是一种不好的做法吗? [复制]

反射学习3-通过反射机制修改类中的私有属性的值