这可能是有史以来最愚蠢的问题,但我认为这对于 Java 新手来说是相当混乱的。
- 有人可以澄清什么是不可变的吗?
- 为什么是
String
不可变的? - 不可变对象的优点/缺点是什么?
- 为什么像 String 这样的可变对象
StringBuilder
应该优先于 String ,反之亦然?
一个很好的例子(在Java中)将不胜感激。
这可能是有史以来最愚蠢的问题,但我认为这对于 Java 新手来说是相当混乱的。
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
,但使用 aStringBuilder
而不是 aString
作为成员,您可以看到调用者 togetValue()
将能够更改实例的StringBuilder
属性。Foo
还要注意您可能会发现的不同类型的不变性: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 做底层代码。子字符串很简单,但是现在因为在空格 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
对象是本地引用,因此您不必担心并发安全性(只有一个线程接触过它)。而且由于它没有在其他任何地方引用,它只在堆栈上分配,所以一旦函数调用完成它就会被释放(你不必担心垃圾收集)。您可以获得可变性和不变性的所有性能优势。
如果您使用上面建议的维基百科定义,实际上 String 不是不可变的。
字符串的状态确实会改变构建后的状态。看一下 hashcode() 方法。String 将 hashcode 值缓存在本地字段中,但直到第一次调用 hashcode() 时才计算它。这种对哈希码的惰性求值将 String 作为一个状态发生变化的不可变对象置于一个有趣的位置,但如果不使用反射就无法观察到它发生了变化。
所以也许不可变的定义应该是一个不能被观察到已经改变的对象。
如果一个不可变对象在创建后状态发生变化,但没有人可以看到它(没有反射),那么该对象仍然是不可变的吗?
不可变对象是不能以编程方式更改的对象。它们特别适用于多线程环境或其他多个进程能够更改(变异)对象中的值的环境。
然而,澄清一下,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 是一个可变对象,因为它可以在不创建新实例的情况下更改。
字符串 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,“旧字符串”仍会在内存中,但我们将无法访问它......
“不可变”意味着你不能改变价值。如果您有一个 String 类的实例,那么您调用的任何似乎修改值的方法实际上都会创建另一个 String。
String foo = "Hello";
foo.substring(3);
<-- foo here still has the same value "Hello"
要保留更改,您应该执行以下操作 foo = foo.sustring(3);
当您使用集合时,不可变与可变可能会很有趣。想想如果你使用可变对象作为 map 的键然后改变值会发生什么(提示:想想equals
and hashCode
)。
可能有点晚了,但为了理解什么是不可变对象,请考虑以下来自新 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
我真的很喜欢SCJP Sun Certified Programmer for Java 5 Study Guide的解释。
为了提高 Java 的内存效率,JVM 预留了一个特殊的内存区域,称为“字符串常量池”。当编译器遇到字符串字面量时,它会检查池以查看是否已经存在相同的字符串。如果找到匹配项,则对新文字的引用将定向到现有字符串,并且不会创建新的字符串文字对象。
不可变对象在创建后不能更改其状态。
尽可能使用不可变对象有三个主要原因,所有这些都将有助于减少您在代码中引入的错误数量:
当您知道对象的状态是不可变的时,您还可以在代码中进行其他一些优化——例如缓存计算的哈希值——但这些都是优化,因此几乎没有那么有趣。
一个含义与值如何存储在计算机中有关,例如.Net字符串,这意味着内存中的字符串无法更改,当您认为要更改它时,实际上是在创建一个新的内存中的字符串并将现有变量(它只是指向其他地方的实际字符集合的指针)指向新字符串。
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 类型是不可变类型,而是编译器创建一个具有“Bye”值的新 String 对象并s1
引用它。在这里,当我们打印s2
值时,结果将是“Hi”而不是“Bye”,因为s2
引用了s1
具有“Hi”值的先前对象。
不可变意味着一旦对象被创建,它的所有成员都不会改变。String
是不可变的,因为您无法更改其内容。例如:
String s1 = " abc ";
String s2 = s1.trim();
在上面的代码中,字符串 s1 没有改变,另一个对象 ( s2
) 是使用s1
.
不可变只是意味着不可更改或不可修改。一旦创建了字符串对象,它的数据或状态就不能改变
考虑下面的例子,
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 immutable
. s
,仍指“未来”。如果你需要调用“未来世界”,
String s="Future";
s=s.concat(" World");
System.out.println(s);//print Future World
为什么字符串对象在java中是不可变的?
因为Java使用了字符串字面量的概念。假设有5个引用变量,都指向一个对象“Future”。如果一个引用变量改变了对象的值,就会影响到所有的引用变量。这就是为什么字符串对象在 java 中是不可变的。
一经实例化,不可更改。考虑一个类,它的实例可能用作哈希表或类似的键。查看 Java 最佳实践。
由于接受的答案并不能回答所有问题。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
不可变对象
如果一个对象在构造后它的状态不能改变,那么它就被认为是不可变的。最大限度地依赖不可变对象被广泛认为是创建简单、可靠代码的合理策略。
不可变对象在并发应用程序中特别有用。由于它们无法更改状态,因此它们不会被线程干扰破坏或观察到不一致的状态。
程序员通常不愿意使用不可变对象,因为他们担心创建新对象而不是就地更新对象的成本。对象创建的影响通常被高估,并且可以被与不可变对象相关的一些效率所抵消。这些包括由于垃圾收集而减少的开销,以及消除保护可变对象免受损坏所需的代码。
以下小节采用实例可变的类,并从中派生具有不可变实例的类。通过这样做,他们给出了这种转换的一般规则,并展示了不可变对象的一些优点。
不可变对象是在创建后无法修改的对象。一个典型的例子是字符串文字。
越来越流行的AD编程语言通过“invariant”关键字具有“不变性”的概念。查看 Dr.Dobb 的这篇文章 - http://dobbscodetalk.com/index.php?option=com_myblog&show=Invariant-Strings.html&Itemid=29。它完美地解释了这个问题。