不可变的是什么意思?

问题

这可能是有史以来最愚蠢的问题,但我认为这对新手来说完全是混乱。

  • 有人可以澄清不可变的含义吗?
  • 为什么字符串是不可变的?
  • 不可变对象有哪些优点/缺点?
  • 为什么像StringBuilder这样的可变对象比String更受欢迎?

一个很好的例子(在Java中)将非常感激。


#1 热门回答(230 赞)

不可变意味着一旦对象的构造函数完成执行,该实例就无法更改。

这很有用,因为它意味着你可以传递对象的引用,而不用担心其他人会改变它的内容。特别是在处理并发时,对于永不改变的对象没有锁定问题

例如

class Foo
{
     private final String myvar;

     public Foo(final String initialValue)
     {
         this.myvar = initialValue;
     }

     public String getValue()
     {
         return this.myvar;
     }
}

Foo不必担心调用者togetValue()可能会更改字符串中的文本。

如果你想象一个类似于Foo的类,但是a35358211不是aString的成员,你可以看到一个调用者togetValue()将能够改变aFoo实例的StringBuilder属性。

还要注意你可能会发现的不同类型的不变性:Eric Lippert写了一篇关于这个的17373710。基本上你可以拥有其接口是不可变的但在幕后实际可变的私有状态的对象(因此不能在线程之间安全地共享)。


#2 热门回答(76 赞)

不可变对象是无法更改内部字段(或至少影响其外部行为的所有内部字段)的对象。

不可变字符串有很多优点:

**性能:**进行以下操作:

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));

请注意,连接被称为两次意味着整个字符串必须循环通过!将其与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对象是本地引用,因此你不必担心并发安全性(只有一个线程触及它)。并且由于它没有在其他任何地方引用,它只在堆栈上分配,所以一旦函数调用完成就会释放它(你不必担心垃圾收集)。并且你可以获得可变性和不变性的所有性能优势。


#3 热门回答(30 赞)

实际上,如果你使用上面建议的维基百科定义,String不是不可变的。

字符串的状态会改变后期构建。看一下hashcode()方法。 String将哈希码值缓存在本地字段中,但在第一次调用hashcode()之前不会计算它。这种对哈希码的懒惰评估将String置于一个有趣的位置,作为状态改变的不可变对象,但是如果不使用反射就无法观察到它的变化。

所以也许不可变的定义应该是一个无法观察到的变化的对象。

如果状态在创建之后在不可变对象中发生变化但是没有人能看到它(没有反射),那么对象仍然是不可变的?