【发布时间】:2012-09-11 13:13:08
【问题描述】:
我知道适用于一般不可变类的常见原因,即
- 不能作为副作用改变
- 很容易推断他们的状态
- 本质上是线程安全的
- 无需提供克隆/拷贝构造函数/工厂拷贝方法
- 实例缓存
- 无需防御性副本。
但是,包装类表示原始类型,而原始类型是可变的。那么为什么包装类不可变呢?
【问题讨论】:
标签: java immutability mutable primitive-types
我知道适用于一般不可变类的常见原因,即
但是,包装类表示原始类型,而原始类型是可变的。那么为什么包装类不可变呢?
【问题讨论】:
标签: java immutability mutable primitive-types
但是,包装类表示原始类型,原始类型(字符串除外)是可变的。
首先,String 不是原始类型。
其次,谈论原始类型是可变的是没有意义的。如果您像这样更改 变量 的值:
int x = 5;
x = 6;
这并没有改变数字 5 - 它改变了 x 的值。
虽然包装器类型可以设置为可变的,但在我看来,这样做会很烦人。我经常使用这些类型的只读集合,并且不希望它们是可变的。偶尔我想要一个可变的等价物,但在这种情况下,很容易想出一个,或者使用 Atomic* 类。
我发现自己希望 Date 和 Calendar 是不可变的比我希望 Integer 是可变的要频繁得多......(当然,我通常会使用 Joda Time,但其中一个好处是Joda Time 是不变性。)
【讨论】:
String x = "hello"; x = "there";,那不会让它们变得可变,不是吗?更改变量的值与更改该值本身不同。
setTime 等将其视为普通Date 的东西都会失败。
这是一个例子,当 Integer 可变时会很糟糕
class Foo{
private Integer value;
public set(Integer value) { this.value = value; }
}
/* ... */
Foo foo1 = new Foo();
Foo foo2 = new Foo();
Foo foo3 = new Foo();
Integer i = new Integer(1);
foo1.set(i);
++i;
foo2.set(i);
++i;
foo3.set(i);
现在 foo1、foo2 和 foo3 的值是什么?您会认为它们是 1、2 和 3。但是当 Integer 可变时,它们现在都将是 3,因为 Foo.value 都将指向同一个 Integer 对象。
【讨论】:
对于某些类型,还有可变的、线程安全的包装器。
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicLongArray
AtomicReference - can wrap a String.
AtomicReferenceArray
加上一些奇特的包装器
AtomicMarkableReference - A reference and boolean
AtomicStampedReference - A reference and int
【讨论】:
包装类是不可变的,因为可变是没有意义的。
考虑以下代码:
int n = 5;
n = 6;
Integer N = new Integer(n);
起初,如果您可以更改 N 的值,看起来很简单, 就像你可以改变 n 的值一样。
但实际上 N 不是 n 的包装器,而是 6 的包装器! 再看下面一行:
Integer N = new Integer(n);
您实际上是在将 n 的值(即 6)传递给 N。 并且由于 Java 是按值传递的,因此您不能将 n 传递给 N, 使 N 成为 n 的包装器。
所以,如果我们确实在包装器中添加了一个 set 方法:
Integer N = new Integer(n);
N.setValue(7);
print(N); // ok, now it is 7
print(n); // oops, still 6!
n 的值不会改变,这会让人困惑!
结论:
包装器类是值的包装器,而不是变量的包装器。
如果你确实添加了 set 方法会很混乱。
如果你知道它是一个值的包装器,你将不再要求一个 set 方法。例如,您不会执行“6.setValue(7)”。
在 Java 中无法对变量进行包装。
【讨论】:
供您参考:如果您想要可变持有者类,您可以使用 java.util.concurrent 包中的 Atomic* 类,例如AtomicInteger, AtomicLong
【讨论】:
但是,包装类表示原始类型,原始类型(字符串除外)是可变的。
不,它们不是(而且 String 不是原始类型)。但是由于原始类型无论如何都不是对象,所以它们一开始就不能真正称为可变/不可变。
不管怎样,包装类是不可变的这一事实是一个设计决策(一个很好的 IMO。)它们本可以很容易地被设置为可变的,或者也提供了可变的替代方案(确实有几个库提供了这一点,其他语言也可以这样做)默认。)
【讨论】:
任何具有任何可变方面的对象实例都必须具有唯一的身份;否则,另一个对象实例在某个时刻碰巧在除其身份之外的所有方面都相同,但在其他时刻可能在其可变方面有所不同。但是,在许多情况下,对于没有标识的类型很有用——能够传递“4”而不必担心 哪个“4”正在传递。虽然有时拥有一个原始或不可变类型的可变包装器可能会有所帮助,但在更多时候,拥有一个在某个时刻持有相同数据的所有实例都可以被视为的类型是有用的可以互换。
【讨论】:
原始类型是可变的,但它们不可共享——也就是说,没有两段代码会引用同一个 int 变量(它们总是按值传递)。因此,您可以更改您的副本,而其他人不会看到更改,反之亦然。正如菲利普在他的回答中所表明的那样,可变包装类不会出现这种情况。所以我的猜测是,当包装原始数据类型时,他们有一个选择:
符合您可以更改原始类型的值的事实,
对比
与可以传递原始类型的事实相匹配,并且任何其他数据用户都不会看到用户所做的任何更改。
他们选择了后者,后者需要不变性。
【讨论】:
例如,考虑以下 java 程序:
class WhyMutable
{
public static void main(String[] args)
{
String name = "Vipin";
Double sal = 60000.00;
displayTax(name, sal);
}
static void displayTax(String name, Double num) {
name = "Hello " + name.concat("!");
num = num * 30 / 100;
System.out.println(name + " You have to pay tax $" + num);
}
}
Result: Hello Vipin! You have to pay tax $18000.0
包装类参数的引用传递也是如此。而且,如果字符串和包装类不是最终的,任何人都可以扩展这些类并编写自己的代码来修改包装的原始数据。因此,为了保持数据完整性,我们用于数据存储的变量必须是只读的,
即,字符串和包装类必须是最终的和不可变的,并且“通过 不应提供“通过引用”功能。
【讨论】: