Tại sao mã Java này biên dịch?


96

Trong phạm vi phương thức hoặc lớp, dòng bên dưới biên dịch (có cảnh báo):

int x = x = 1;

Trong phạm vi lớp, nơi các biến nhận giá trị mặc định của chúng , lỗi sau gây ra 'tham chiếu không xác định':

int x = x + 1;

Nó không phải là lần đầu tiên x = x = 1kết thúc với cùng một lỗi 'tham chiếu không xác định'? Hoặc có thể dòng thứ hai int x = x + 1nên biên dịch? Hoặc có một cái gì đó tôi đang thiếu?


1
Nếu bạn thêm từ khóa staticvào biến phạm vi lớp, như trong static int x = x + 1;, bạn có gặp lỗi tương tự không? Bởi vì trong C # nó tạo ra sự khác biệt nếu nó tĩnh hoặc không tĩnh.
Jeppe Stig Nielsen

static int x = x + 1không thành công trong Java.
Marcin

1
trong c # cả int a = this.a + 1;int b = 1; int a = b + 1;trong phạm vi lớp (cả hai đều tốt trong Java) không thành công, có thể là do §17.4.5.2 - "Bộ khởi tạo biến cho một trường cá thể không thể tham chiếu cá thể đang được tạo." Tôi không biết nếu nó được cho phép rõ ràng ở đâu đó nhưng tĩnh không có hạn chế như vậy. Trong Java các quy tắc khác nhau và static int x = x + 1thất bại với lý do tương tự mà int x = x + 1không
msam

Nhà cảm xạ với mã bytecode đó sẽ xóa mọi nghi ngờ.
rgripper

Câu trả lời:


101

tl; dr

Đối với các trường , int b = b + 1là bất hợp pháp vì blà tham chiếu chuyển tiếp bất hợp pháp tới b. Bạn thực sự có thể khắc phục điều này bằng cách viết int b = this.b + 1, biên dịch mà không có khiếu nại.

Đối với các biến cục bộ , int d = d + 1là bất hợp pháp vì dkhông được khởi tạo trước khi sử dụng. Đây không phải là trường hợp của các trường luôn được khởi tạo mặc định.

Bạn có thể thấy sự khác biệt bằng cách cố gắng biên dịch

int x = (x = 1) + x;

như một khai báo trường và như một khai báo biến cục bộ. Cái trước sẽ thất bại, nhưng cái sau sẽ thành công, vì sự khác biệt về ngữ nghĩa.

Giới thiệu

Trước hết, các quy tắc cho bộ khởi tạo biến trường và biến cục bộ rất khác nhau. Vì vậy, câu trả lời này sẽ giải quyết các quy tắc trong hai phần.

Chúng tôi sẽ sử dụng chương trình thử nghiệm này trong suốt:

public class test {
    int a = a = 1;
    int b = b + 1;
    public static void Main(String[] args) {
        int c = c = 1;
        int d = d + 1;
    }
}

Khai báo của bkhông hợp lệ và không thành công với một illegal forward referencelỗi.
Khai báo của dkhông hợp lệ và không thành công với một variable d might not have been initializedlỗi.

Thực tế là các lỗi này khác nhau nên gợi ý rằng lý do của các lỗi cũng khác nhau.

Lĩnh vực

Các trình khởi tạo trường trong Java được điều chỉnh bởi JLS §8.3.2 , Khởi tạo trường.

Các phạm vi của một trường được quy định tại JLS §6.3 , Phạm vi của một tuyên bố.

Các quy tắc liên quan là:

  • Phạm vi khai báo của một thành viên được mkhai báo hoặc kế thừa bởi kiểu lớp C (§8.1.6) là toàn bộ phần thân của C, bao gồm mọi khai báo kiểu lồng nhau.
  • Biểu thức khởi tạo cho các biến cá thể có thể sử dụng tên đơn giản của bất kỳ biến tĩnh nào được khai báo trong hoặc kế thừa bởi lớp, ngay cả một biến có khai báo xuất hiện ở dạng văn bản sau đó.
  • Việc sử dụng các biến cá thể có khai báo xuất hiện dưới dạng văn bản sau khi sử dụng đôi khi bị hạn chế, mặc dù các biến cá thể này nằm trong phạm vi. Xem §8.3.2.3 để biết các quy tắc chính xác điều chỉnh tham chiếu chuyển tiếp đến các biến cá thể.

§8.3.2.3 cho biết:

Tuyên bố của một thành viên cần phải xuất hiện dưới dạng văn bản trước khi nó được sử dụng chỉ khi thành viên đó là một trường thể hiện (tương ứng là tĩnh) của một lớp hoặc giao diện C và tất cả các điều kiện sau đây được giữ nguyên:

  • Việc sử dụng xảy ra trong bộ khởi tạo biến thể hiện (tương ứng là tĩnh) của C hoặc trong bộ khởi tạo biến thể (tương ứng là tĩnh) của C.
  • Việc sử dụng không nằm ở phía bên trái của một bài tập.
  • Cách sử dụng là thông qua một cái tên đơn giản.
  • C là lớp trong cùng hoặc giao diện bao quanh việc sử dụng.

Bạn thực sự có thể tham chiếu đến các trường trước khi chúng được khai báo, ngoại trừ một số trường hợp nhất định. Những hạn chế này nhằm ngăn chặn mã như

int j = i;
int i = j;

khỏi biên dịch. Đặc tả Java cho biết "các hạn chế ở trên được thiết kế để bắt, tại thời điểm biên dịch, vòng tròn hoặc các khởi tạo không đúng định dạng khác."

Những quy tắc này thực sự sôi động để làm gì?

Nói tóm lại, các quy tắc về cơ bản nói rằng bạn phải khai báo trước một trường của một tham chiếu đến trường đó nếu (a) tham chiếu nằm trong bộ khởi tạo, (b) tham chiếu không được gán cho, (c) tham chiếu là tên đơn giản (không có định nghĩa như this.) và (d) nó không được truy cập từ bên trong một lớp bên trong. Vì vậy, một tham chiếu chuyển tiếp thỏa mãn tất cả bốn điều kiện là không hợp pháp, nhưng một tham chiếu chuyển tiếp không thành công với ít nhất một điều kiện là OK.

int a = a = 1;biên dịch vì nó vi phạm (b): tham chiếu a đang được chỉ định cho, vì vậy việc tham chiếu atrước akhai báo hoàn chỉnh là hợp pháp .

int b = this.b + 1cũng biên dịch vì nó vi phạm (c): tham chiếu this.bkhông phải là một tên đơn giản (nó đủ điều kiện với this.). Cấu trúc kỳ lạ này vẫn hoàn toàn được xác định rõ ràng, bởi vì this.bcó giá trị bằng không.

Vì vậy, về cơ bản, các hạn chế về tham chiếu trường trong bộ khởi tạo ngăn không cho int a = a + 1biên dịch thành công.

Quan sát rằng khai báo trường int b = (b = 1) + bsẽ không biên dịch được, vì cuối cùng bvẫn là một tham chiếu chuyển tiếp bất hợp pháp.

Biến cục bộ

Khai báo biến cục bộ được điều chỉnh bởi JLS §14.4 , Tuyên bố khai báo biến cục bộ.

Các phạm vi của một biến địa phương được quy định tại JLS §6.3 , Phạm vi của một Tuyên bố:

  • Phạm vi khai báo biến cục bộ trong một khối (§14.4) là phần còn lại của khối trong đó khai báo xuất hiện, bắt đầu với bộ khởi tạo của chính nó và bao gồm bất kỳ bộ khai báo nào khác ở bên phải trong câu lệnh khai báo biến cục bộ.

Lưu ý rằng bộ khởi tạo nằm trong phạm vi của biến được khai báo. Vậy tại sao không int d = d + 1;biên dịch?

Nguyên nhân là do quy tắc gán xác định của Java ( JLS §16 ). Phép gán xác định về cơ bản nói rằng mọi quyền truy cập vào một biến cục bộ phải có một phép gán trước cho biến đó và trình biên dịch Java kiểm tra các vòng lặp và nhánh để đảm bảo rằng phép gán luôn xảy ra trước khi sử dụng (đây là lý do tại sao phép gán xác định có toàn bộ phần đặc tả với nó). Quy tắc cơ bản là:

  • Đối với mọi truy cập của một biến cục bộ hoặc trường cuối cùng trống x, xchắc chắn phải được chỉ định trước khi truy cập, nếu không xảy ra lỗi thời gian biên dịch.

Trong int d = d + 1;, quyền truy cập vào dđược giải quyết cho biến cục bộ, nhưng vì dchưa được chỉ định trước khi dđược truy cập, trình biên dịch gây ra lỗi. Trong int c = c = 1, c = 1xảy ra trước, cái nào chỉ định c, và sau đó cđược khởi tạo thành kết quả của lần gán đó (là 1).

Lưu ý rằng do các quy tắc gán xác định, khai báo biến cục bộ int d = (d = 1) + d; sẽ biên dịch thành công ( không giống như khai báo trường int b = (b = 1) + b), bởi vì dchắc chắn được gán vào thời điểm cuối cùng dđạt được.


+1 cho các tham chiếu, tuy nhiên tôi nghĩ rằng bạn đã hiểu sai từ ngữ này: "int a = a = 1; biên dịch vì nó vi phạm (b)", nếu vi phạm bất kỳ một trong 4 yêu cầu, nó sẽ không biên dịch. Tuy nhiên nó không vì nó ở phía bên tay trái của một bài tập (âm đôi trong cách diễn đạt của JLS không giúp gì nhiều ở đây). Trong câu int b = b + 1b ở bên phải (không phải bên trái) của bài tập vì vậy nó sẽ vi phạm điều này ...
msam

... Điều tôi không quá chắc chắn là những điều sau: 4 điều kiện đó phải được đáp ứng nếu khai báo không xuất hiện ở dạng văn bản trước nhiệm vụ, trong trường hợp này, tôi nghĩ khai báo xuất hiện "dạng văn bản" trước nhiệm vụ int x = x = 1, trong đó trường hợp này không áp dụng.
msam

@msam: Hơi khó hiểu, nhưng về cơ bản bạn phải vi phạm một trong bốn điều kiện để tạo tham chiếu chuyển tiếp. Nếu tham chiếu chuyển tiếp của bạn đáp ứng tất cả bốn điều kiện, nó là bất hợp pháp.
nneonneo

@msam: Ngoài ra, khai báo đầy đủ chỉ có hiệu lực sau trình khởi tạo.
nneonneo

@mrfishie: Câu trả lời lớn, nhưng có một lượng sâu đáng ngạc nhiên trong thông số Java. Câu hỏi không quá đơn giản như bề ngoài. (Tôi đã từng viết một tập hợp con của trình biên dịch Java, vì vậy tôi khá quen thuộc với nhiều phần trong và ngoài của JLS).
nneonneo

86
int x = x = 1;

tương đương với

int x = 1;
x = x; //warning here

trong khi ở

int x = x + 1; 

đầu tiên chúng ta cần tính toán x+1nhưng giá trị của x không được biết nên bạn sẽ gặp lỗi (trình biên dịch biết rằng giá trị của x không được biết)


4
Điều này cộng với gợi ý về sự kết hợp đúng đắn từ OpenSauce, tôi thấy rất hữu ích.
TobiMcNamobi

1
Tôi nghĩ rằng giá trị trả về của một phép gán là giá trị đang được gán, không phải giá trị biến.
zzzzBov

2
@zzzzBov là chính xác. int x = x = 1;tương đương với int x = (x = 1), không phải x = 1; x = x; . Bạn sẽ không nhận được cảnh báo trình biên dịch khi thực hiện việc này.
nneonneo

int x = x = 1;s tương đương với int x = (x = 1)vì phải kết hợp của =nhà điều hành
Grijesh Chauhan

1
@nneonneo và int x = (x = 1)tương đương với int x; x = 1; x = x;(khai báo biến, đánh giá trình khởi tạo trường, gán biến cho kết quả đánh giá đã nói), do đó cảnh báo
msam

41

Nó gần tương đương với:

int x;
x = 1;
x = 1;

Thứ nhất, int <var> = <expression>;luôn luôn tương đương với

int <var>;
<var> = <expression>;

Trong trường hợp này, biểu thức của bạn là x = 1, cũng là một câu lệnh. x = 1là một câu lệnh hợp lệ, vì var xđã được khai báo. Nó cũng là một biểu thức có giá trị 1, sau đó được gán xlại cho.


Được rồi, nhưng nếu nó diễn ra như bạn nói, tại sao trong phạm vi lớp, câu lệnh thứ hai lại báo lỗi? Ý tôi là bạn nhận 0giá trị mặc định cho số nguyên, vì vậy tôi mong đợi kết quả là 1, không phải undefined reference.
Marcin

Hãy xem câu trả lời @izogfif. Có vẻ như đang hoạt động, vì trình biên dịch C ++ gán giá trị mặc định cho các biến. Tương tự như cách java làm đối với các biến cấp lớp.
Marcin

@Marcin: trong Java, int không được khởi tạo bằng 0 khi chúng là biến cục bộ. Chúng chỉ được khởi tạo bằng 0 nếu chúng là biến thành viên. Vì vậy, trong dòng thứ hai của bạn, x + 1không có giá trị xác định, vì xchưa được khởi tạo.
OpenSauce

1
@OpenSauce Nhưng x được định nghĩa là một biến thành viên ("trong phạm vi lớp").
Jacob Raihle

@JacobRaihle: Ah ok, không phát hiện ra phần đó. Tôi không chắc chắn rằng mã bytecode để khởi tạo một var thành 0 sẽ được tạo bởi trình biên dịch nếu nó thấy có một hướng dẫn khởi tạo rõ ràng. Có một bài viết ở đây đi vào một số chi tiết về khởi tạo lớp và đối tượng, mặc dù tôi không nghĩ nó giải quyết vấn đề chính xác này: javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce

12

Trong java hoặc trong bất kỳ ngôn ngữ hiện đại nào, phép gán đến từ bên phải.

Giả sử nếu bạn đang có hai biến x và y,

int z = x = y = 5;

Câu lệnh này hợp lệ và đây là cách trình biên dịch phân tách chúng.

y = 5;
x = y;
z = x; // which will be 5

Nhưng trong trường hợp của bạn

int x = x + 1;

Trình biên dịch đã đưa ra một ngoại lệ bởi vì, nó phân tách như thế này.

x = 1; // oops, it isn't declared because assignment comes from the right.

cảnh báo là x = x không x = 1
Asim Ghaffar

8

int x = x = 1; Không bằng:

int x;
x = 1;
x = x;

javap giúp chúng tôi một lần nữa, đây là hướng dẫn JVM được tạo cho mã này:

0: iconst_1    //load constant to stack
1: dup         //duplicate it
2: istore_1    //set x to constant
3: istore_1    //set x to constant

giống như:

int x = 1;
x = 1;

Đây không có lý do gì để ném lỗi tham chiếu không xác định. Hiện tại đã có cách sử dụng biến trước khi nó khởi chạy, vì vậy mã này hoàn toàn tuân thủ đặc điểm kỹ thuật. Trong thực tế, không có cách sử dụng biến nào cả , chỉ là các phép gán. Và trình biên dịch JIT sẽ còn tiến xa hơn nữa, nó sẽ loại bỏ các cấu trúc như vậy. Thành thật mà nói, tôi không hiểu mã này được kết nối như thế nào với đặc tả của JLS về cách khởi tạo và sử dụng biến. Không sử dụng không có vấn đề. ;)

Vui lòng sửa nếu tôi sai. Tôi không thể tìm ra lý do tại sao các câu trả lời khác, đề cập đến nhiều đoạn văn JLS thu thập nhiều điểm cộng như vậy. Những đoạn này không có điểm chung nào với trường hợp này. Chỉ là hai nhiệm vụ nối tiếp và không hơn.

Nếu chúng ta viết:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;

bằng:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5

Hầu hết các biểu thức bên phải chỉ được gán cho từng biến một, không có bất kỳ đệ quy nào. Chúng ta có thể xáo trộn các biến theo bất kỳ cách nào chúng ta muốn:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;

7

Trong int x = x + 1;bạn thêm 1 vào x, vì vậy giá trị của là gì x, nó vẫn chưa được tạo ra.

Nhưng trong int x=x=1;sẽ biên dịch không có lỗi vì bạn gán 1 cho x.


5

Đoạn mã đầu tiên của bạn chứa một mã thứ hai =thay vì một dấu cộng. Điều này sẽ biên dịch ở bất cứ đâu trong khi đoạn mã thứ hai sẽ không biên dịch ở cả hai nơi.


5

Trong đoạn mã thứ hai, x được sử dụng trước phần khai báo của nó, trong khi ở đoạn mã đầu tiên, nó chỉ được gán hai lần, điều này không có ý nghĩa nhưng hợp lệ.


5

Hãy chia nhỏ nó ra từng bước, phù hợp

int x = x = 1

x = 1, gán 1 cho một biến x

int x = x, gán giá trị x cho chính nó, dưới dạng int. Vì x trước đây đã được gán là 1 nên nó vẫn giữ nguyên 1, mặc dù theo kiểu thừa.

Điều đó biên dịch tốt.

int x = x + 1

x + 1, thêm một vào một biến x. Tuy nhiên, x không được xác định, điều này sẽ gây ra lỗi biên dịch.

int x = x + 1, do đó, dòng này biên dịch lỗi vì phần bên phải của dấu bằng sẽ không biên dịch thêm một vào một biến chưa được gán


Không, nó là phép kết hợp phải khi có hai =toán tử, vì vậy nó giống như int x = (x = 1);.
Jeppe Stig Nielsen

Ah, lệnh của tôi tắt. Xin lỗi vì điều đó. Lẽ ra họ phải làm ngược lại. Tôi đã thay đổi nó ngay bây giờ.
steventnorris

3

Cái thứ hai int x=x=1là biên dịch vì bạn đang gán giá trị cho x nhưng trong trường hợp khác int x=x+1ở đây biến x không được khởi tạo, Hãy nhớ trong java biến cục bộ không được khởi tạo thành giá trị mặc định. Lưu ý Nếu nó cũng ( int x=x+1) trong phạm vi lớp thì nó cũng sẽ đưa ra lỗi biên dịch vì biến không được tạo.


2
int x = x + 1;

biên dịch thành công trong Visual Studio 2008 với cảnh báo

warning C4700: uninitialized local variable 'x' used`

2
Bắt giữ. Có phải là C / C ++ không?
Marcin

@Marcin: vâng, nó là C ++. @msam: xin lỗi, tôi nghĩ rằng tôi đã thấy thẻ cthay vì javanhưng rõ ràng đó là câu hỏi khác.
izogfif

Nó biên dịch vì trong C ++, trình biên dịch gán giá trị mặc định cho các kiểu nguyên thủy. Sử dụng bool y;y==truesẽ trả về false.
Sri Harsha Chilakapati

@SriHarshaChilakapati, nó có phải là một loại tiêu chuẩn trong trình biên dịch C ++ không? Bởi vì khi tôi biên dịch void main() { int x = x + 1; printf("%d ", x); }trong Visual Studio 2008, trong Gỡ lỗi, tôi nhận được ngoại lệ Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.và trong Bản phát hành, tôi nhận được số 1896199921được in trong bảng điều khiển.
izogfif

1
@SriHarshaChilakapati Nói về các ngôn ngữ khác: Trong C #, đối với một statictrường (biến tĩnh cấp lớp), các quy tắc tương tự cũng được áp dụng. Ví dụ một trường được khai báo là public static int x = x + 1;biên dịch mà không có cảnh báo trong Visual C #. Có thể giống nhau trong Java?
Jeppe Stig Nielsen

2

x không được khởi tạo trong x = x + 1;.

Ngôn ngữ lập trình Java được định kiểu tĩnh, có nghĩa là tất cả các biến phải được khai báo trước khi chúng có thể được sử dụng.

Xem các kiểu dữ liệu nguyên thủy


3
Nhu cầu khởi tạo các biến trước khi sử dụng giá trị của chúng không liên quan gì đến việc nhập tĩnh. Kiểu kiểu tĩnh: bạn cần khai báo kiểu biến là gì. Khởi tạo trước khi sử dụng: nó cần phải có một giá trị cho phép trước khi bạn có thể sử dụng giá trị đó.
Jon Bright

@JonBright: Việc cần khai báo các loại biến cũng không liên quan gì đến việc nhập tĩnh. Ví dụ, có những ngôn ngữ được nhập kiểu tĩnh với kiểu suy luận.
hammar

@hammar, theo cách tôi thấy, bạn có thể lập luận theo hai cách: với kiểu suy luận, bạn đang ngầm khai báo kiểu của biến theo cách mà hệ thống có thể suy ra. Hoặc, kiểu suy luận là cách thứ ba, trong đó các biến không được nhập động trong thời gian chạy, nhưng ở mức nguồn, tùy thuộc vào việc sử dụng chúng và các suy luận được thực hiện. Dù bằng cách nào, tuyên bố vẫn đúng. Nhưng bạn nói đúng, tôi không nghĩ về các hệ thống loại khác.
Jon Bright

2

Dòng mã không biên dịch với cảnh báo vì cách mã thực sự hoạt động. Khi bạn chạy mã int x = x = 1, Java trước tiên sẽ tạo biến x, như đã định nghĩa. Sau đó, nó chạy mã gán ( x = 1). Vì xđã được xác định, hệ thống không có lỗi khi đặt xthành 1. Giá trị này trả về giá trị 1, vì đó bây giờ là giá trị của x. Therefor, xbây giờ cuối cùng được đặt là 1.
Về cơ bản Java thực thi mã như thể nó là:

int x;
x = (x = 1); // (x = 1) returns 1 so there is no error

Tuy nhiên, trong đoạn mã thứ hai của bạn int x = x + 1, + 1câu lệnh yêu cầu xphải được xác định, sau đó thì không. Vì các câu lệnh gán luôn có nghĩa là mã ở bên phải của =được chạy đầu tiên, mã sẽ bị lỗi vì không xđược xác định. Java sẽ chạy mã như sau:

int x;
x = x + 1; // this line causes the error because `x` is undefined

-1

Máy tính đọc các câu lệnh từ phải sang trái và chúng tôi thiết kế để làm điều ngược lại. Đó là lý do tại sao nó khó chịu lúc đầu. Hãy biến điều này thành một habbit để đọc các câu lệnh (mã) từ phải sang trái, bạn sẽ không gặp vấn đề như vậy.

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.