这可能是有史以来最愚蠢的问题,但我认为这对新手来说完全是一个混乱.
有人可以澄清不可变的含义吗?
为什么是String
不可改变的?
不可变对象有哪些优点/缺点?
为什么一个可变对象StringBuilder
比首字母和副词更受欢迎?
一个很好的例子(在Java中)将非常感激.
不可变意味着一旦对象的构造函数完成执行,该实例就无法更改.
这很有用,因为它意味着您可以将引用传递给对象,而不必担心其他人会更改其内容.特别是在处理并发时,对于永不改变的对象没有锁定问题
例如
class Foo { private final String myvar; public Foo(final String initialValue) { this.myvar = initialValue; } public String getValue() { return this.myvar; } }
Foo
不必担心调用者getValue()
可能会更改字符串中的文本.
如果你想象一个类似的类Foo
,但有一个StringBuilder
而不是一个String
成员,你可以看到一个调用者getValue()
能够改变StringBuilder
一个Foo
实例的属性.
还要注意你可能会发现的不同类型的不变性:Eric Lippert写了一篇关于此的博客文章.基本上你可以拥有其接口是不可变的但在幕后实际可变的私有状态的对象(因此不能在线程之间安全地共享).
不可变对象是一个对象,其中无法更改内部字段(或至少影响其外部行为的所有内部字段).
不可变字符串有很多优点:
表现:采取以下操作:
String substring = fullstring.substring(x,y);
substring()方法的底层C可能是这样的:
// Assume string is stored like this: struct String { char* characters; unsigned int length; }; // Passing pointers because Java is pass-by-reference struct String* substring(struct String* in, unsigned int begin, unsigned int end) { struct String* out = malloc(sizeof(struct String)); out->characters = in->characters + begin; out->length = end - begin; return out; }
请注意,不得复制任何字符! 如果String对象是可变的(字符可能会在以后更改),那么您必须复制所有字符,否则对子字符串中字符的更改将在稍后的其他字符串中反映出来.
并发:如果不可变对象的内部结构有效,它将始终有效.不同的线程不可能在该对象中创建无效状态.因此,不可变对象是线程安全的.
垃圾收集:垃圾收集器更容易对不可变对象做出逻辑决策.
但是,不变性也有缺点:
表现:等等,我以为你说性能是不变的好处!嗯,有时候,但并非总是如此.请使用以下代码:
foo = foo.substring(0,4) + "a" + foo.substring(5); // foo is a String bar.replace(4,5,"a"); // bar is a StringBuilder
这两行都用字母"a"替换第四个字符.第二段代码不仅更具可读性,而且速度更快.看看你将如何为foo做底层代码.子串很容易,但现在因为在第五空间已经有了一个字符而其他东西可能引用了foo,你不能只改变它; 你必须复制整个字符串(当然,这些功能中的一些被抽象为真正底层C中的函数,但这里的重点是显示在一个地方执行的代码).
struct String* concatenate(struct String* first, struct String* second) { struct String* new = malloc(sizeof(struct String)); new->length = first->length + second->length; new->characters = malloc(new->length); int i; for(i = 0; i < first->length; i++) new->characters[i] = first->characters[i]; for(; i - first->length < second->length; i++) new->characters[i] = second->characters[i - first->length]; return new; } // The code that executes struct String* astring; char a = 'a'; astring->characters = &a; astring->length = 1; foo = concatenate(concatenate(slice(foo,0,4),astring),slice(foo,5,foo->length));
请注意,连接被调用两次意味着整个字符串必须循环通过!将其与操作的C代码进行比较bar
:
bar->characters[4] = 'a';
可变字符串操作显然要快得多.
结论:在大多数情况下,您需要一个不可变的字符串.但是如果你需要做很多追加和插入字符串,你需要速度的可变性.如果您希望并发安全和垃圾收集带来好处,关键是将可变对象保持在方法的本地:
// This will have awful performance if you don't use mutable strings String join(String[] strings, String separator) { StringBuilder mutable; boolean first = true; for(int i = 0; i < strings.length; i++) { if(!first) first = false; else mutable.append(separator); mutable.append(strings[i]); } return mutable.toString(); }
由于该mutable
对象是本地引用,因此您不必担心并发安全性(只有一个线程会触及它).并且由于它没有在其他任何地方引用,它只在堆栈上分配,所以一旦函数调用完成就会释放它(你不必担心垃圾收集).并且您可以获得可变性和不变性的所有性能优势.
实际上,如果您使用上面建议的维基百科定义,String不是不可变的.
字符串的状态会改变后期构建.看一下hashcode()方法.String将哈希码值缓存在本地字段中,但在第一次调用hashcode()之前不会计算它.这种对hashcode的懒惰评估将String置于一个有趣的位置,作为状态改变的不可变对象,但是如果不使用反射就无法观察到它已被改变.
因此,也许不可变的定义应该是一个无法观察到的变化的对象.
如果状态在创建后变为不可变对象,但没有人可以看到它(没有反射),对象仍然是不可变的吗?
不可变对象是无法以编程方式更改的对象.它们特别适用于多线程环境或其他多个进程能够更改(变异)对象中的值的环境.
然而,只是为了澄清,StringBuilder实际上是一个可变对象,而不是一个不可变对象.常规java String是不可变的(意味着一旦创建它就不能在不更改对象的情况下更改底层字符串).
例如,假设我有一个名为ColoredString的类,它具有String值和String颜色:
public class ColoredString { private String color; private String string; public ColoredString(String color, String string) { this.color = color; this.string = string; } public String getColor() { return this.color; } public String getString() { return this.string; } public void setColor(String newColor) { this.color = newColor; } }
在这个例子中,ColoredString被认为是可变的,因为你可以改变(mutate)它的一个关键属性而不创建一个新的ColoredString类.这可能是坏的原因是,例如,假设您有一个具有多个线程的GUI应用程序,并且您正在使用ColoredStrings将数据打印到窗口.如果你有一个创建为的ColoredString实例
new ColoredString("Blue", "This is a blue string!");
然后你会期望字符串总是"蓝色".但是,如果另一个线程得到了这个实例并且被调用了
blueString.setColor("Red");
当你想要一个"蓝色"字符串时,你会突然,并且可能意外地,现在有一个"红色"字符串.因此,在传递对象实例时,几乎总是首选不可变对象.如果您确实需要可变对象,那么您通常只需要从特定的控制域传递副本来保护对象.
回顾一下,在Java中,java.lang.String是一个不可变对象(一旦创建就无法更改),java.lang.StringBuilder是一个可变对象,因为它可以在不创建新实例的情况下进行更改.
在大型应用程序中,字符串文字通常会占用大量内存.因此,为了有效地处理内存,JVM会分配一个名为"String constant pool"的区域.(请注意,在内存中,即使是未引用的String也会携带char [],其长度为int,另一个为hashCode.相比之下,最多需要八个立即字节)
当complier遇到String字面值时,它会检查池以查看是否存在相同的文字.如果找到一个,则对新文本的引用将定向到现有的String,并且不会创建新的"String literal object"(现有的String只会获得一个额外的引用).
因此:字符串可变性可以节省内存......
但是当任何变量改变值时,实际上 - 它只是它们的引用被改变了,而不是内存中的值(因此它不会影响引用它的其他变量),如下所示....
String s1 ="Old string";
//s1 variable, refers to string in memory reference | MEMORY | variables | | [s1] --------------->| "Old String" |
字符串s2 = s1;
//s2 refers to same string as s1 | | [s1] --------------->| "Old String" | [s2] ------------------------^
s1 ="新字符串";
//s1 deletes reference to old string and points to the newly created one [s1] -----|--------->| "New String" | | | | |~~~~~~~~~X| "Old String" | [s2] ------------------------^
原始字符串'在内存中'没有更改,但引用变量已更改,以便它引用新字符串.如果我们没有s2,"Old String"仍会在内存中,但我们将无法访问它...
"不可变"意味着你无法改变价值.如果你有一个String类的实例,你调用的任何似乎修改该值的方法实际上会创建另一个String.
String foo = "Hello"; foo.substring(3); <-- foo here still has the same value "Hello"
为了保留变化,你应该做这样的事情foo = foo.sustring(3);
使用集合时,不可变对可变性可能很有趣.想想如果你使用可变对象作为地图的关键然后改变价值会发生什么(提示:想想equals
和hashCode
).
它可能有点晚,但为了理解不可变对象是什么,请考虑新Java 8 Date and Time API(java.time)中的以下示例.您可能知道Java 8中的所有日期对象都是不可变的,因此在下面的示例中
LocalDate date = LocalDate.of(2014, 3, 18); date.plusYears(2); System.out.println(date);
输出:
2014年3月18日
这将打印与初始日期相同的年份,因为它plusYears(2)
返回一个新对象,因此旧日期仍然保持不变,因为它是一个不可变对象.一旦创建,您无法进一步修改它,日期变量仍然指向它.
因此,该代码示例应捕获并使用由该调用实例化并返回的新对象plusYears
.
LocalDate date = LocalDate.of(2014, 3, 18); LocalDate dateAfterTwoYears = date.plusYears(2);
date.toString()... 2014-03-18
dateAfterTwoYears.toString()... 2016-03-18
我非常喜欢SCJP Sun认证程序员Java 5学习指南的解释.
为了提高Java的内存效率,JVM预留了一个称为"字符串常量池"的特殊内存区域.当编译器遇到String文本时,它会检查池以查看是否已存在相同的String.如果找到匹配项,则对新文本的引用将定向到现有String,并且不会创建新的String文本对象.
不可变的对象在创建后不能更改其状态.
尽可能使用不可变对象有三个主要原因,所有这些都有助于减少您在代码中引入的错误数量:
当您知道对象的状态不能被其他方法更改时,更容易推断您的程序如何工作
不可变对象是自动线程安全的(假设它们是安全发布的),所以永远不会成为那些难以确定的多线程错误的原因
不可变对象将始终具有相同的哈希代码,因此它们可以用作HashMap(或类似)中的键.如果要改变哈希表中元素的哈希码,那么表条目将有效地丢失,因为在表中找到它的尝试最终会在错误的位置查找.这是String对象不可变的主要原因 - 它们经常用作HashMap键.
当您知道对象的状态是不可变的时,您可以在代码中进行一些其他优化 - 例如缓存计算的哈希 - 但这些是优化,因此不太有趣.
一个含义与值如何存储在计算机中有关,例如,对于.Net字符串,这意味着内存中的字符串无法更改,当您认为正在更改它时,您实际上正在创建一个新的字符串在内存中并将现有变量(它只是指向其他地方的实际字符集合的指针)指向新字符串.