这可能是有史以来最愚蠢的问题,但我认为对于Java新手来说,这非常令人困惑。


有人可以澄清什么是不变的吗?
为什么String是不可变的?
不可变对象的优点/缺点是什么?
为什么像StringBuilder这样的可变对象比String和相反的对象更受青睐?

我们将不胜感激一个不错的示例(使用Java)。

评论

瞧,这不是一个愚蠢的问题。很高兴你问!

顺便说一句,我不认为这是有史以来最愚蠢的问题:)我认为这是理解一个非常重要的概念

当您说StringBuilder时,您不是说可变的类StringBuffer吗? String和StringBuffer在功能上比String和StringBuilder更相似。 StringBuffer实际上是可变字符串。

我可以建议我们在这个问题上添加“ beginner”标签,以便对Java陌生的程序员可以在搜索其他入门问题时找到它?

stackoverflow.com/questions/2971315/…

#1 楼

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

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

例如

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属性。

还要当心您可能会发现的各种不变性:Eric Lippert撰写了一篇有关此的博客文章。基本上,您可以拥有其接口是不可变的对象,但在幕后是实际的可变私有状态(因此不能在线程之间安全地共享)。

评论


我认为您应该添加一个arg构造函数以至少分配一次值。当前代码的重点尚不清楚,因为实际上没有任何值可以更改:)。

–乔治·博尤巴(Georgy Bolyuba)
08年10月10日在23:21

您应该将该字段设置为只读。它完全明确表明该字段是不可变的。现在,按照惯例它是不可变的

– JaredPar
08年11月11日,0:15

myVar成员应该是最终决定,这才是真正不变的。

– laz
2009年1月9日在21:16

您是正确的,在Foo之外无法访问myVar。但是,final的存在向将来可能要修改该类的任何人表明,其值并不意味着要改变。在这种情况下,我倾向于尽可能明确。

– laz
09年1月12日在21:41

“仅使用final关键字不能使引用类型不可变。final仅防止重新分配。”来自en.wikipedia.org/wiki/Immutable_object

–优莎·阿里尤布
15年4月17日在17:08

#2 楼

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

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

性能:执行以下操作:

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


注意,连接被调用了两次,这意味着整个字符串必须循环通过!将此与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)

–悉达多
2012年10月27日下午4:50

必需的不是字段是不可变的,而是对象定义的可观察状态是不可变的。如果一个对象引用另一个对象作为封装其中包含的状态的一种手段,则只有当它暴露给外界的所有状态的封装方面都是不可变的时,该对象才是不可变的。请注意,字段是不可变的类型既不必要也不充分。重要的是可见状态。

–超级猫
2014年1月6日在21:19

因为Java是传递引用而传递指针Java不是“传递值”吗?

–克里斯蒂安·古图(Cristian Gutu)
14-10-2在1:31



@CristianGutu是的,您是对的JAVA是“按值传递”而不是“按引用传递”

– Arsh Kaushal
17年5月16日在7:15

引用作为值传递!

– devv
19年1月15日在1:47

#3 楼

实际上,如果您使用上面建议的Wikipedia定义,则String并非一成不变的。

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

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

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

评论


好主意-无法观察到已更改的对象,也无法从外部对其进行更改。 hashCode()的私有字段是内部更改,对对象的外部可见状态不重要。

–mparaz
09年2月22日在15:55

实际上,如果使用反射,可以观察到它已发生变化。如果允许反射,请参阅Sedgewick的《字符串可变》。

–米格尔
13年7月4日在8:42

#4 楼

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

请澄清一下,StringBuilder实际上是可变的对象,而不是一成不变的对象。常规的Java字符串是不可变的(这意味着一旦创建它,​​就不能在不更改对象的情况下更改基础字符串)。例如,假设我有一个名为ColoredString的类,该类具有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据说是可变的,因为您可以更改(变异)其Key属性之一而无需创建新的ColoredString类。例如,假设您有一个GUI应用程序具有多个线程,并且您正在使用ColoredStrings将数据打印到窗口中,那么这可能是不好的原因。如果您有一个ColoredString实例创建为

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


,那么您希望该字符串始终为“蓝色”。但是,如果另一个线程抓住了该实例并调用了

blueString.setColor("Red");


现在,当您想使用“蓝色的。因此,在传递对象实例时,几乎总是首选不可变对象。如果确实需要可变对象,那么通常只通过从特定控制区域传递副本来保护对象。

在Java中,java.lang.String概括地说。是一个不可变的对象(创建后便无法更改),而java.lang.StringBuilder是一个可变对象,因为可以在不创建新实例的情况下对其进行更改。

评论


您应该将字段设置为只读。现在,按照惯例,您的课程是不可变的。没有迹象表明未来的开发人员是刻不容缓的。将字段设置为只读将有助于阐明您对未来开发人员的意图

– JaredPar
08年11月11日,0:16

@JaredPar-实际上,该类根本不是一成不变的……这是一个可变类的示例,以说明为什么可能是一个问题。

–Jason Coco
08年11月11日在1:04

@JaredPar-哦,那完全没关系:)我要重写一点以便更加清楚,但是Douglas的写得很好,似乎是最喜欢的,所以我只剩下我的一个例子。但实际上有人确实对其进行了编辑,以使属性最终确定,我认为这很有趣:)

–Jason Coco
08年11月11日在2:52

#5 楼


在大型应用程序中,字符串文字通常占用大量内存。因此,为了有效地处理内存,JVM分配了一个称为“字符串常量池”的区域。(请注意,即使在内存中,即使未引用的String也会携带char [],其长度为int以及其hashCode为另一个。 ,相比之下,最多需要八个立即字节。
当编译器遇到String文字时,它将检查池以查看是否已经存在相同的文字。并且如果找到一个,则对新文字的引用将定向到现有的String,并且不会创建新的“ String文字对象”(现有的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内存没有变化,但
参考变量已更改,因此它引用了新字符串。
如果没有s2,则“旧字符串”仍将存在于内存中,但是
我们将无法访问它...


#6 楼

“不变”表示您无法更改值。如果您有String类的实例,则您调用的任何似乎修改值的方法实际上都会创建另一个String。

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


要保留更改,您应该执行一些操作像这样
foo = foo.sustring(3);

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

#7 楼

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- 03-18


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

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

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



date.toString()…2014-03-18

dateAfterTwoYears.toString()…2016-03-18


#8 楼

我真的很喜欢SCJP Sun Java 5程序员认证学习指南中的解释。


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


评论


它应该能够对任何相同的不可变对象执行此操作,但是我想那会花费太多的运行时间。

–赞·山猫
08年11月11日3:00

#9 楼

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

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


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

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

#10 楼

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

#11 楼

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:创建的对象s2参考了s1对象。

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

评论


你能补充一点解释吗?

– minigeek
17年6月1日在7:44

#12 楼

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

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


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

#13 楼

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

考虑以下示例,

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中是不可变的。


#14 楼

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

#15 楼

由于接受的答案无法回答所有问题。我被迫在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

#16 楼

不可变的对象

如果对象的状态在构造后无法更改,则认为该对象是不可变的。作为创建简单,可靠的代码的合理策略,最大程度地依赖不可变对象已得到广泛接受。

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

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

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

Source

#17 楼

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

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

评论


我确实相信,自D 2.020起,关键字已从不变更改为不变。我看不到要点,但它确实说“现在实现了不可变的”。 digitalmars.com/d/2.0/changelog.html#new2_020

–he_the_great
08年11月11日,0:50