什么是不可改变的?

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

  • 有人可以澄清不变的含义吗?
  • 为什么String不可变?
  • 不可变对象有什么优点/缺点?
  • 为什么像StringBuilder这样的可变对象比String和副诗更受欢迎?
  • 一个很好的例子(在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()的调用方将能够更改Foo实例的StringBuilder属性。

    还要注意你可能会发现的不同类型的不变性: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));
    

    请注意,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对象是本地引用,因此不必担心并发安全性(只有一个线程触及它)。 由于它在其他地方没有被引用,因此它只在堆栈中分配,所以只要函数调用完成(您不必担心垃圾收集),它就会被释放。 你可以获得可变性和不变性的所有性能优势。


    实际上,如果您使用上面提到的维基百科定义,则字符串不是不可变的。

    字符串的状态确实会改变后期制作。 看看hashcode()方法。 字符串将哈希码值存储在本地字段中,但在第一次调用hashcode()之前不计算它。 对散列码的懒惰评估将String放置在一个有趣的位置,作为一个状态发生改变的不可变对象,但是如果不使用反射,它就不能被观察到。

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

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

    链接地址: http://www.djcxy.com/p/73629.html

    上一篇: What is meant by immutable?

    下一篇: Java compile time error in case of casting