不可变的意思是什么?

时间:2008-11-10 23:05:26

标签: java string immutability

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

  1. 有人可以澄清 immutable 的含义吗?
  2. 为什么String不可变?
  3. 不可变对象有哪些优点/缺点?
  4. 为什么像StringBuilder这样的可变对象优先于String而不是反之?
  5. 一个很好的例子(在Java中)将非常感激。

18 个答案:

答案 0 :(得分:254)

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

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

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()的来电者是能够更改StringBuilder实例的Foo属性。

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

答案 1 :(得分:79)

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

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

效果:执行以下操作:

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对象是可变的(字符可能会在以后更改),那么您必须复制所有字符,否则更改为substring稍后会反映在另一个字符串中。

并发:如果不可变对象的内部结构有效,它将始终有效。不同的线程不可能在该对象中创建无效状态。因此,不可变对象是线程安全

垃圾收集:垃圾收集器更容易对不可变对象做出逻辑决策。

然而,不变性也存在缺点:

表现:等等,我以为你说性能是不变的好处!嗯,有时候,但并非总是如此。请使用以下代码:

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

答案 2 :(得分:30)

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

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

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

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

答案 3 :(得分:24)

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

然而,为了澄清,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被认为是可变的,因为您可以更改(mutate)其中一个键属性,而无需创建新的ColoredString类。这可能是坏的原因是,例如,假设您有一个具有多个线程的GUI应用程序,并且您正在使用ColoredStrings将数据打印到窗口。如果你有一个创建为

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

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

blueString.setColor("Red");

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

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

答案 4 :(得分:23)

  1. 在大型应用程序中,字符串文字常见的是占用大量内存。因此,为了有效地处理内存,JVM会分配一个名为“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
  2. 当编译器遇到字符串文字时,它会检查池以查看是否存在相同的字面值。如果找到一个,则对新文本的引用将定向到现有的String,并且不会创建新的“String literal object”(现有的String只会获得一个额外的引用)。
  3. 因此:字符串可变性可以节省内存......
  4. 但是当任何变量改变值时,实际上 - 只有它们的引用被改变了,而不是内存中的值(因此它不会影响引用它的其他变量),如下所示....
  5. String s1 =“Old string”;

    //s1 variable, refers to string in memory
            reference                 |     MEMORY       |
            variables                 |                  |
    
               [s1]   --------------->|   "Old String"   |
    

    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]   ------------------------^
    
      

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

答案 5 :(得分:16)

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

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

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

使用集合时,Immutable vs mutable可能很有趣。想想如果你使用可变对象作为地图的关键然后改变价值会发生什么(提示:想想equalshashCode)。

答案 6 :(得分:10)

java.time

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

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

输出:

  

2014年3月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

答案 7 :(得分:8)

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

  

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

答案 8 :(得分:8)

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

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

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

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

答案 9 :(得分:5)

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

答案 10 :(得分:4)

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”创建一个新的String对象值和s1引用它。这里当我们打印s2值时,结果将是“Hi”而不是“Bye”,因为s2引用了之前具有“Hi”值的s1对象。

答案 11 :(得分:3)

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

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

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

答案 12 :(得分:3)

不可更改仅表示不可更改或不可更改。创建字符串对象后,其数据或状态就无法更改

考虑下面的例子,

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  
  }  
 }  

让我们考虑一下波纹管图,

enter image description here

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

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

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

  

因为Java使用字符串文字的概念。假设有5个参考变量,所有参考变量都引用一个对象“ Future”。如果一个参考变量更改了该对象的值,则将影响所有参考变量。这就是为什么字符串对象在Java中是不可变的。

答案 13 :(得分:2)

一旦实现,就无法改变。考虑一个类,该实例可以用作哈希表或类似的键。查看Java最佳实践。

答案 14 :(得分:1)

因为接受的答案无法回答所有问题。 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()调用要求最终的String时,它使用简单但聪明的内部数据结构来避免进行任何复制直到最后:

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

答案 15 :(得分:0)

不可变对象

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

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

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

以下小节介绍了一个实例可变的类,并从中派生出一个具有不可变实例的类。通过这样做,它们为这种转换提供了一般规则,并展示了不可变对象的一些优点。

Source

答案 16 :(得分:0)

oracle docs says

  

如果某个对象的状态在其后无法更改,则该对象被视为不可变   是建造的。最大程度上依赖于不可变对象   被认为是创建简单,可靠代码的合理策略。

     

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

我喜欢post

中的这句话
  

不可变对象简化了并发编程

答案 17 :(得分:-1)

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

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