整数是否是不可变的?

114

我知道这可能很愚蠢,但很多地方声称Java中的Integer类是不可变的,然而以下代码:

Integer a=3;
Integer b=3;
a+=b;
System.out.println(a);

执行没有问题,得到了(预期的)结果6。所以a的值实际上已经改变了。这是否意味着Integer是可变的?第二个问题有点离题:“不可变类不需要复制构造函数”。有人能解释一下为什么吗?


13
这个类是不可变的,但是自动装箱会导致一些奇怪的问题发生:https://dev59.com/anA75IYBdhLWcg3w0sp9 - wkl
谢谢,盒装是我需要谷歌的关键词 :) - K.Steff
9
你把不可变的值和final或常量值混淆了。 - Code Enthusiastic
12个回答

101

Immutable并不意味着a永远不能等于另一个值。例如,String也是不可变的,但我仍然可以这样做:

Immutable并不代表a永远不可能等于其他值。例如,String也是不可变的,但我仍然可以执行以下操作:

String str = "hello";
// str equals "hello"
str = str + "world";
// now str equals "helloworld"

str并未改变,相反str现在是一个全新实例化的对象,就像你的Integer一样。因此,a的值没有发生变化,但它被替换为一个全新的对象,即new Integer(6)


17
这是因为str现在是一个全新的对象。或者说,str(一个变量)指向一个新的对象。对象本身是不可变的,但由于该变量不是final,它可以指向另一个对象。 - Sandman
是的,它指向一个不同的对象,这个对象是由 += 操作实例化的结果。 - Travis Webb
11
严格来说,它不需要是一个新对象。装箱使用 Integer.valueOf(int) 方法并维护一个 Integer 对象的缓存。因此,在 Integer 变量上执行 += 操作的结果可能是先前存在的对象(或者甚至可能是相同的对象……在 a += 0 的情况下)。 - Stephen C
3
为什么String的JavaDoc明确说明它是不可变的,但Integer的JavaDoc没有这样说呢?正是因为这种差异,我才会阅读这个问题... - cellepo

59

a 是指向整数3的“引用”,你的简写 a+=b 实际上意味着执行以下操作:

a = new Integer(3 + 3)

所以,整数是不可变的,但指向它们的变量是可变的*。

*可以有不可变的变量,这些变量用关键字final表示,这意味着引用不能更改。

final Integer a = 3;
final Integer b = 3;
a += b; // compile error, the variable `a` is immutable, too.

这个答案非常有帮助。因此,可以将final关键字视为在C中标记指针常量。 - j3141592653589793238

23

你可以使用 System.identityHashCode() 确定对象是否已更改(更好的方法是使用普通的 ==,但不太明显是引用而不是值已更改)

Integer a = 3;
System.out.println("before a +=3; a="+a+" id="+Integer.toHexString(System.identityHashCode(a)));
a += 3;
System.out.println("after a +=3; a="+a+" id="+Integer.toHexString(System.identityHashCode(a)));

打印

before a +=3; a=3 id=70f9f9d8
after a +=3; a=6 id=2b820dda

您可以看到对象a所引用的底层"id"已经发生了改变。


2
System.identityHashCode() 是一个非常好的提示。谢谢这个。 - Ad Infinitum

11

对于最初提出的问题,

Integer a=3;
Integer b=3;
a+=b;
System.out.println(a);

整数是不可变的,因此上面发生的情况是'a'已经改变成了一个新的值为6的引用。初始值3在内存中没有引用(它未被更改),因此可以进行垃圾回收。

如果这种情况发生在字符串上,它将在池中保留(在PermGen空间中)的时间比整数长,因为它期望有引用。


9

是的,整数是不可变的。

A是一个指向对象的引用。当您运行a += 3时,它会重新分配A以引用一个新的整数对象,并且该对象具有不同的值。

您从未修改过原始对象,而是将引用指向了另一个对象。

请阅读这里关于对象和引用之间的区别。


用通俗易懂的语言简单明了地表达,避免使用其他复杂的解释 :) - Roshan Fernando

5

Immutable并不意味着您不能为变量更改值。它只是意味着任何新的赋值都会创建一个新对象(分配一个新的内存位置),然后将值赋给它。

为了自己理解这一点,在循环中执行整数赋值(使用在循环外声明的整数),并查看内存中的实时对象。

为什么不需要为不可变对象编写复制构造函数是因为这是常识。由于每个赋值都创建一个新对象,语言在技术上已经创建了一个副本,因此您不必创建另一个副本。


3

我对不可变对象的理解如下:

int a=3;    
int b=a;
b=b+5;
System.out.println(a); //this returns 3
System.out.println(b); //this returns 8

如果整数可以改变,"a" 的值将打印8,但它不能改变,这就是为什么它的值是3。您的例子只是一个新赋值操作。

2
“不可变类不需要拷贝构造器”,有人可以解释一下为什么吗?
原因在于,很少有必要复制(甚至没有复制的意义)一个不可变类的实例。对象的复制应该“与”原始对象相同,如果它是相同的,就没有必要创建它。
然而,这里有一些基本假设: - 它假定你的应用程序不会将类的实例身份放置在任何含义上。 - 它假设该类已经重载了equals和hashCode方法,以便实例的副本将根据这些方法“与”原始副本相同。
这两个假设中的任何一个或者两个都可能是错误的,这时候可能需要添加一个拷贝构造器。

0
public static void main(String[] args) {
    // TODO Auto-generated method stub

    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

    Integer i=1000;
    Integer i2=i;

    i=5000;

    System.out.println(i2); // 1000
    System.out.println(i); // 5000

    int j=1000;
    int j2=j;

    j=5000;

    System.out.println(j2); // 1000
    System.out.println(j); //  5000


    char c='a';
    char b=c;

    c='d';

    System.out.println(c); // d
    System.out.println(b); // a
}

输出结果为:

嗨 再见 1000 5000 1000 5000 d a

因此,char是可变的,而String、Integer和int是不可变的。


1
此答案没有提供任何其他信息。 - Giulio Caccin

0

我可以通过简单的示例代码来说明 Integer(以及其他类似 Float、Short 等)是不可变的:

示例代码

public class Test{
    public static void main(String... args){
        Integer i = 100;
        StringBuilder sb = new StringBuilder("Hi");
        Test c = new Test();
        c.doInteger(i);
        c.doStringBuilder(sb);
        System.out.println(sb.append(i)); //Expected result if Integer is mutable is Hi there 1000
    }

    private void doInteger(Integer i){
        i=1000;
    }

    private void doStringBuilder(StringBuilder sb){
        sb.append(" there");
    }

}
实际结果 结果显示为Hi There 100,而不是预期结果(在sb和i都是可变对象的情况下)Hi There 1000 这表明在主函数中创建的i对象没有被修改,而sb对象被修改了。
所以StringBuilder展示了可变行为,但Integer没有。
所以Integer是不可变的。 因此得证 另一个没有只有Integer的代码:
public class Test{
    public static void main(String... args){
        Integer i = 100;
        Test c = new Test();
        c.doInteger(i);
        System.out.println(i); //Expected result is 1000 in case Integer is mutable
    }

    private void doInteger(Integer i){
        i=1000;
    }


}

你正在做两件不同的事情 - 尝试重新分配整数并在 StringBuilder 上调用方法。如果你这样做 private void doStringBuilder(StringBuilder sb){ sb = new StringBuilder(); } 那么 sb 不会改变。 - MT0
我添加了可变的StringBuilder,只是为了将Integer与另一个可变对象并置。如果你想的话,可以删除所有与StringBuilder相关的代码,只需打印i即可看到100。 - Ashutosh Nigam
这并不证明不可变性 - 你所做的只是重新散列这个例子,证明Java使用按值传递(而对象传递的值是指针)。 - MT0
尝试 private void doInteger(Integer i){ System.out.println( i == 100 ); i=1000; System.out.println( i == 100 ); } - MT0
@MT0 当您按值传递时,StringBuilder仍指向同一对象,但整数传递新的副本而不是引用到同一对象。如果在doInteger中打印,您将显示函数拥有的副本而不是主函数。我们想知道主要功能中所指向的对象是否相同。希望它能清楚概念 :) 还有,StringBuilder的不可变版本是String。让我知道如果您想要一个示例。 - Ashutosh Nigam
让我们在聊天中继续这个讨论 - Ashutosh Nigam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接