【问题标题】:What is meant by immutable?不可变是什么意思?
【发布时间】:2010-09-21 17:24:54
【问题描述】:

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

  1. 谁能解释一下不可变是什么意思?
  2. 为什么String 是不可变的?
  3. 不可变对象的优点/缺点是什么?
  4. 为什么像StringBuilder 这样的可变对象应该优先于字符串,反之亦然?

一个很好的例子(Java)将不胜感激。

【问题讨论】:

  • 看,这不是一个愚蠢的问题。很高兴你问!
  • 顺便说一句,我不认为这是有史以来最愚蠢的问题 :) 我认为这是一个非常重要的概念,需要理解
  • 你说的StringBuilder,不是指可变类StringBuffer吗? String 和 StringBuffer 在功能上比 String 和 StringBuilder 更相似。 StringBuffer 实际上是一个可变字符串。
  • 我可以建议我们在这个问题上添加“初学者”标签,以便 Java 新手可以在搜索其他介绍性问题时找到它?

标签: java string immutability


【解决方案1】:

一旦实例化,就无法更改。考虑一个类,其实例可能用作哈希表或类似的键。查看 Java 最佳实践。

【讨论】:

    【解决方案2】:

    不可变意味着一旦对象被创建,它的任何成员都不会改变。 String 是不可变的,因为您无法更改其内容。 例如:

    String s1 = "  abc  ";
    String s2 = s1.trim();
    

    在上面的代码中,字符串 s1 没有改变,另一个对象 (s2) 是使用 s1 创建的。

    【讨论】:

      【解决方案3】:

      “不可变”意味着你不能改变价值。如果你有一个 String 类的实例,你调用的任何似乎修改值的方法实际上都会创建另一个 String。

      String foo = "Hello";
      foo.substring(3);
      <-- foo here still has the same value "Hello"
      

      要保留更改,您应该执行以下操作 foo = foo.sustring(3);

      当您使用集合时,不可变与可变可能会很有趣。想想如果你使用可变对象作为 map 的键然后更改值会发生什么(提示:想想 equalshashCode)。

      【讨论】:

        【解决方案4】:

        一个含义与值如何存储在计算机中有关,例如.Net字符串,它意味着内存中的字符串无法更改,当您认为您正在更改它时,您实际上是在内存中创建一个新字符串并将现有变量(它只是指向其他地方的实际字符集合的指针)指向新字符串。

        【讨论】:

          【解决方案5】:

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

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

          例如

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

          还要注意您可能会发现的不同类型的不变性:Eric Lippert 就此写了一封 blog article。基本上你可以拥有接口不可变但在幕后实际可变私有状态的对象(因此不能在线程之间安全共享)。

          【讨论】:

          • 我认为您应该添加一个单参数构造函数来至少分配一次值。当前代码的要点尚不清楚,因为没有真正改变的价值:)。
          • 您应该将该字段设为只读。它完全明确地表明该字段是不可变的。现在它按照惯例是不可变的
          • 成员 myVar 应该是最终的,这样才能真正不可变。
          • 您是正确的,myVar 在 Foo 之外无法访问。然而,final 的存在向任何可能在未来修改类的人表明它的值不会改变。在这种情况下,我倾向于尽可能明确。
          • “引用类型不能仅仅通过使用 final 关键字就变得不可变。final 只能防止重新分配。”来自en.wikipedia.org/wiki/Immutable_object
          【解决方案6】:

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

          只是为了澄清一下,StringBuilder 实际上是一个可变对象,而不是不可变对象。一个普通的 java String 是不可变的(意味着一旦它被创建,你就不能在不改变对象的情况下改变底层字符串)。

          例如,假设我有一个名为 ColoredString 的类,它有一个字符串值和一个字符串颜色:

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

          那么你会期望字符串总是“蓝色”。然而,如果另一个线程得到了这个实例并调用了

          blueString.setColor("Red");
          

          当您想要一个“蓝色”字符串时,您会突然(并且可能出乎意料地)现在有一个“红色”字符串。因此,在传递对象实例时,几乎总是首选不可变对象。当您遇到真正需要可变对象的情况时,您通常会通过仅从您的特定控制领域传递副本来保护对象。

          回顾一下,在 Java 中,java.lang.String 是一个不可变对象(它不能一旦创建就可以更改),而 java.lang.StringBuilder 是一个可变对象,因为它可以在不更改的情况下更改创建一个新实例。

          【讨论】:

          • 您应该将这些字段设为只读。现在你的类按照惯例是不可变的。没有迹象表明未来的开发人员是故意的。将这些字段设为只读将有助于阐明您对未来开发人员的意图
          • @JaredPar - 实际上,该类根本不是不可变的......它是一个可变类的示例来说明为什么它可能是一个问题。
          • @JaredPar - 哦,那完全没问题 :) 我打算重写一下以更清楚,但道格拉斯的已经写得很好而且似乎是最喜欢的,所以我就留下我的作为另一个例子;但实际上有人确实对其进行了编辑以使属性最终成为我认为很有趣的:)
          【解决方案7】:

          我真的很喜欢SCJP Sun Certified Programmer for Java 5 Study Guide的解释。

          为了提高 Java 的内存效率,JVM 预留了一个特殊的内存区域,称为“字符串常量池”。当编译器遇到字符串文字时,它会检查池以查看是否已经存在相同的字符串。如果找到匹配项,则对新文字的引用将定向到现有字符串,并且不会创建新的字符串文字对象。

          【讨论】:

          • 它应该能够对任何相同的不可变对象执行此操作,但我认为这会花费太多运行时间。
          【解决方案8】:

          不可变对象是在创建后无法修改的对象。一个典型的例子是字符串字面量。

          越来越流行的 D 编程语言通过“invariant”关键字具有“不变性”的概念。查看 Dobb 博士关于它的文章 - http://dobbscodetalk.com/index.php?option=com_myblog&show=Invariant-Strings.html&Itemid=29。它完美地解释了这个问题。

          【讨论】:

          【解决方案9】:

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

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

          • 当您知道对象的状态不能被其他方法更改时,更容易推断您的程序是如何工作的
          • 不可变对象是自动线程安全的(假设它们是安全发布的),因此永远不会成为那些难以确定的多线程错误的原因
          • 不可变对象将始终具有相同的哈希码,因此它们可以用作 HashMap(或类似)中的键。如果哈希表中某个元素的哈希码发生变化,那么表项实际上会丢失,因为在表中查找它的尝试最终会在错误的位置查找。这是 String 对象不可变的主要原因 - 它们经常用作 HashMap 键。

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

          【讨论】:

            【解决方案10】:

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

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

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

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

            【讨论】:

            • 好主意 - 一个无法观察到已更改的对象,也无法从外部更改它。 hashCode() 的私有字段是一个内部更改,对对象的外部可见状态不重要。
            • 实际上,如果您使用反射,它可以观察到发生了变化。在 Sedgewick 的Strings are mutable if you allow reflection 上查看更多信息。
            【解决方案11】:

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

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

            性能:进行如下操作:

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

            【讨论】:

            • 伟大的阅读!我认为只有一件事应该是 if(first) 而不是 if(!first)
            • 需要的不是字段不可变,而是对象定义的可观察状态不可变;一个对象持有对另一个对象的引用作为封装其中包含的状态的一种手段,只有当它暴露给外部世界的所有封装的状态方面同样是不可变的时,它才能是不可变的。请注意,字段是不可变类型既不是必要的,也不是充分的。重要的是可见状态。
            • Passing pointers because Java is pass-by-reference java不是“传值”吗?
            • @CristianGutu 是的,你是对的 JAVA 是“按值传递”而不是“按引用传递”
            • 引用作为值传递!!
            【解决方案12】:
            1. 在大型应用程序中,字符串文字通常会占用大量内存。因此,为了有效地处理内存,JVM 分配了一个称为“字符串常量池”的区域。(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)
            2. 当编译器遇到字符串文字时,它会检查池以查看是否已经存在相同的文字。如果找到一个,则对新文字的引用将定向到现有字符串,并且不会创建新的“字符串文字对象”(现有字符串只是获得额外的引用)。
            3. 因此:字符串可变性可以节省内存...
            4. 但是,当任何变量的值发生变化时,实际上 - 只是它们的引用发生了变化,而不是内存中的值(因此它不会影响引用它的其他变量),如下所示......

            String 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 = "新字符串";

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

            原来的字符串'in memory'没有改变,但是 引用变量已更改,以便它引用新字符串。 如果我们没有 s2,“旧字符串”仍会在内存中,但 我们将无法访问它...

            【讨论】:

              【解决方案13】:

              java.time

              可能有点晚了,但为了理解什么是不可变对象,请考虑以下来自新 Java 8 日期和时间 API (java.time) 的示例。您可能知道 Java 8 中的所有日期对象都是不可变的,因此在以下示例中

              LocalDate date = LocalDate.of(2014, 3, 18); 
              date.plusYears(2);
              System.out.println(date);
              

              输出:

              2014-03-18

              这将打印与初始日期相同的年份,因为plusYears(2) 返回一个新对象,因此旧日期仍然不变,因为它是一个不可变对象。一旦创建,您将无法进一步修改它,并且日期变量仍然指向它。

              因此,该代码示例应捕获并使用通过调用 plusYears 实例化并返回的新对象。

              LocalDate date = LocalDate.of(2014, 3, 18); 
              LocalDate dateAfterTwoYears = date.plusYears(2);
              

              date.toString()…2014-03-18

              dateAfterTwoYears.toString()…2016-03-18

              【讨论】:

                【解决方案14】:
                String s1="Hi";
                String s2=s1;
                s1="Bye";
                
                System.out.println(s2); //Hi  (if String was mutable output would be: Bye)
                System.out.println(s1); //Bye
                

                s1="Hi":创建了一个对象s1,其中包含“Hi”值。

                s2=s1 : 引用 s1 对象创建对象 s2

                s1="Bye" : 之前的 s1 对象的值不会改变,因为 s1 具有 String 类型并且 String 类型是不可变类型,而是编译器创建一个带有“Bye”值和 s1 引用的新 String 对象给它。这里当我们打印s2 值时,结果将是“Hi”而不是“Bye”,因为s2 引用了之前具有“Hi”值的s1 对象。

                【讨论】:

                • 你能补充一点吗?
                【解决方案15】:

                不可变对象

                一个对象被认为是不可变的,如果它的状态在构造后不能改变。最大限度地依赖不可变对象已被广泛认为是创建简单、可靠代码的合理策略。

                不可变对象在并发应用程序中特别有用。由于它们无法更改状态,因此它们不会被线程干扰破坏或观察到不一致的状态。

                程序员通常不愿意使用不可变对象,因为他们担心创建新对象而不是就地更新对象的成本。对象创建的影响通常被高估,并且可以被与不可变对象相关的一些效率所抵消。其中包括由于垃圾收集而减少的开销,以及消除保护可变对象免受损坏所需的代码。

                以下小节采用实例可变的类,并从中派生具有不可变实例的类。通过这样做,他们给出了这种转换的一般规则,并展示了不可变对象的一些优点。

                Source

                【讨论】:

                  【解决方案16】:

                  不可变只是意味着不可更改或不可修改。一旦创建了字符串对象,其数据或状态就无法更改

                  考虑下面的例子,

                  class Testimmutablestring{  
                    public static void main(String args[]){  
                      String s="Future";  
                      s.concat(" World");//concat() method appends the string at the end  
                      System.out.println(s);//will print Future because strings are immutable objects  
                    }  
                   }  
                  

                  让我们考虑一下下面的图表,

                  在此图中,您可以看到创建为“未来世界”的新对象。但不要改变“未来”。Because String is immutables,还是指“未来”。如果需要调用“未来世界”,

                  String s="Future";  
                  s=s.concat(" World");  
                  System.out.println(s);//print Future World
                  

                  为什么字符串对象在 java 中是不可变的?

                  因为 Java 使用字符串字面量的概念。假设有5个引用变量,都指向一个对象“Future”。如果一个引用变量改变了对象的值,就会影响到所有的引用变量。这就是为什么字符串对象在 java 中是不可变的。

                  【讨论】:

                    【解决方案17】:

                    因为接受的答案并不能回答所有问题。 11年零6个月后,我不得不给出答案。

                    有人能解释一下不可变是什么意思吗?

                    希望您的意思是不可变对象(因为我们可以考虑不可变引用)。

                    一个对象是不可变的:如果一旦创建,它们总是代表相同的值(没有任何改变值的方法)。

                    为什么String 是不可变的?

                    尊重上述定义,可以通过查看Sting.java 源代码来检查。

                    不可变对象的优点/缺点是什么? 不可变类型是:

                    • 更安全地远离错误。

                    • 更容易理解。

                    • 为改变做好准备。

                    为什么像 StringBuilder 这样的可变对象比 String 更受欢迎,反之亦然?

                    缩小问题的范围为什么我们在编程中需要可变的 StringBuilder? 它的一个常见用途是将大量字符串连接在一起,如下所示:

                    String s = "";
                    for (int i = 0; i < n; ++i) {
                        s = s + n;
                    }
                    

                    使用不可变字符串,这会产生大量临时副本——字符串的第一个数字(“0”)实际上在构建最终字符串的过程中被复制了 n 次,第二个数字被复制了 n-1 次, 等等。尽管我们只连接了 n 个元素,但实际上只需要 O(n2) 时间来完成所有复制。

                    StringBuilder 旨在最大限度地减少这种复制。它使用一个简单但巧妙的内部数据结构来避免在最后进行任何复制,直到最后,当您通过 toString() 调用请求最终字符串时:

                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < n; ++i) {
                      sb.append(String.valueOf(n));
                    }
                    String s = sb.toString();
                    

                    获得良好的性能是我们使用可变对象的原因之一。另一个是方便的共享:通过共享一个通用的可变数据结构,您的程序的两个部分可以更方便地进行通信。

                    更多可以在这里找到:https://web.mit.edu/6.005/www/fa15/classes/09-immutability/#useful_immutable_types

                    【讨论】:

                      猜你喜欢
                      • 2013-07-24
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多