【问题标题】:Do I need defensive copying constructing an immutable class with *non final* albeit immutable fields?我是否需要防御性复制来构建一个具有 *non final * 尽管是不可变字段的不可变类?
【发布时间】:2013-11-30 15:09:50
【问题描述】:

如果一个人想要构造一个不可变的类,不应该公开对非 final 字段的引用 - 但即使是像字符串这样的不可变对象?

public final class Test { // Test class is meant to be immutable

    private String s; // CAN'T MAKE THIS FINAL

    void onCreate(String s) { // a callback called ONCE after construction
        this.s = new String(s); // do I need to do this ? (protect me from me)
    }

    public String getS() {
        return new String(s); //do I need to do this ?(protect me from the world)
    }
}

【问题讨论】:

    标签: java security immutability encapsulation defensive-copy


    【解决方案1】:

    理论上,通过不安全的发布,可以看到Test 类的实例具有未初始化的 (null) s,也可以通过正确初始化的s 看到。这可以通过设置 s volatile 来解决。

    但是,如果您有类似的回调发生,我认为您想重新审视一下您的设计。

    如果你要上课 Serializable 那么你会遇到更多问题。

    【讨论】:

    • 所以 - 阅读 - 不安全的发布本质上意味着对象(引用)在构建对象时对许多线程可见 - 因此线程以不一致的状态查看对象。如果单个线程正在构造和访问对象,这应该不是问题 - 但是如果将引用传递给另一个线程,即使在构造之后它可能会看到对象处于不一致状态?我不这么认为,但我必须阅读它
    【解决方案2】:

    这个类是否不可变并不重要(对于不可变的任何定义)。特别是,引用s 是否更改为指向不同的字符串并不重要。 字符串对象是不可变的,所以你不需要复制它。如果没有防御性复制,getS 的调用者将获得对Test 的方法和getS 的其他调用者使用的相同字符串对象的引用。这无关紧要,因为他们对这个字符串所做的任何1 都会影响其他所指对象。这将是浪费时间和内存。

    1 我忽略了反思。像这样恶意使用反射的代码几乎可以破坏任何东西,而且不是偶然编写的,也不是很难发现的。担心这种情况根本不切实际。

    【讨论】:

    【解决方案3】:

    我认为没有必要。甚至在文档中是这样说的:

    字符串是常量;它们的值在它们被更改后不能更改 创建。因为字符串 对象是不可变的,它们可以共享。

    因此,一旦创建了 String 对象,它的值就永远不会改变。如果我们想“改变”变量的值,就会创建一个新的 String 对象。比如toUpperCase方法中,原字符串不变,但创建了一个新的副本。

    编辑:

    在考虑字符串时,文字被放入共享池中,这意味着:

    String h = "HELLO";
    String h1 = "HELLO";
    

    s1s2 都指向同一个对象。

    你可以试试下面的代码返回true

    String h = "HELLO";
    String h1 = "HELLO";
    boolean r = (h==h1);
    System.out.println(r);
    

    但是,您可以使用反射来更改 String 的值:

    java.lang.reflect.Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);
    valueField.set("Original", "Modified".toCharArray()); 
    

    【讨论】:

    • 我想知道是否可以以某种方式获取对字符串的引用并执行类似ref="MyString" 的操作。它不仅适用于字符串
    • 那么改变的是该字段现在将引用一个值为“MyString”的新对象。我认为如果不变性做得对,它应该适用于所有对象。加上final 字段与对象的不变性无关,它不会简单地允许更改变量指向的引用。
    • @Mr_and_Mrs_D 如果你想构造一个不可变的类,那为什么不声明所有变量final呢?
    • @KubaSpatny 说了什么 :)
    • @Vic:你听说过 android 吗? ;)
    【解决方案4】:

    从技术上讲,如果您真的想要 Java 中的不可变类,您必须确保类的实例在创建后不能更改。因此,它的所有字段都可以是最终的,并且如果它们通过 getter“暴露”给世界,例如,这些字段本身必须是不可变的(就像字符串一样)或者不返回到外部世界(保持私有并创建防御性副本它们在 getter 中),因此原始字段值保持不变。这种不变性也不能容易被从这个类继承而破坏。

    您可以在 Effective Java(Joshua Bloch 的一本书)中阅读有关它的更多信息,或者从互联网上做一些笔记,例如 from here

    关于您最近对帖子的更新,这里有一个建议,可以确保只进行一次初始化:

    private String s; // CAN'T MAKE THIS FINAL
    private boolean stringWasSet = false;
    
    public void onCreate(String s) { // a callback called ONCE after construction
        if (!stringWasSet) {
            this.s = s; // No need for defensive copy here, if the variable itself is immutable, like String
            stringWasSet = true;
        }
    }
    
    public String getS() {
        return s; // No need for defensive copy here, if the variable itself is immutable, like String
    }
    

    【讨论】:

    • 我读过有效的 Java - 我的问题是我们如何创建一个具有非最终但不可变字段的不可变类 - 这并不容易
    • @Mr_and_Mrs_D 如果该字段包含不可变对象,那么您可以返回它(因为它的不变性应该确保它不会被更改)但是对于您的类,您不能有任何允许更改的方法非最终字段
    • @Mr_and_Mrs_D,这些字段不是私有的/有设置器吗?
    • @Vic:当然不是——我想创建一个不可变的类——那我为什么要添加 setter 呢?让我担心的是我没有想到的攻击——在安全领域每隔一天就会发生一次 :) 那么我需要防御性复制吗?还是有其他方法(当然除了反射)?
    • 所以你的类是不可变的;您不必定义最终字段,但它不会造成伤害,并向查看您的代码的下一个开发人员声明您的意图。
    猜你喜欢
    • 2012-08-13
    • 1970-01-01
    • 1970-01-01
    • 2019-05-30
    • 1970-01-01
    • 1970-01-01
    • 2016-09-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多