不可变是什么意思?
这可能是有史以来最愚蠢的问题,但我认为对于Java新手来说,这是非常令人困惑的。
- 有人能澄清不可变的含义吗?
- 为什么是不可变的?
String
- 不可变对象的优点/缺点是什么?
- 为什么可变对象(如 )应该优先于 String 和 viceverse?
StringBuilder
一个很好的例子(在Java中)将非常值得赞赏。
这可能是有史以来最愚蠢的问题,但我认为对于Java新手来说,这是非常令人困惑的。
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()
如果您想象一个与 类似的类,但将 a 而不是 a 作为成员,则可以看到 调用方将能够更改实例的属性。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