Java不可变性的背后原理

4

所以,我是Java的新手,正在努力理解不可变对象是如何实现的,以及为什么它们看起来仍然是可变的。我查看了很多资料,似乎都在“暗示”发生了什么,但我只想澄清一下是否我走在了正确的道路上。

使用以下简单的示例:

import java.math.*;

class BIMutability {

   public static void main(String args[]) {

      BigInteger biValue = new BigInteger("2");

      for (int i=1; i<10; i++) {
         System.out.println(i + ". biValue = " + biValue);
         biValue = biValue.multiply(biValue);
      }
   }
}

当运行时,会产生以下结果:
1. biValue = 2
2. biValue = 4
3. biValue = 16
4. biValue = 256
5. biValue = 65536
6. biValue = 4294967296
7. biValue = 18446744073709551616
8. biValue = 340282366920938463463374607431768211456
9. biValue = 115792089237316195423570985008687907853269984665640564039457584007913129639936

表面上看,biValue似乎是可变的。但实际上并非如此,我的理解是:biValue本质上是一个指针变量。在运行时进行实例化时,为BigInteger类的对象分配了堆空间,调用了它的构造函数(其中包括根据字面值“2”初始化对象的值),最后将一个指向对象空间的指针赋给了biValue。(这样说对吗?)
随后,在循环的每次迭代中,multiply方法会为新对象实例分配额外的堆空间以包含结果不可变的值(例如,为第四次迭代的结果分配堆空间,并将新对象赋值为256),然后将一个指向新对象的指针赋给biValue。(这也正确吗?)
(顺便说一下,据我所知,先前的对象的堆空间只是被遗弃了吗?还是立即执行了垃圾收集?如果没有,那么在某些情况下,您可能很快就会耗尽堆空间。)
所以,我是否正确地理解了这一点,或者我漏掉了重要细节,或者……?
谢谢!

2
你说的一切都是正确的;为了消除你对垃圾回收的困惑,当 biValue 被设置为其他值时,之前的 BigInteger 对象将不再有指向它的引用,因此它可以被垃圾回收。它不会立即被垃圾回收,但 JVM 会为你处理这个问题。 - Jacob G.
1
你说的一切都是正确的。你没有提到的一件事是变量和对象之间的区别。biValue 是一个变量。在 Java 程序中,每个变量都保存着一个原始值(例如 intdouble),或者它保存着一个对象引用。在 Java 中(不像 C++),对象从来不会存储在变量中。它们只能在堆上找到。在你的程序中,biValue 是一个可变的变量,它保存着对一个不可变对象的引用。如果你想要一个不可变的变量,你可以将其声明为 final,或者你可以简单地不编写任何分配它的代码。 - Solomon Slow
一个 final 变量并不会使对象变为不可变。如果 final 引用了一个对象,那么它只是意味着该变量不能被重新赋值。通过确保没有可变的方法,可以使类变为不可变。这里没有任何神奇的事情发生。 - jkratz
@jameslarge "不编写任何分配代码" 并不能使变量成为不可变的。这就像说不调用任何setter方法会使对象成为不可变的一样。不可变性关注的是你可以做什么,而不是你实际做了什么。 - Andreas
@Andreas,你说得对。我很少声明任何公共变量。只有当公共变量被声明为“final”时,它才是不可变的。私有变量...嗯,这并不重要,因为你的代码已经实现了它的功能,除非你改变它,否则它永远不会做其他事情。 - Solomon Slow
1个回答

2
你说的关于 multiply 在每次调用时创建新的 BigInteger 对象,以及在每次迭代中 biValue 的值会发生变化是正确的。
当我们谈论不可变性时,我们指的是对象不能被改变,而非变量。使用final修饰符可以声明一个不可变的变量。
是的,许多不可变的类看起来可能是可变的。在这种情况下,看似具有变异作用的方法将创建一个新对象。
至于循环中之前迭代的BigInteger对象的命运,它们将被垃圾回收。但是,这个过程发生的时间是不确定的。请注意,它可能会填满堆,这不是因为GC没有收集未使用的对象,而是因为数字太大。

可变对象可以在不重新分配给变量的情况下进行突变。这个说法有点模糊。通常通过分配给实例变量(也称为字段)来进行突变。有八种类型的变量:类变量、实例变量、数组组件、方法参数、构造函数参数、lambda参数、异常参数和局部变量。 - Andreas
由于通过重新分配保存对该对象引用的变量来改变对象是不可能的,因此说您可以在不重新分配的情况下改变对象是多余的,并且会混淆问题。 - Andreas
@Andreas 好的,已经移除了那行代码。 - Sweeper

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