3. 基本引用 部分 2¶
3.1. 共享¶
两个都指向同一个被引用对象的引用被称为“共享”。有时我们说每个引用都是另一个的 别名 ,因为我们可以用任一名称来引用该对象。两个或更多引用能够协作共享单个内存结构是引用的一个关键优势。上述示例中的引用 second 和 empRef 共享同一个对象,因此任一引用都可以修改该对象的值。引用操作只是一种技术—,共享往往是真正的目标。稍后我们将看到如何利用共享来为程序各部分之间提供高效的通信。
3.2. 浅拷贝与深拷贝¶
3.2.1. 复制意味着什么?¶
共享的一个重要用途是使两个方法之间能够通信。一个方法将感兴趣对象的引用传递给另一个方法。两个方法都可以访问感兴趣的对象,但感兴趣的对象本身不会被复制。这种通信被称为 浅拷贝 ,因为,而不是制作并发送感兴趣的对象的(大型)副本,而是发送一个(小型)引用,并且感兴趣的对象被共享。接收方需要理解他们拥有一个浅拷贝,这样他们就知道不要意外地更改或删除它,因为它被共享。另一种—其中制作并发送完整的副本—被称为一个 深拷贝 。深拷贝在某种意义上更简单,因为每个方法可以更改它们的副本而不会干扰另一个副本。但是,由于所有的复制,深拷贝运行得更慢。如果第二个方法 旨在 为对象的所有用户修改副本,那么深拷贝就不会允许这样做。下面的图示显示了两个方法 A() 和 B() 之间的浅拷贝和深拷贝。在浅拷贝的情况下,通过两个方法之间传递引用,笑脸被共享。在深拷贝的情况下,笑脸被复制,每个方法都得到自己的。
3.3. 浅比较与深比较¶
与正确复制相关(浅拷贝意味着复制引用本身,深拷贝意味着复制被引用的对象),当涉及对象时,我们可能会使用不同级别来比较事物。一种可能性是,我们仅仅想知道两个引用变量是否引用同一个对象。我们可以使用以下代码来查看 ref1 和 ref2 是否引用同一个对象:
如果(指针 1 等于 指针 2)
因此,我们比较箭头的值,它们都指向同一个盒子。虽然有时这样做是值得的,但实际上并不常见。任何对引用变量的使用 == 都应引起特别警惕。
通常,我们想知道两个对象的 值 是否相同。这在对象是字符串时尤其常见。我们可能想知道两个字符串是否相同,即它们具有相同的字符且顺序一致, 而非 两个字符串引用变量是否引用同一个字符串对象。如果我们想知道两个字符串的内容是否相同,那么在大多数语言中,我们 不 简单地比较两个引用变量以判断其相等性。相反,我们需要执行一种“深度比较”,即查看字符串本身的字符。大多数语言都提供了执行此操作的方法。在 Java 中,我们可以使用 .equals() 类中的 String 方法来判断两个字符串是否相同,例如:
如果(ref1.equals(ref2))
在 Java 中,比较两个字符串对象值的另一个选项是使用 .compareTo() 方法。如果第一个字符串按字母顺序小于第二个,则返回 -1;如果它们相同,则返回 0;如果第一个字符串大于第二个,则返回 1。以下代码与上述使用 .equals() 的代码等效:
如果(ref1 与 ref2 的)比较结果为 0
3.4. 指针¶
当引用首次分配时,它没有指向对象。该引用为 未初始化 或简单地称为"bad"。在 Java 中,引用实际上被初始化为值 null ,而在某些其他语言中,它们字面上具有未知值。无论如何,解引用 bad 或 null 指针值都会导致严重的运行时错误。解引用操作会立即崩溃或停止。每个引用必须在支持解引用操作之前被分配一个指向对象。在此之前,该引用是 bad 且不得被使用。在我们的内存图中,bad 指针值被绘制为引用变量方框对角线之间的对角线。
错误的引用很常见。事实上,每个引用最初都包含一个错误的值。正确的代码会将这个错误值覆盖为对目标对象的正确引用,此后该引用即可正常工作。没有任何机制能自动赋予引用有效的目标对象。你必须小心编写代码。幸运的是,Java 编译器通常会发现引用变量未初始化的情况,并生成错误(这意味着你甚至无法完成编译并运行程序)。
另一方面,这并不能阻止你将变量赋值给一个稍后会导致严重问题的对象。如果你的代码正在崩溃,一个 badPointer 应该成为你首先怀疑的对象。在 Java 中,运行系统每次解引用引用变量时都会检查它是否为 null 。因此,代码仍然可能表现出引用错误,但解引用一个 null 值的错误类型至少会在出错行上礼貌地停止。一个称为 NullPointerException 的运行时错误将发生,程序将停止。因此,在 Java 中更容易定位和修复引用错误。这些运行时检查也是 Java 运行速度通常比 C 或 C++ 语言稍慢的原因之一。
一种思考引用代码的方式是,它操作于两个层级:—引用层级和指针层级。窍门在于, 两个 层级都需要初始化和连接,事物才能正常工作。(1) 引用必须分配,(2) 指针必须分配,以及 (3) 引用必须被分配指向指针。忘记步骤 (1) 的情况很少见。但忘记 (2) 或 (3),整个东西会在第一次解引用时崩溃。例如,一个流行的错误是声明一个字符串变量,但从未在尝试打印或其他方式使用它之前给它分配一个实际的字符串。记得要同时考虑两个层级。在设计期间绘制内存图可以帮助确保它是正确的。
具有最常见引用错误的代码看起来 正确,但缺少了引用被赋值的中间步骤。坏代码可以正常编译,但在运行时,每个对坏引用的解引用都会引发 NullPointerException ,导致程序崩溃。你的任务是确保在使用之前每个引用都被赋值为一个点值。这里有一个简单的坏代码示例,以及如果执行此代码内存将如何反应的可视化图。
Employee badPointer; // Allocate the reference ...
// badPointer = new Employee("Sam", 1000); ... but forget the pointee
System.out.println(badPointer.getName()); // This line causes a NullPointerException
Employee badPointer; // Allocate the reference ...
// badPointer = new Employee("Sam", 1000); ... but forget the pointee
System.out.println(badPointer.getName()); // This line causes a NullPointerException
3.4.1. 为什么指针错误如此常见?¶
在上文给出的 badPointer 例子中,编译器实际上会在该错误被允许运行之前将其捕获,因为未初始化的指针正在被引用。但是,如果你的程序由于某种原因将 badPointer 的值设置为 null ,也会发生完全相同的结果。编译器无法替你捕获这一点。
必须存在一个原因,Java 如此关注解引用 null 指针,因为它时刻警惕着这种情况。为什么?因为这在很多程序中都会发生。
为什么程序员经常分配一个引用,却忘记将其设置为指向被引用对象?或者,为什么程序员会将引用的值设置为 null ,然后对其进行解引用?引用的规则似乎并不复杂,然而每个程序员都会反复犯这个错误。为什么?一个解释是我们被所使用的工具所训练。简单变量不需要任何额外的设置。你可以分配一个简单的变量,例如 int ,并立即使用它。你可以将其更改为你想要的任何值,其值通常不会导致程序崩溃。请尝试记住不要解引用一个 null 指针值。但不要惊讶于它的发生,以及你的程序会崩溃。

