Là cuối cùng không xác định?


186

Đầu tiên, một câu đố: Mã sau đây in gì?

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10);

    private static long scale(long value) {
        return X * value;
    }
}

Câu trả lời:

0

Kẻ phá hoại dưới đây.


Nếu bạn in Xvề quy mô (dài) và xác định lại X = scale(10) + 3, các bản in sẽ X = 0sau đó X = 3. Điều này có nghĩa Xlà tạm thời được đặt thành 0và sau đó được đặt thành 3. Đây là một vi phạm final!

Công cụ sửa đổi tĩnh, kết hợp với công cụ sửa đổi cuối cùng, cũng được sử dụng để xác định các hằng số. Công cụ sửa đổi cuối cùng chỉ ra rằng giá trị của trường này không thể thay đổi .

Nguồn: https://docs.oracle.com/javase/tutorial/java/javaOO/ classvars.html [nhấn mạnh thêm]


Câu hỏi của tôi: Đây có phải là một lỗi? Là finalkhông xác định?


Đây là mã mà tôi quan tâm. XĐược gán hai giá trị khác nhau: 03. Tôi tin rằng điều này là vi phạm final.

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10) + 3;

    private static long scale(long value) {
        System.out.println("X = " + X);
        return X * value;
    }
}

Câu hỏi này đã được gắn cờ là một bản sao có thể có của thứ tự khởi tạo trường tĩnh cuối cùng của Java . Tôi tin rằng câu hỏi này không phải là một bản sao vì câu hỏi khác giải quyết thứ tự khởi tạo trong khi câu hỏi của tôi giải quyết việc khởi tạo theo chu kỳ kết hợp với finalthẻ. Từ câu hỏi khác, tôi không thể hiểu tại sao mã trong câu hỏi của tôi không gây ra lỗi.

Điều này đặc biệt rõ ràng bằng cách nhìn vào đầu ra mà ernesto nhận được: khi ađược gắn thẻ final, anh ta nhận được đầu ra sau:

a=5
a=5

mà không liên quan đến phần chính của câu hỏi của tôi: Làm thế nào một finalbiến thay đổi biến của nó?


17
Cách tham chiếu Xthành viên này giống như tham chiếu đến một thành viên lớp con trước khi trình xây dựng siêu hạng kết thúc, đó là vấn đề của bạn chứ không phải định nghĩa final.
daniu

4
Từ JLS:A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Ivan

1
@Ivan, Đây không phải là về hằng số mà là về biến thể hiện. Nhưng bạn có thể thêm chương?
AxelH

9
Cũng như một lưu ý: Không bao giờ thực hiện bất kỳ điều này trong mã sản xuất. Thật khó hiểu cho mọi người nếu ai đó bắt đầu khai thác sơ hở trong JLS.
Zabuzard

13
FYI bạn cũng có thể tạo chính xác tình huống này trong C #. C # hứa rằng các vòng lặp trong khai báo liên tục sẽ bị bắt tại thời điểm biên dịch, nhưng không đưa ra lời hứa nào về khai báo chỉ đọc và trong thực tế, bạn có thể gặp tình huống trong đó giá trị 0 của trường được quan sát bởi trình khởi tạo trường khác. Nếu nó đau khi bạn làm điều đó, đừng làm điều đó . Trình biên dịch sẽ không cứu bạn.
Eric Lippert

Câu trả lời:


217

Một phát hiện rất thú vị. Để hiểu nó, chúng ta cần đi sâu vào Đặc tả ngôn ngữ Java ( JLS ).

Lý do là finalchỉ cho phép một nhiệm vụ . Giá trị mặc định, tuy nhiên, không có sự phân công . Trong thực tế, mọi biến như vậy ( biến lớp, biến thể hiện, thành phần mảng) đều trỏ đến giá trị mặc định của nó ngay từ đầu, trước khi gán . Nhiệm vụ đầu tiên sau đó thay đổi tham chiếu.


Biến lớp và giá trị mặc định

Hãy xem ví dụ sau:

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

Chúng tôi đã không chỉ định rõ ràng một giá trị cho x, mặc dù nó trỏ đến null, đó là giá trị mặc định. So sánh với §4.12.5 :

Giá trị ban đầu của biến

Mỗi biến lớp , biến thể hiện hoặc thành phần mảng được khởi tạo với giá trị mặc định khi nó được tạo ( §15.9 , §15.10.2 )

Lưu ý rằng điều này chỉ giữ cho các loại biến, như trong ví dụ của chúng tôi. Nó không giữ cho các biến cục bộ, xem ví dụ sau:

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

Từ cùng một đoạn JLS:

Một biến cục bộ ( §14.4 , §14,14 ) phải được cung cấp một giá trị rõ ràng trước khi nó được sử dụng, bằng cách khởi tạo ( §14.4 ) hoặc gán ( §15,26 ), theo cách có thể được xác minh bằng cách sử dụng quy tắc cho phép gán xác định ( § 16 (Bài tập xác định) ).


Biến cuối cùng

Bây giờ chúng ta hãy xem final, từ §4.12.4 :

biến cuối cùng

Một biến có thể được khai báo cuối cùng . Một biến cuối cùng chỉ có thể được gán cho một lần . Đó là lỗi thời gian biên dịch nếu một biến cuối cùng được gán cho trừ khi nó chắc chắn không được gán ngay trước khi gán ( §16 (Chuyển nhượng xác định) ).


Giải trình

Bây giờ trở lại ví dụ của bạn, sửa đổi một chút:

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

Nó xuất ra

Before: 0
After: 1

Nhớ lại những gì chúng ta đã học. Bên trong phương pháp assignbiến Xđã không được gán một giá trị cho được nêu ra. Do đó, nó trỏ đến giá trị mặc định của nó vì nó là một biến lớp và theo JLS, các biến đó luôn ngay lập tức trỏ đến các giá trị mặc định của chúng (trái ngược với các biến cục bộ). Sau assignphương thức, biến Xđược gán giá trị 1và vì finalchúng ta không thể thay đổi nó nữa. Vì vậy, những điều sau đây sẽ không hoạt động do final:

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

Ví dụ trong JLS

Nhờ @Andrew tôi đã tìm thấy một đoạn JLS bao gồm chính xác kịch bản này, nó cũng thể hiện điều đó.

Nhưng trước tiên hãy xem

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

Tại sao điều này không được phép, trong khi truy cập từ phương thức là? Hãy xem §8.3.3 nói về thời điểm truy cập vào các trường bị hạn chế nếu trường chưa được khởi tạo.

Nó liệt kê một số quy tắc liên quan đến các biến lớp:

Đối với một tham chiếu theo tên đơn giản cho một biến lớp được fkhai báo trong lớp hoặc giao diện C, đó là lỗi thời gian biên dịch nếu :

  • Tham chiếu xuất hiện trong bộ khởi tạo biến lớp Choặc trong bộ khởi tạo tĩnh của C( §8.7 ); và

  • Tham chiếu xuất hiện trong trình khởi tạo của công cụ fkhai báo riêng hoặc tại một điểm bên trái của công cụ fkhai báo; và

  • Tham chiếu không nằm ở phía bên trái của biểu thức gán ( §15,26 ); và

  • Lớp trong cùng hoặc giao diện kèm theo tham chiếu là C.

Thật đơn giản, X = X + 1bị bắt bởi những quy tắc đó, phương thức truy cập không. Họ thậm chí liệt kê kịch bản này và đưa ra một ví dụ:

Truy cập bằng các phương thức không được kiểm tra theo cách này, vì vậy:

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

tạo ra đầu ra:

0

bởi vì trình khởi tạo biến để isử dụng phương thức peek để truy cập giá trị của biến jtrước đó jđã được khởi tạo bởi trình khởi tạo biến của nó, tại thời điểm đó nó vẫn có giá trị mặc định ( §4.12.5 ).


1
@Andrew Có, biến lớp, cảm ơn. Vâng, nó sẽ hoạt động nếu không có một số quy tắc bổ sung hạn chế quyền truy cập đó: §8.3.3 . Hãy xem bốn điểm được chỉ định cho các biến lớp (mục nhập đầu tiên). Cách tiếp cận phương thức trong ví dụ OP không bị bắt bởi các quy tắc đó, do đó chúng ta có thể truy cập Xtừ phương thức. Tôi sẽ không bận tâm nhiều như vậy. Nó chỉ phụ thuộc vào cách chính xác JLS định nghĩa mọi thứ để hoạt động chi tiết. Tôi sẽ không bao giờ sử dụng mã như vậy, nó chỉ khai thác một số quy tắc trong JLS.
Zabuzard

4
Vấn đề là bạn có thể gọi các phương thức cá thể từ hàm tạo, một cái gì đó có lẽ không được phép. Mặt khác, việc chỉ định người dân địa phương trước khi gọi siêu, sẽ hữu ích và an toàn, không được phép. Đi hình.
Phục hồi Monica

1
@Andrew bạn có lẽ là người duy nhất ở đây thực sự được đề cập forwards references(đó cũng là một phần của JLS). Điều này thật đơn giản nếu không có câu trả lời looong stackoverflow.com/a/49371279/1059372
Eugene

1
"Nhiệm vụ đầu tiên sau đó thay đổi tham chiếu." Trong trường hợp này, nó không phải là kiểu tham chiếu, mà là kiểu nguyên thủy.
fabian

1
Câu trả lời này là đúng, nếu hơi lâu. :-) Tôi nghĩ rằng tl; dr là OP đã trích dẫn một hướng dẫn nói rằng "trường [cuối cùng] không thể thay đổi", chứ không phải JLS. Mặc dù hướng dẫn của Oracle khá tốt, nhưng chúng không bao gồm tất cả các trường hợp cạnh. Đối với câu hỏi của OP, chúng ta cần đi đến định nghĩa JLS thực tế cuối cùng - và định nghĩa đó không đưa ra yêu cầu (rằng OP thách thức chính đáng) rằng giá trị của trường cuối cùng không bao giờ có thể thay đổi.
yshavit

22

Không có gì để làm với trận chung kết ở đây.

Vì nó ở mức cá thể hoặc cấp lớp, nó giữ giá trị mặc định nếu chưa có gì được gán. Đó là lý do bạn thấy0 khi bạn truy cập nó mà không cần gán.

Nếu bạn truy cập Xmà không gán hoàn toàn, nó sẽ giữ các giá trị mặc định dài 0, do đó sẽ có kết quả.


3
Điều khó khăn ở đây là nếu bạn không gán giá trị, nó sẽ không được gán với giá trị mặc định, nhưng nếu bạn sử dụng nó để tự gán giá trị "cuối cùng", nó sẽ ...
AxelH

2
@AxelH Tôi hiểu ý của bạn là gì Nhưng đó là cách nó hoạt động nếu không thế giới sụp đổ;).
Suresh Atta

20

Không phải là một lỗi.

Khi cuộc gọi đầu tiên scaleđược gọi từ

private static final long X = scale(10);

Nó cố gắng đánh giá return X * value. Xchưa được gán một giá trị và do đó, giá trị mặc định cho a longđược sử dụng (đó là0 ).

Vì vậy, dòng mã đó ước tính có X * 10nghĩa 0 * 100.


8
Tôi không nghĩ đó là điều OP nhầm lẫn. Những gì nhầm lẫn là X = scale(10) + 3. Vì X, khi được tham chiếu từ phương thức, là 0. Nhưng sau đó là nó 3. Vì vậy, OP nghĩ rằng Xđược gán hai giá trị khác nhau, sẽ xung đột với final.
Zabuzard

4
@Zabuza không giải thích điều này với " Nó cố gắng đánh giá return X * value. XChưa được gán một giá trị và do đó lấy giá trị mặc định cho giá trị longđó 0. "? Người ta không nói Xđược gán với giá trị mặc định mà Xlà "được thay thế" (vui lòng không trích dẫn thuật ngữ đó;)) bằng giá trị mặc định.
AxelH

14

Nó hoàn toàn không phải là một lỗi, chỉ đơn giản là nó không phải là một hình thức tham chiếu chuyển tiếp bất hợp pháp , không có gì hơn thế.

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

Nó chỉ đơn giản là cho phép bởi Đặc điểm kỹ thuật.

Lấy ví dụ của bạn, đây chính xác là nơi phù hợp với điều này:

private static final long X = scale(10) + 3;

Bạn đang thực hiện một tham chiếu chuyển tiếp đến scaleđó không phải là bất hợp pháp theo bất kỳ cách nào như đã nói trước đây, nhưng cho phép bạn có được giá trị mặc định làX . một lần nữa, điều này được Spec cho phép (chính xác hơn là nó không bị cấm), vì vậy nó hoạt động tốt


câu trả lời tốt! Tôi chỉ tò mò về lý do tại sao thông số kỹ thuật cho phép trường hợp thứ hai biên dịch. Đây có phải là cách duy nhất để thấy trạng thái "không nhất quán" của trường cuối cùng không?
Andrew Tobilko

@Andrew điều này đã làm phiền tôi khá nhiều thời gian, tôi có xu hướng nghĩ rằng C ++ hoặc C làm điều đó (không biết điều này có đúng không)
Eugene

@Andrew: Bởi vì làm khác đi sẽ là giải quyết định lý bất toàn Turing.
Joshua

9
@Joshua: Tôi nghĩ rằng bạn đang trộn lẫn một số khái niệm khác nhau ở đây: (1) vấn đề tạm dừng, (2) vấn đề quyết định, (3) Định lý không hoàn chỉnh của Godel và (4) Ngôn ngữ lập trình hoàn chỉnh. Người viết trình biên dịch không cố gắng giải quyết vấn đề "biến này có được gán chắc chắn trước khi nó được sử dụng không?" hoàn toàn bởi vì vấn đề đó tương đương với việc giải quyết vấn đề dừng và chúng tôi biết rằng chúng tôi không thể làm như vậy.
Eric Lippert

4
@EricLippert: Haha ôi. Turing không đầy đủ và vấn đề tạm dừng chiếm cùng một vị trí trong tâm trí của tôi.
Joshua

4

Các thành viên cấp lớp có thể được khởi tạo bằng mã trong định nghĩa lớp. Mã byte được biên dịch không thể khởi tạo nội tuyến thành viên lớp. (Các thành viên sơ thẩm được xử lý tương tự, nhưng điều này không liên quan đến câu hỏi được cung cấp.)

Khi một người viết một cái gì đó như sau:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

Mã byte được tạo sẽ tương tự như sau:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

Mã khởi tạo được đặt trong một trình khởi tạo tĩnh được chạy khi trình nạp lớp đầu tiên tải lớp. Với kiến ​​thức này, mẫu ban đầu của bạn sẽ tương tự như sau:

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVM tải RecursiveStatic làm điểm vào của jar.
  2. Trình nạp lớp chạy trình khởi tạo tĩnh khi định nghĩa lớp được tải.
  3. Trình khởi tạo gọi hàm scale(10)để gán static finaltrường X.
  4. Các scale(long)chức năng chạy trong khi lớp là một phần khởi đọc giá trị chưa được khởi tạo củaX đó là mặc định của dài hoặc 0.
  5. Giá trị của 0 * 10được gán choX và trình nạp lớp hoàn thành.
  6. JVM chạy phương thức chính void void công khai gọi scale(5)nhân 5 với Xgiá trị khởi tạo bây giờ là 0 trả về 0.

Trường cuối cùng tĩnh Xchỉ được chỉ định một lần, duy trì bảo đảm được giữ bởi finaltừ khóa. Đối với truy vấn tiếp theo về việc thêm 3 trong bài tập, bước 5 ở trên trở thành đánh 0 * 10 + 3giá giá trị nào 3và phương thức chính sẽ in kết quả 3 * 5là giá trị 15.


3

Đọc một trường chưa được khởi tạo của một đối tượng phải dẫn đến một lỗi biên dịch. Thật không may cho Java, nó không.

Tôi nghĩ lý do cơ bản tại sao trường hợp này lại "ẩn" sâu trong định nghĩa về cách các đối tượng được khởi tạo và xây dựng, mặc dù tôi không biết chi tiết về tiêu chuẩn.

Theo một nghĩa nào đó, cuối cùng là không xác định bởi vì nó thậm chí không hoàn thành mục đích đã nêu là do vấn đề này. Tuy nhiên, nếu tất cả các lớp học của bạn được viết đúng, bạn không gặp phải vấn đề này. Có nghĩa là tất cả các trường luôn được đặt trong tất cả các hàm tạo và không có đối tượng nào được tạo mà không gọi một trong các hàm tạo của nó. Điều đó có vẻ tự nhiên cho đến khi bạn phải sử dụng một thư viện tuần tự hóa.

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.