为什么这个代码没有抛出NullPointerException异常?

89

需要澄清以下代码:

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
referToSample.append("B");
System.out.println(sample);

这将打印B,因此证明samplereferToSample对象引用相同的内存引用。

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
sample.append("A");
referToSample.append("B");
System.out.println(referToSample);

这将打印出 AB,这也证明了相同的事实。

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
referToSample = null;
referToSample.append("A");
System.out.println(sample);

显然,这会抛出NullPointerException,因为我试图在一个空引用上调用append

StringBuilder sample = new StringBuilder();
StringBuilder referToSample = sample;
referToSample = null;
sample.append("A");
System.out.println(sample);

所以我的问题是,为什么最后一个代码示例没有抛出NullPointerException?因为从前两个示例中我所看到和理解的是,如果两个对象引用同一个对象,那么如果我们更改任何值,它也会反映到另一个对象上,因为两者都指向相同的内存引用。那么为什么这个规则在这里不适用呢?如果我将null赋值给referToSample,则sample也应该为null,并且它应该抛出NullPointerException,但实际上它并没有抛出,为什么呢?


31
“sample”仍然是“sample”。你只改变了“referToSample”。 - Dave Newton
25
点赞/标星了!问题非常基础,但这是一个很好的例子,解释了你的问题并且提问得很好。 - Ryan Ransford
15
你问题中术语的一个要点是:你一直将“样本(sample)”和“参考样本(referToSample)”称为对象,但它们不是对象,它们是变量。变量可以保存对对象的引用,但它本身并不是对象。这是一个微妙的区别,但基本上是你混淆的本质。 - Daniel Pryden
1
将对象变量视为指针会有所帮助。任何作用于变量(volatilefinal=``==等)的运算符,当应用于对象变量时,会影响指针而不是它所引用的对象。 - MikeFHay
2
@Arpit 我尊重地不同意。这个问题中隐藏着一个重要的概念,那就是对象和对该对象的引用之间的区别。大多数情况下,我们不需要(也不想)意识到这种差异,语言设计者努力将其隐藏起来。例如,在C++中考虑按引用传递的参数。因此,对于初学者被所有这些魔法搞糊涂并不奇怪! - Gyom
显示剩余13条评论
7个回答

89
null赋值不会通过全局销毁对象来更改。那种行为会导致难以追踪的错误和反直觉的行为。它们只会中断特定的引用
为了简单起见,假设sample指向地址12345。这可能不是地址,仅用于在这里简化事情。地址通常使用Object#hashCode()中给出的奇怪十六进制表示形式表示,但这取决于实现。1
StringBuilder sample = new StringBuilder(); //sample refers to 
//StringBuilder at 12345 

StringBuilder referToSample = sample; //referToSample refers to 
//the same StringBuilder at 12345 
//SEE DIAGRAM 1

referToSample = null; //referToSample NOW refers to 00000, 
//so accessing it will throw a NPE. 
//The other reference is not affected.
//SEE DIAGRAM 2

sample.append("A"); //sample STILL refers to the same StringBuilder at 12345 
System.out.println(sample);

从标记有查看图示的线条上可以得知,当时物体的图示如下:

图示1:

[StringBuilder sample]    -----------------> [java.lang.StringBuilder@00012345][StringBuilder referToSample] ------------------------/

图 2:

[StringBuilder sample]    -----------------> [java.lang.StringBuilder@00012345]

[StringBuilder referToSample] ---->> [null pointer]

图示2表明取消referToSample并不会破坏sample对于位于00012345的StringBuilder的引用。

1考虑到垃圾回收的因素,这种情况不太可能发生。


如果这回答了你的问题,请点击上面文本左侧的投票箭头下方的勾来确认。 - Ray Britton
@commit 是的,它将引用一个特殊地址(或一组地址)表示空值,可能是000000。这里的数字只是示例。它只是表示没有特定的引用对象。 - nanofarad
11
hashCode() 不等同于内存地址。 - Kevin Panko
6
身份哈希码通常是在第一次调用该方法时从对象的内存位置派生的。从那时起,即使内存管理器决定将对象移动到不同的内存位置,该哈希码也是固定和记忆的。 - Holger
3
重写 hashCode 方法的类通常将其定义为返回与内存地址无关的值。我认为您可以在不提及 hashCode 的情况下阐述有关对象引用和对象之间的差异。获取内存地址的细节可以留待以后再讨论。 - Kevin Panko
显示剩余7条评论

62

最初,正如您所说的那样,referToSample 引用了下面显示的 sample

1. 场景1:

referToSample refers to sample

2. 场景1 (续):

referToSample.append("B")

  • 在这里,由于 referToSample 引用了 sample,所以它追加了 "B",您编写的是:

    referToSample.append("B")

同样的事情发生在场景2中:

但是,在3. 场景3中,就像 hexafraction 说的那样,

当您将 null 赋值给 referToSample 时,它引用了 sample,它没有改变 ,而只是打破了从 samplereferToSample引用,现在它指向一个不存在的地方。如下图所示:

when referToSample = null

现在,由于 referToSample 指向了不存在的地方,所以当您执行 referToSample.append("A"); 时,它将无法找到任何值或引用可以追加 A。因此,它会抛出 NullPointerException

但是 sample 仍然与您初始化它的方式相同:

StringBuilder sample = new StringBuilder(); 因此它已经被初始化,现在它可以追加 A,也不会抛出 NullPointerException


5
好的图表,有助于说明它。 - nanofarad
不错的图表,但底部的注释应该是“现在referToSample不再引用“”,因为当你执行referToSample = sample;时,你是在引用sample,只是复制了所引用内容的地址。 - Khaled.K

17
简而言之:你将 null 赋值给一个引用变量,而不是赋值给一个对象。
在一个例子中,你改变了被两个引用变量所指向的对象的状态。当这种情况发生时,两个引用变量都会反映出这个改变。
在另一个例子中,你改变了分配给一个变量的引用,但这对对象本身没有影响,因此仍然指向原始对象的第二个变量不会注意到对象状态的任何改变。
具体到你提出的“规则”:
如果两个对象引用同一个对象,那么如果我们改变任何值,那么它也会反映到另一个对象上,因为两者都指向同一个内存引用。
同样地,你指的是修改两个变量所引用的同一对象的状态。
那么为什么这个规则不适用于这里呢?如果我将 referToSample 赋值为 null ,那么 sample 也应该为空,并且应该抛出 nullPointerException,但它并没有抛出,为什么?
再次强调,你改变的是一个变量的引用,而这对另一个变量的引用没有任何影响。
这两个行为是完全不同的,将产生完全不同的结果。

3
@commit:这是Java中一个基本但至关重要的概念,一旦你掌握了它,就会一生受用。 - Hovercraft Full Of Eels

8

看这个简单的图示:

diagram

当你在referToSample上调用一个method时,[your object]被更新,所以它也会影响到sample。但是当你说referToSample = null时,你只是改变了referToSample所指向的东西。


3

在这里,“sample”和“referToSample”引用的是同一对象。这就是不同指针访问同一内存位置的概念。因此,将一个引用变量赋值为null并不会销毁该对象。

   referToSample = null;

意思是'referToSample'只指向null,对象仍然相同,其他引用变量运行良好。所以对于'sample',它不指向null并且具有有效的对象。

   sample.append("A");

运行良好。但是,如果我们尝试将null附加到“referToSample”,它将显示NullPointException。

   referToSample .append("A");-------> NullPointerException

这就是为什么你在第三个代码片段中遇到了NullPointerException错误。

0
每当使用new关键字时,它会在堆上创建一个对象。

1)StringBuilder sample = new StringBuilder();

2)StringBuilder referToSample = sample;

在第二个示例中,引用referSample被创建在同一对象样本上

因此,referToSample = null; 只是将referSample引用置空,不影响样本 这就是为什么您没有收到NULL指针异常的原因 感谢Java的垃圾回收


0

很简单,Java没有按引用传递,它只是传递对象引用。


2
参数传递语义与所描述的情况实际上没有任何关系。 - Michael Myers

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