这可能是有史以来最愚蠢的问题,但我认为这对于 Java 新手来说非常令人困惑。
- 有人可以澄清什么是 不可变 的吗?
- 为什么
String
是不可变的? - 不可变对象的优点/缺点是什么?
- 为什么像
StringBuilder
这样的可变对象优于 String,反之亦然?
一个很好的例子(在 Java 中)将不胜感激。
原文由 ashokgelal 发布,翻译遵循 CC BY-SA 4.0 许可协议
这可能是有史以来最愚蠢的问题,但我认为这对于 Java 新手来说非常令人困惑。
String
是不可变的?StringBuilder
这样的可变对象优于 String,反之亦然?一个很好的例子(在 Java 中)将不胜感激。
原文由 ashokgelal 发布,翻译遵循 CC BY-SA 4.0 许可协议
不可变对象是内部字段(或至少影响其外部行为的所有内部字段)无法更改的对象。
不可变字符串有很多优点:
性能: 采取以下操作:
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 做底层代码。子串很简单,但是现在因为在空格 5 处已经有一个字符,而其他东西可能引用 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));
请注意,concatenate 被调用 了两次,这意味着必须循环遍历整个字符串!将此与 bar
操作的 C 代码进行比较:
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
对象是本地引用,您不必担心并发安全(只有一个线程接触过它)。由于它没有在其他任何地方被引用,它只分配在堆栈上,所以一旦函数调用完成它就会被释放(你不必担心垃圾收集)。您可以获得可变性和不变性的所有性能优势。
原文由 Imagist 发布,翻译遵循 CC BY-SA 4.0 许可协议
15 回答8.4k 阅读
8 回答6.2k 阅读
1 回答4.1k 阅读✓ 已解决
3 回答2.2k 阅读✓ 已解决
2 回答3.1k 阅读
2 回答3.8k 阅读
3 回答1.7k 阅读✓ 已解决
不可变意味着一旦对象的构造函数完成执行,该实例就无法更改。
这很有用,因为它意味着您可以传递对对象的引用,而不必担心其他人会更改其内容。 特别是在处理并发时,永不改变的对象不存在锁定问题
例如
Foo
不必担心getValue()
的调用者可能会更改字符串中的文本。If you imagine a similar class to
Foo
, but with aStringBuilder
rather than aString
as a member, you can see that a caller togetValue()
将能够改变StringBuilder
Foo
实例的属性。还要注意您可能会发现的不同类型的不变性:Eric Lippert 写了一篇关于此的 博客文章。基本上你可以拥有接口不可变但在幕后实际可变私有状态的对象(因此不能在线程之间安全共享)。