Tại sao điều này không ném NullPointerException?


89

Nead làm rõ cho mã sau:

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

Điều này sẽ in ra Bđể chứng minh samplereferToSamplecác đối tượng tham chiếu đến cùng một tham chiếu bộ nhớ.

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

Điều này sẽ in ABra cũng chứng minh điều tương tự.

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

Rõ ràng điều này sẽ ném NullPointerExceptionbởi vì tôi đang cố gắng gọi appendmột tham chiếu rỗng.

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

Vì vậy, đây là câu hỏi của tôi, tại sao mẫu mã cuối cùng không ném ra NullPointerExceptionbởi vì những gì tôi thấy và hiểu được từ hai ví dụ đầu tiên là nếu hai đối tượng tham chiếu đến cùng một đối tượng thì nếu chúng ta thay đổi bất kỳ giá trị nào thì nó cũng sẽ phản ánh sang đối tượng khác vì cả hai đều trỏ đến cùng một tham chiếu bộ nhớ. Vậy tại sao quy tắc đó không được áp dụng ở đây? Nếu tôi gán nullcho referenceToSample thì mẫu cũng phải là null và nó sẽ ném NullPointerException nhưng nó không ném một cái, tại sao?


31
samplevẫn còn sample. Bạn chỉ thay đổi referToSample.
Dave Newton

25
Bình chọn / gắn dấu sao! Câu hỏi rất cơ bản, nhưng đây là một ví dụ tuyệt vời về việc giải thích vấn đề của bạn đặt câu hỏi tốt.
Ryan Ransford

15
Một điểm của thuật ngữ trong câu hỏi của bạn: bạn tiếp tục đề cập đến samplereferToSamplenhư các đối tượng nhưng chúng không phải là đối tượng, chúng là các biến. Một biến có thể chứa một tham chiếu đến một đối tượng, nhưng bản thân nó không phải là một đối tượng. Đó là một sự khác biệt tinh tế, nhưng về cơ bản nó là bản chất của sự nhầm lẫn của bạn.
Daniel Pryden

1
Nó giúp nghĩ về các biến đối tượng chỉ là con trỏ. Bất kỳ nhà điều hành có vai trò trên một biến ( volatile, final, =, ==...) khi áp dụng cho một biến đối tượng ảnh hưởng đến con trỏ , không phải là đối tượng mà nó đề cập đến.
MikeFHay

2
@Arpit Tôi kính trọng không đồng ý. Có một khái niệm lớn ẩn trong câu hỏi này, và đó là, sự khác biệt giữa một đối tượng và một tham chiếu đến đối tượng đó. Hầu hết thời gian, chúng ta không cần (cũng như không muốn) nhận thức được sự khác biệt này và các nhà thiết kế ngôn ngữ làm việc chăm chỉ để che giấu nó với chúng ta. Ví dụ: chỉ cần nghĩ đến các đối số tham chiếu truyền trong C ++. Vì vậy, không có gì ngạc nhiên đối với tôi khi thấy những người mới bắt đầu bối rối bởi tất cả những điều kỳ diệu đó!
Gyom

Câu trả lời:


89

nullphép gán không thay đổi giá trị bằng cách hủy toàn cục đối tượng đó. Loại hành vi đó sẽ dẫn đến lỗi khó theo dõi và hành vi phản trực giác. Họ chỉ phá vỡ tham chiếu cụ thể đó .

Để đơn giản, giả sử rằng sampletrỏ đến địa chỉ 12345. Đây có thể không phải là địa chỉ và chỉ được sử dụng để làm cho mọi thứ trở nên đơn giản ở đây. Địa chỉ thường được biểu diễn bằng hệ thập lục phân kỳ lạ được đưa ra Object#hashCode(), nhưng điều này phụ thuộc vào việc triển khai. 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);

Từ các đường kẻ đã đánh dấu See diagramsơ đồ của các đối tượng lúc đó như sau:

Sơ đồ 1:

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

Sơ đồ 2:

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

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

Sơ đồ 2 cho thấy rằng việc hủy bỏ referToSamplekhông phá vỡ tham chiếu sampleđến StringBuilder tại 00012345.

1 Các cân nhắc của GC làm cho điều này trở nên khó tin.


@commit, nếu điều này trả lời câu hỏi của bạn, vui lòng nhấp vào dấu kiểm bên dưới mũi tên biểu quyết ở bên trái của văn bản ở trên.
Ray Britton

@RayBritton Tôi thích cách mọi người cho rằng câu trả lời được bình chọn cao nhất là câu trả lời bắt buộc phải trả lời câu hỏi. Mặc dù số lượng đó rất quan trọng nhưng nó không phải là số liệu duy nhất; Câu trả lời -1 và -2 có thể được chấp nhận chỉ vì chúng đã giúp OP nhiều nhất.
nanofarad

11
hashCode()không được điều tương tự như địa chỉ bộ nhớ
Kevin Panko

6
Mã băm nhận dạng thường bắt nguồn từ vị trí bộ nhớ của Đối tượng trong lần đầu tiên phương thức được gọi. Từ đó trở đi Mã băm đó được cố định và ghi nhớ ngay cả khi trình quản lý bộ nhớ quyết định di chuyển đối tượng đến một vị trí bộ nhớ khác.
Holger

3
Các lớp ghi đè hashCodethường xác định nó để trả về một giá trị không liên quan gì đến địa chỉ bộ nhớ. Tôi nghĩ bạn có thể đưa ra quan điểm của mình về các tham chiếu đối tượng so với các đối tượng mà không cần đề cập đến hashCode. Các điểm tốt hơn về cách lấy địa chỉ bộ nhớ có thể để lại cho ngày khác.
Kevin Panko

62

Ban đầu nó như bạn nói referToSamplelà tham samplechiếu như hình dưới đây:

1. Tình huống1:

referenceToSample đề cập đến mẫu

2. Tình huống 1 (tiếp):

referenceToSample.append ("B")

  • Ở đây như referToSampleđược đề cập đến sample, vì vậy nó đã thêm "B" khi bạn viết

    referToSample.append("B")

Điều tương tự đã xảy ra trong Kịch bản 2:

Nhưng, trong 3. Kịch bản 3: như hexafraction đã nói,

khi bạn gán nullcho referToSamplekhi nó được tham chiếu, samplenó không thay đổi giá trị thay vào đó nó chỉ ngắt tham chiếu từ samplevà bây giờ nó chỉ ở đâu cả . như hình bên dưới:

khi referenceToSample = null

Bây giờ, không córeferToSample điểm nào , vì vậy trong khi bạn, referToSample.append("A");nó sẽ không có bất kỳ giá trị hoặc tham chiếu nào mà nó có thể thêm A. Vì vậy, nó sẽ ném NullPointerException.

NHƯNG samplevẫn giống như bạn đã khởi tạo nó với

StringBuilder sample = new StringBuilder(); vì vậy nó đã được tạo ra, vì vậy bây giờ nó có thể thêm A và sẽ không ném NullPointerException


5
Sơ đồ đẹp. Giúp minh họa nó.
nanofarad

sơ đồ tốt đẹp, nhưng ghi chú ở phía dưới nên 'Bây giờ referToSample không đề cập đến ''' bởi vì khi bạn làm referToSample = sample;bạn đang đề cập đến mẫu, bạn chỉ cần sao chép các địa chỉ của những gì được tham chiếu
Khaled.K

17

Tóm lại: Bạn gán null cho một biến tham chiếu, không phải cho một đối tượng.

Trong một ví dụ, bạn thay đổi trạng thái của một đối tượng được tham chiếu bởi hai biến tham chiếu. Khi điều này xảy ra, cả hai biến tham chiếu sẽ phản ánh sự thay đổi.

Trong một ví dụ khác, bạn thay đổi tham chiếu được gán cho một biến, nhưng điều này không ảnh hưởng đến bản thân đối tượng và do đó, biến thứ hai, vẫn tham chiếu đến đối tượng ban đầu, sẽ không nhận thấy bất kỳ thay đổi nào trong trạng thái đối tượng.


Theo "quy tắc" cụ thể của bạn:

nếu hai đối tượng tham chiếu đến cùng một đối tượng thì nếu chúng ta thay đổi bất kỳ giá trị nào thì nó cũng sẽ phản ánh sang đối tượng khác vì cả hai đều trỏ đến cùng một tham chiếu bộ nhớ.

Một lần nữa, bạn đề cập đến việc thay đổi trạng thái của một đối tượng mà cả hai biến đều tham chiếu đến.

Vậy tại sao quy tắc đó không được áp dụng ở đây? Nếu tôi gán null cho referenceToSample thì mẫu cũng phải là null và nó sẽ ném nullPointerException nhưng nó không ném, tại sao?

Một lần nữa, bạn thay đổi tham chiếu của một biến mà hoàn toàn không ảnh hưởng đến tham chiếu của biến kia.

Đây là hai hành động hoàn toàn khác nhau và sẽ dẫn đến hai kết quả hoàn toàn khác nhau.


3
@commit: đó là một khái niệm cơ bản nhưng quan trọng làm nền tảng cho tất cả Java và một khi bạn nhìn thấy, bạn sẽ không bao giờ quên.
Hovercraft Full Of Lươn

8

Xem sơ đồ đơn giản này:

biểu đồ

Khi bạn gọi một phương thức trên referToSample, sau đó [your object]được cập nhật, vì vậy nó cũng ảnh hưởng sample. Nhưng khi bạn nói referToSample = null, thì bạn chỉ đơn giản là thay đổi những gì referToSample đề cập đến.


3

Ở đây 'sample' và 'referenceToSample' đang tham chiếu đến cùng một đối tượng, đó là khái niệm về các con trỏ khác nhau truy cập cùng một vị trí bộ nhớ. Vì vậy, việc gán một biến tham chiếu cho null không phá hủy đối tượng.

   referToSample = null;

có nghĩa là 'referenceToSample' chỉ trỏ đến null, đối tượng vẫn như cũ và biến tham chiếu khác đang hoạt động tốt. Vì vậy, đối với 'mẫu' không trỏ đến null và có một đối tượng hợp lệ

   sample.append("A");

hoạt động tốt. Nhưng nếu chúng tôi cố gắng thêm null vào 'referenceToSample', nó sẽ hiển thị NullPointException. Đó là,

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

Đó là lý do tại sao bạn có NullPointerException trong đoạn mã thứ ba của mình.


0

Bất cứ khi nào một từ khóa mới được sử dụng, nó sẽ tạo một đối tượng tại đống

1) Mẫu StringBuilder = new StringBuilder ();

2) StringBuilder referenceToSample = sample;

Trong 2) Tham chiếu của referenceSample được tạo trên cùng một mẫu đối tượng

do đó, referenceToSample = null; là Nulling Chỉ có tham chiếu Mẫu tham chiếu không có tác dụng đối với mẫu, đó là lý do tại sao bạn không nhận được NULL Pointer Exception Nhờ Bộ sưu tập rác của Java


0

Đơn giản thôi, Java không có tham chiếu truyền, Nó chỉ truyền tham chiếu đối tượng.


2
Ngữ nghĩa truyền tham số thực sự không liên quan gì đến tình huống như được mô tả.
Michael Myers
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.