如果想要构建一个不可变的类,就不应该公开对非最终字段的引用,但即使是对于像Strings这样的不可变对象?
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)
}
}
理论上,通过不安全的发布可以看到具有未初始化(null
)s
的Test
类的实例,也可以看到具有正确初始化的s
的实例。这可以通过使s
成为volatile
来解决。
然而,如果你遇到了这样的回调,我想你应该重新审视一下你的设计。
如果你要使类Serializable
,那么你会遇到更多的问题。
这个类是否是不可变的并不重要(对于任何不可变的定义)。特别地,引用s
是否被改变为指向不同的字符串并不重要。字符串对象是不可变的,因此您不需要复制它。如果没有防御性复制,getS
的调用方将获得对Test
的方法和getS
的其他调用方使用的相同字符串对象的引用。这并不重要,因为他们对这个字符串所做的任何事情都不会影响其他引用。那将是浪费时间和记忆。
1我忽略了反射。像这样恶意使用反射的代码几乎可以破坏任何东西,而且不是偶然或难以发现的。担心这个案子根本不现实。
我认为没有必要。甚至在文件中说:
字符串是常量;它们的值在创建。因为字符串对象是不可变的,它们可以共享。
因此,一旦创建了String对象,它的值就永远不会改变。如果我们想"更改"变量的值,就会创建一个新的String对象。例如在toUpperCase
方法中,原始字符串保持不变,但会创建新的副本。
编辑:
当考虑字符串时,文字被放入一个共享池中,这意味着:
String h = "HELLO";
String h1 = "HELLO";
CCD_ 13和CCD_。
您可以尝试以下代码返回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());
从技术上讲,如果你真的想要一个Java中的不可变类,你必须确保类的实例在创建后不会被更改。因此,它的所有字段都可以是最终字段,例如,如果它们通过getter"暴露"给世界,这些字段本身必须是不可变的(就像字符串一样),或者不返回到外部世界(保持私有并在getter中创建它们的防御副本),因此原始字段值保持不变。这种不变性决不能因为从这个类继承而被破坏。
你可以在乔舒亚·布洛赫的《高效Java》一书中阅读更多关于它的内容,或者从互联网上做一些笔记,比如在这里。
关于你最近对帖子的更新,这里有一个建议,可以确保初始化只进行一次:
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
}