什么是不可变的?

这可能是问过的最愚蠢的问题,但我认为对于Java新手来说,这是相当令人困惑的。

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

一个很好的例子(在Java中)将非常感谢。

187821 次浏览

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

不可变意味着一旦对象被创建,它的成员都不会改变。String是不可变的,因为你不能改变它的内容。 例如:< / p >

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

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

“不可变”意味着你不能改变值。如果你有一个String类的实例,你调用的任何方法,只要看起来是在修改这个值,实际上都会创建另一个String。

String foo = "Hello";
foo.substring(3);
<-- foo here still has the same value "Hello"
要保存更改,您应该这样做 Foo = Foo .sustring(3);

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

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

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

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

如。

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是一个可变对象,因为它可以在不创建新实例的情况下被改变。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

性能:执行以下操作:

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

  1. 在大型应用程序中,字符串字面值通常会占用大量内存。因此,为了有效地处理内存,JVM分配了一个名为“字符串常量池”的区域。
  2. 当编译器遇到一个String字面值时,它会检查池,看看是否有一个相同的字面值已经存在。如果找到一个,对新字面值的引用将指向现有的String,并且不会创建新的'String字面值对象'(现有的String只是获得一个额外的引用)。
  3. 因此:字符串可变性节省内存…
  4. 但是当任何变量改变值时,实际上-只是它们的引用被改变了,而不是内存中的值(因此它不会影响引用它的其他变量),如下所示....

字符串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仍然在内存中,但是 我们将无法访问它…

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

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对象。

不可变对象

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

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

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

下面的子节采用一个实例是可变的类,并从中派生一个具有不可变实例的类。在这样做的过程中,他们给出了这种转换的一般规则,并演示了不可变对象的一些优点。

Source .

不可改变的意思是不可改变的。字符串对象一旦创建,其数据或状态就不能更改

考虑下面的例子:

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

在这个图中,你可以看到一个新对象被创建为“Future World”。但不改变"Future". __abc0。s,仍然指“未来”。如果你需要给“未来世界”打电话,

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

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

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

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

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

希望你指的是不可变的对象(因为我们可以考虑不可变的参考)。

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

为什么String是不可变的?

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

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

  • 更安全。

  • 更容易理解。

  • 和更多的准备改变。

为什么像StringBuilder这样的可变对象应该优先于String,反之亦然?

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

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

使用不可变字符串,这将产生大量临时拷贝——在构建最终字符串的过程中,字符串的第一个数字("0")实际上被复制n次,第二个数字被复制n-1次,以此类推。实际上,做所有这些复制需要花费O(n2)时间,尽管我们只连接了n个元素。

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