不可变到底是什么意思——也就是说,对象可变或不可变的结果是什么?特别是,为什么Java的字符串是不可变的?
我的理解是StringBuilder类型类似于String的可变等价。什么时候我会使用StringBuilder而不是字符串,反之亦然?
不可变到底是什么意思——也就是说,对象可变或不可变的结果是什么?特别是,为什么Java的字符串是不可变的?
我的理解是StringBuilder类型类似于String的可变等价。什么时候我会使用StringBuilder而不是字符串,反之亦然?
当前回答
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
其他回答
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
其中一个含义与值如何存储在计算机中有关,例如,对于一个。net字符串,它意味着内存中的字符串不能被更改,当你认为你正在更改它时,你实际上是在内存中创建了一个新的字符串,并将现有的变量(这只是一个指向其他地方的实际字符集合的指针)指向新的字符串。
“不可变”意味着你不能改变值。如果你有一个String类的实例,你调用的任何方法,只要看起来是在修改这个值,实际上都会创建另一个String。
String foo = "Hello";
foo.substring(3);
<-- foo here still has the same value "Hello"
要保存更改,您应该这样做 Foo = Foo .sustring(3);
当您使用集合时,不可变与可变可能会很有趣。想想如果使用可变对象作为map的键,然后更改值会发生什么(提示:考虑equals和hashCode)。
不可变对象
一个对象被认为是不可变的,如果它的状态在被构造后不能改变。对于创建简单、可靠的代码,最大程度地依赖于不可变对象是一种被广泛接受的合理策略。
不可变对象在并发应用程序中特别有用。由于它们不能改变状态,因此不会被线程干扰破坏或在不一致的状态下观察到。
程序员通常不愿意使用不可变对象,因为他们担心创建新对象的成本,而不是在适当的地方更新对象。对象创建的影响经常被高估,并且可以被一些与不可变对象相关的效率所抵消。这包括减少垃圾收集带来的开销,以及消除保护可变对象不受损坏所需的代码。
下面的子节采用一个实例是可变的类,并从中派生一个具有不可变实例的类。在这样做的过程中,他们给出了这种转换的一般规则,并演示了不可变对象的一些优点。
源
不可变对象是内部字段(或者至少是影响其外部行为的所有内部字段)不能被更改的对象。
不可变字符串有很多优点:
性能:执行如下操作:
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
The two lines both replace the fourth character with the letter "a". Not only is the second piece of code more readable, it's faster. Look at how you would have to do the underlying code for foo. The substrings are easy, but now because there's already a character at space five and something else might be referencing foo, you can't just change it; you have to copy the whole string (of course some of this functionality is abstracted into functions in the real underlying C, but the point here is to show the code that gets executed all in one place).
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();
}
因为可变对象是一个本地引用,所以不必担心并发安全性(只有一个线程接触过它)。由于它没有在其他任何地方被引用,所以它只在堆栈上分配,所以函数调用一结束它就会被释放(您不必担心垃圾收集)。你可以同时获得可变性和不可变性的性能优势。