不可变到底是什么意思——也就是说,对象可变或不可变的结果是什么?特别是,为什么Java的字符串是不可变的?

我的理解是StringBuilder类型类似于String的可变等价。什么时候我会使用StringBuilder而不是字符串,反之亦然?


当前回答

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

字符串的状态改变后构造。看一下hashcode()方法。String将hashcode值缓存在本地字段中,但直到第一次调用hashcode()才计算它。这种对hashcode的惰性求值将String置于一个有趣的位置,作为状态发生变化的不可变对象,但如果不使用反射,就无法观察到它发生了变化。

所以也许不可变的定义应该是一个不能被观察到已经改变的对象。

如果一个不可变对象在创建后状态发生了变化,但是没有人可以看到它(没有反射),这个对象仍然是不可变的吗?

其他回答

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

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

e.g.

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()的调用者能够改变Foo实例的StringBuilder属性。

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

不可变对象是不能通过编程改变的对象。它们特别适用于多线程环境或其他多个进程能够更改(突变)对象中的值的环境。

不过,澄清一下,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被认为是可变的,因为您可以在不创建新的ColoredString类的情况下更改(突变)它的一个关键属性。这可能很糟糕的原因是,例如,假设您有一个GUI应用程序,它有多个线程,并且您正在使用ColoredStrings将数据打印到窗口。如果你有一个ColoredString的实例,它被创建为

new ColoredString("Blue", "This is a blue string!");

然后你会期望字符串总是“Blue”。然而,如果另一个线程获得了这个实例并调用

blueString.setColor("Red");

当你想要一个“蓝色”的字符串时,你会突然,很可能出乎意料地得到一个“红色”的字符串。正因为如此,在传递对象实例时,几乎总是首选不可变对象。在确实需要可变对象的情况下,通常只需从特定的控制字段传递副本来保护对象。

概括一下,在Java中,Java .lang. string是一个不可变对象(一旦创建就不能更改),而Java .lang. stringbuilder是一个可变对象,因为它可以在不创建新实例的情况下进行更改。

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

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

性能:执行如下操作:

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

The two lines both replace the fourth character with the letter "a". Not only is the second piece of code more readable, it's faster. Look at how you would have to do the underlying code for foo. The substrings are easy, but now because there's already a character at space five and something else might be referencing foo, you can't just change it; you have to copy the whole string (of course some of this functionality is abstracted into functions in the real underlying C, but the point here is to show the code that gets executed all in one place).

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

因为可变对象是一个本地引用,所以不必担心并发安全性(只有一个线程接触过它)。由于它没有在其他任何地方被引用,所以它只在堆栈上分配,所以函数调用一结束它就会被释放(您不必担心垃圾收集)。你可以同时获得可变性和不可变性的性能优势。

不可变的对象在创建后不能改变其状态。

尽可能使用不可变对象有三个主要原因,所有这些都将有助于减少你在代码中引入的错误数量:

It is much easier to reason about how your program works when you know that an object's state cannot be changed by another method Immutable objects are automatically thread safe (assuming they are published safely) so will never be the cause of those hard-to-pin-down multithreading bugs Immutable objects will always have the same Hash code, so they can be used as the keys in a HashMap (or similar). If the hash code of an element in a hash table was to change, the table entry would then effectively be lost, since attempts to find it in the table would end up looking in the wrong place. This is the main reason that String objects are immutable - they are frequently used as HashMap keys.

当你知道一个对象的状态是不可变的时,你还可以在代码中做一些其他的优化——例如缓存计算的哈希——但这些都是优化,因此没有那么有趣。

In large applications its common for string literals to occupy large bits of memory. So to efficiently handle the memory, the JVM allocates an area called "String constant pool".(Note that in memory even an unreferenced String carries around a char[], an int for its length, and another for its hashCode. For a number, by contrast, a maximum of eight immediate bytes is required) When complier comes across a String literal it checks the pool to see if there is an identical literal already present. And if one is found, the reference to the new literal is directed to the existing String, and no new 'String literal object' is created(the existing String simply gets an additional reference). Hence : String mutability saves memory... But when any of the variables change value, Actually - it's only their reference that's changed, not the value in memory(hence it will not affect the other variables referencing it) as seen below....


字符串s1 = "旧字符串";

//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 = "New String";

//s1 deletes reference to old string and points to the newly created one
           [s1]   -----|--------->|   "New String"   |
                       |          |                  |
                       |~~~~~~~~~X|   "Old String"   |
           [s2]   ------------------------^

原来的字符串'in memory'没有改变,但是 引用变量已被更改,以便它引用新字符串。 如果我们没有s2, Old String仍然在内存中,但是 我们无法访问它…