Vòng lặp vô hạn trong Java


82

Nhìn vào whilevòng lặp vô hạn sau trong Java. Nó gây ra lỗi thời gian biên dịch cho câu lệnh bên dưới nó.

while(true) {
    System.out.println("inside while");
}

System.out.println("while terminated"); //Unreachable statement - compiler-error.

whileTuy nhiên, cùng một vòng lặp vô hạn sau đây hoạt động tốt và không gây ra bất kỳ lỗi nào mà tôi vừa thay thế điều kiện bằng một biến boolean.

boolean b=true;

while(b) {
    System.out.println("inside while");
}

System.out.println("while terminated"); //No error here.

Trong trường hợp thứ hai, câu lệnh sau vòng lặp rõ ràng là không thể truy cập được vì biến boolean blà true, trình biên dịch không phàn nàn gì cả. Tại sao?


Chỉnh sửa: Phiên bản sau của whilebị mắc kẹt vào một vòng lặp vô hạn là hiển nhiên nhưng không có lỗi trình biên dịch nào cho câu lệnh bên dưới nó mặc dù ifđiều kiện trong vòng lặp luôn luôn falsevà do đó, vòng lặp không bao giờ có thể trở lại và có thể được xác định bởi trình biên dịch tại thời gian biên dịch của chính nó.

while(true) {

    if(false) {
        break;
    }

    System.out.println("inside while");
}

System.out.println("while terminated"); //No error here.

while(true) {

    if(false)  { //if true then also
        return;  //Replacing return with break fixes the following error.
    }

    System.out.println("inside while");
}

System.out.println("while terminated"); //Compiler-error - unreachable statement.

while(true) {

    if(true) {
        System.out.println("inside if");
        return;
    }

    System.out.println("inside while"); //No error here.
}

System.out.println("while terminated"); //Compiler-error - unreachable statement.

Chỉnh sửa: Điều tương tự với ifwhile.

if(false) {
    System.out.println("inside if"); //No error here.
}

while(false) {
    System.out.println("inside while");
    // Compiler's complain - unreachable statement.
}

while(true) {

    if(true) {
        System.out.println("inside if");
        break;
    }

    System.out.println("inside while"); //No error here.
}      

Phiên bản sau của whilecũng bị mắc kẹt vào một vòng lặp vô hạn.

while(true) {

    try {
        System.out.println("inside while");
        return;   //Replacing return with break makes no difference here.
    } finally {
        continue;
    }
}

Điều này là do finallykhối luôn được thực thi mặc dù returncâu lệnh gặp phải trước nó trong trychính khối.


46
Ai quan tâm? Nó rõ ràng chỉ là một tính năng của trình biên dịch. Đừng lo lắng về những thứ này.
CJ7

17
Làm cách nào một luồng khác có thể thay đổi một biến không tĩnh cục bộ?
CJ7

4
Trạng thái bên trong của đối tượng có thể được thay đổi đồng thời thông qua phản xạ. Đó là lý do tại sao JLS bắt buộc chỉ kiểm tra các biểu thức cuối cùng (hằng số).
lsoliveira

5
Tôi ghét những lỗi ngu ngốc này. Mã không thể truy cập phải là một cảnh báo không phải là một lỗi.
user606723

2
@ CJ7: Tôi sẽ không gọi đây là một "tính năng", và nó khiến việc triển khai một trình biên dịch Java phù hợp trở nên rất tẻ nhạt (không có lý do). Tận hưởng khóa nhà cung cấp theo thiết kế của bạn.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Câu trả lời:


105

Trình biên dịch có thể dễ dàng và rõ ràng chứng minh rằng biểu thức đầu tiên luôn dẫn đến một vòng lặp vô hạn, nhưng nó không dễ dàng như vậy đối với biểu thức thứ hai. Trong ví dụ đồ chơi của bạn, nó đơn giản, nhưng nếu:

  • nội dung của biến đã được đọc từ một tệp?
  • biến không cục bộ và có thể được sửa đổi bởi một chủ đề khác?
  • biến dựa trên một số đầu vào của người dùng?

Trình biên dịch rõ ràng không kiểm tra trường hợp đơn giản hơn của bạn vì nó hoàn toàn bỏ qua con đường đó. Tại sao? Bởi vì nó khó bị cấm hơn nhiều bởi thông số kỹ thuật. Xem phần 14.21 :

(Bằng cách này, trình biên dịch của tôi không phàn nàn khi biến được khai báo final.)


25
-1 - Nó không phải là về những gì trình biên dịch có thể làm. Không có gì là kiểm tra dễ dàng hơn hay khó hơn. Đó là về những gì trình biên dịch được phép làm ... bởi Đặc tả ngôn ngữ Java. Phiên bản thứ hai của mã là Java hợp lệ theo JLS, do đó, lỗi biên dịch sẽ bị sai.
Stephen C

10
@StephenC - Cảm ơn vì thông tin đó. Hạnh phúc được cập nhật để phản ánh càng nhiều.
Wayne

Còn -1; không có "điều gì nếu" của bạn áp dụng ở đây. Như vậy, trình biên dịch thực sự có thể giải quyết vấn đề - trong thuật ngữ phân tích tĩnh, điều này được gọi là sự lan truyền không đổi và được sử dụng rộng rãi thành công trong nhiều tình huống khác. JLS là lý do duy nhất ở đây, và vấn đề khó giải quyết như thế nào là không quan trọng. Tất nhiên, lý do JLS được viết theo cách đó ngay từ đầu có thể liên quan đến độ khó của vấn đề đó, mặc dù cá nhân tôi nghi ngờ đó thực sự là vì rất khó thực thi một giải pháp lan truyền hằng số được chuẩn hóa trên các trình biên dịch khác nhau.
Oak

2
@Oak - Tôi đã thừa nhận trong câu trả lời của mình rằng "Trong ví dụ đồ chơi của [OP] thì nó đơn giản" và dựa trên nhận xét của Stephen, rằng JLS là yếu tố hạn chế. Tôi khá chắc chắn rằng chúng tôi đồng ý.
Wayne

55

Theo các thông số kỹ thuật , sau đây được nói về câu lệnh while.

Một câu lệnh while có thể hoàn thành bình thường và ít nhất một trong những điều sau là đúng:

  • Câu lệnh while có thể truy cập được và biểu thức điều kiện không phải là một biểu thức hằng có giá trị true.
  • Có một câu lệnh break có thể truy cập được thoát khỏi câu lệnh while. \

Vì vậy, trình biên dịch sẽ chỉ nói rằng đoạn mã theo sau câu lệnh while là không thể truy cập được nếu điều kiện while là một hằng số có giá trị đúng hoặc có một câu lệnh break trong lệnh while. Trong trường hợp thứ hai, vì giá trị của b không phải là một hằng số, nó không coi đoạn mã theo sau nó là không thể truy cập được. Có rất nhiều thông tin đằng sau liên kết đó để cung cấp cho bạn thêm chi tiết về những gì được và những gì không được coi là không thể truy cập.


3
+1 vì lưu ý rằng đó không chỉ là những gì người viết trình biên dịch có thể hoặc không thể nghĩ ra - JLS cho họ biết những gì họ có thể và không thể coi là không thể tiếp cận.
yshavit

14

Vì true là hằng số và b có thể được thay đổi trong vòng lặp.


1
Đúng, nhưng không liên quan, vì b KHÔNG được thay đổi trong vòng lặp. Bạn cũng có thể tranh luận rằng vòng lặp sử dụng true có thể bao gồm một câu lệnh break.
deworde

@deworde - miễn là nó có thể bị thay đổi (bằng một luồng khác, chẳng hạn) thì trình biên dịch không thể gọi nó là lỗi. Bạn không thể biết chỉ bằng cách nhìn vào chính vòng lặp liệu b có được thay đổi trong khi vòng lặp đang chạy hay không.
Peter Recore

10

Bởi vì việc phân tích trạng thái biến rất khó, vì vậy trình biên dịch đã bỏ qua khá nhiều thứ và để bạn làm những gì bạn muốn. Ngoài ra, Đặc tả ngôn ngữ Java có các quy tắc rõ ràng về cách trình biên dịch được phép phát hiện mã không thể truy cập .

Có nhiều cách để đánh lừa trình biên dịch - một ví dụ phổ biến khác là

public void test()
{
    return;
    System.out.println("Hello");
}

mà sẽ không hoạt động, vì trình biên dịch sẽ nhận ra rằng khu vực này không thể truy cập được. Thay vào đó, bạn có thể làm

public void test()
{
    if (2 > 1) return;
    System.out.println("Hello");
}

Điều này sẽ hoạt động vì trình biên dịch không thể nhận ra rằng biểu thức sẽ không bao giờ sai.


6
Như các câu trả lời khác đã nêu, điều này không liên quan (trực tiếp) đến việc trình biên dịch có thể dễ dàng hoặc khó phát hiện mã không thể truy cập được. JLS có độ dài lớn để chỉ định các quy tắc phát hiện các câu lệnh không thể truy cập. Theo các quy tắc đó, ví dụ đầu tiên không phải là Java hợp lệ và ví dụ thứ hai là Java hợp lệ. Đó là nó. Trình biên dịch chỉ thực hiện các quy tắc như được chỉ định.
Stephen C

Tôi không tuân theo đối số JLS. Các trình biên dịch có thực sự bắt buộc phải chuyển tất cả Java hợp pháp thành mã byte không? ngay cả khi nó có thể được chứng minh là vô nghĩa.
emory

@emory Có, đó sẽ là định nghĩa của một trình duyệt tuân thủ tiêu chuẩn, rằng nó tuân theo tiêu chuẩn và biên dịch mã Java hợp pháp. Và nếu bạn sử dụng một trình duyệt không tuân thủ các tiêu chuẩn, mặc dù nó có thể có một số tính năng hay, nhưng giờ đây bạn đang dựa vào lý thuyết của một nhà thiết kế trình biên dịch bận rộn về quy tắc "hợp lý". Đó là một kịch bản thực sự khủng khiếp: "Tại sao mọi người muốn sử dụng HAI ? Điều kiện trong cùng nếu tôi không bao giờ có")
deworde

Ví dụ này sử dụng ifhơi khác so với a while, bởi vì trình biên dịch sẽ biên dịch ngay cả trình tự if(true) return; System.out.println("Hello");mà không phàn nàn. IIRC, đây là một ngoại lệ đặc biệt trong JLS. Trình biên dịch sẽ có thể phát hiện mã không thể truy cập sau một ifcách dễ dàng như với while.
Christian Semrau

2
@emory - khi bạn cung cấp một chương trình Java thông qua bộ xử lý chú thích của bên thứ ba, nó ... theo nghĩa rất thực ... không phải là Java nữa. Đúng hơn, nó là Java được phủ lên bởi bất kỳ phần mở rộng và sửa đổi ngôn ngữ nào mà bộ xử lý chú thích thực hiện. Nhưng đó KHÔNG phải là những gì đang diễn ra ở đây. Các câu hỏi của OP là về Java vani và các quy tắc JLS chi phối điều gì là và đâu là một chương trình Java vani hợp lệ.
Stephen C

6

Sau này không phải là không thể truy cập được. Boolean b vẫn có khả năng bị thay đổi thành false ở đâu đó bên trong vòng lặp gây ra điều kiện kết thúc.


Đúng, nhưng không liên quan, vì b KHÔNG được thay đổi trong vòng lặp. Bạn cũng có thể tranh luận rằng vòng lặp sử dụng true có thể bao gồm một câu lệnh break, điều này sẽ đưa ra một điều kiện kết thúc.
deworde

4

Dự đoán của tôi là biến "b" có khả năng thay đổi giá trị của nó, vì vậy System.out.println("while terminated"); có thể đạt được trình biên dịch .


4

Trình biên dịch không hoàn hảo - cũng không nên

Trách nhiệm của trình biên dịch là xác nhận cú pháp - không phải xác nhận việc thực thi. Các trình biên dịch cuối cùng có thể bắt và ngăn chặn nhiều vấn đề về thời gian chạy bằng một ngôn ngữ được đánh máy mạnh - nhưng chúng không thể bắt được tất cả các lỗi như vậy.

Giải pháp thực tế là có nhiều bài kiểm tra đơn vị để bổ sung cho việc kiểm tra trình biên dịch của bạn HOẶC sử dụng các thành phần hướng đối tượng để triển khai logic được biết là mạnh mẽ, thay vì dựa vào các biến nguyên thủy và điều kiện dừng.

Gõ mạnh và OO: tăng hiệu quả của trình biên dịch

Một số lỗi có bản chất cú pháp - và trong Java, cách gõ mạnh làm cho rất nhiều ngoại lệ thời gian chạy có thể được xử lý. Tuy nhiên, bằng cách sử dụng các kiểu tốt hơn, bạn có thể giúp trình biên dịch của mình thực thi logic tốt hơn.

Nếu bạn muốn trình biên dịch thực thi logic hiệu quả hơn, trong Java, giải pháp là xây dựng các đối tượng được yêu cầu, mạnh mẽ có thể thực thi logic đó và sử dụng các đối tượng đó để xây dựng ứng dụng của bạn, thay vì nguyên thủy.

Một ví dụ cổ điển về điều này là việc sử dụng mẫu vòng lặp, kết hợp với vòng lặp foreach của Java, cấu trúc này ít bị loại lỗi mà bạn minh họa hơn là một vòng lặp while đơn giản.


Lưu ý rằng OO không phải là cách duy nhất để giúp các cơ chế gõ tĩnh mạnh tìm ra lỗi. Các kiểu tham số và các lớp kiểu của Haskell (hơi khác một chút so với những gì được gọi là lớp trong ngôn ngữ OO) thực sự được cho là tốt hơn trong việc này.
bùng binh bên trái

3

Trình biên dịch không đủ phức tạp để chạy qua các giá trị bcó thể chứa (mặc dù bạn chỉ gán nó một lần). Ví dụ đầu tiên dễ dàng cho trình biên dịch thấy nó sẽ là một vòng lặp vô hạn vì điều kiện không biến.


4
Điều này không liên quan đến "độ tinh vi" của trình biên dịch. JLS có độ dài lớn để chỉ định các quy tắc phát hiện các câu lệnh không thể truy cập. Theo các quy tắc đó, ví dụ đầu tiên không phải là Java hợp lệ và ví dụ thứ hai là Java hợp lệ. Đó là nó. Người viết trình biên dịch phải thực hiện các quy tắc như được chỉ định ... nếu không trình biên dịch của anh ta không tuân thủ.
Stephen C

3

Tôi ngạc nhiên khi trình biên dịch của bạn từ chối biên dịch trường hợp đầu tiên. Điều đó có vẻ lạ đối với tôi.

Nhưng trường hợp thứ hai không được tối ưu hóa cho trường hợp đầu tiên vì (a) một luồng khác có thể cập nhật giá trị của b(b) hàm được gọi có thể sửa đổi giá trị của bnhư một hiệu ứng phụ.


2
Nếu bạn đang ngạc nhiên, sau đó bạn chưa đọc đủ của ngôn ngữ Java Spec :-)
Stephen C

1
Haha :) Thật tuyệt khi biết vị trí của tôi. Cảm ơn!
sarnold

3

Trên thực tế, tôi không nghĩ rằng bất cứ ai hiểu nó đúng QUITE (ít nhất là không theo nghĩa của người hỏi ban đầu). OQ tiếp tục đề cập:

Đúng, nhưng không liên quan, vì b KHÔNG được thay đổi trong vòng lặp

Nhưng nó không quan trọng vì dòng cuối cùng có thể truy cập được. Nếu bạn lấy mã đó, biên dịch nó thành một tệp lớp và giao tệp lớp cho người khác (giả sử như một thư viện), họ có thể liên kết lớp đã biên dịch với mã sửa đổi "b" thông qua phản chiếu, thoát khỏi vòng lặp và gây ra lỗi cuối cùng dòng để thực thi.

Điều này đúng với bất kỳ biến nào không phải là hằng số (hoặc biến cuối cùng biên dịch thành hằng số ở vị trí mà nó được sử dụng - đôi khi gây ra lỗi kỳ lạ nếu bạn biên dịch lại lớp với lớp cuối cùng chứ không phải lớp tham chiếu đến nó, tham chiếu lớp sẽ vẫn giữ giá trị cũ mà không có bất kỳ lỗi nào)

Tôi đã sử dụng khả năng phản chiếu để sửa đổi các biến riêng không cuối cùng của một lớp khác để vá một lớp trong thư viện đã mua - sửa một lỗi để chúng tôi có thể tiếp tục phát triển trong khi chờ các bản vá chính thức từ nhà cung cấp.

Nhân tiện, điều này có thể không thực sự hoạt động trong những ngày này - mặc dù tôi đã làm điều đó trước đây, nhưng có khả năng một vòng lặp nhỏ như vậy sẽ được lưu vào bộ nhớ cache của CPU và vì biến không được đánh dấu là dễ bay hơi nên mã được lưu trong bộ nhớ cache có thể không bao giờ nhận giá trị mới. Tôi chưa bao giờ thấy điều này trong thực tế nhưng tôi tin rằng nó đúng về mặt lý thuyết.


Điểm tốt. Tôi giả sử rằng đó blà một biến phương thức. Bạn thực sự có thể sửa đổi một biến phương thức bằng cách sử dụng phản chiếu không? Bất kể điểm chung là chúng ta không nên cho rằng không thể truy cập được dòng cuối cùng.
emory 21/12/11

Ouch, bạn nói đúng, tôi cho rằng b là một thành viên. Nếu b là một biến phương pháp sau đó tôi tin rằng trình biên dịch "có thể" biết rằng nó sẽ không thay đổi nhưng không (mà làm cho tất cả các câu trả lời khác khá chính xác sau khi tất cả)
Bill K

3

Nó chỉ đơn giản là vì trình biên dịch không làm quá nhiều công việc của trẻ nhỏ, mặc dù điều đó là có thể.

Ví dụ được hiển thị là đơn giản và hợp lý để trình biên dịch phát hiện vòng lặp vô hạn. Nhưng làm thế nào về việc chúng ta chèn 1000 dòng mã mà không có bất kỳ mối quan hệ nào với biến b? Và làm thế nào về những tuyên bố là tất cả b = true;? Trình biên dịch chắc chắn có thể đánh giá kết quả và cho bạn biết cuối cùng nó đúng trongwhile vòng lặp, nhưng nó sẽ chậm đến mức nào khi biên dịch một dự án thực?

PS, công cụ lint chắc chắn nên làm điều đó cho bạn.


2

Từ quan điểm của trình biên dịch, btrongwhile(b) đâu đó có thể thay đổi thành sai. Trình biên dịch không bận tâm kiểm tra.

Để thử vui vẻ while(1 < 2), for(int i = 0; i < 1; i--)v.v.


2

Các biểu thức được đánh giá tại thời gian chạy, do đó, khi thay thế giá trị vô hướng "true" bằng một thứ gì đó như biến boolean, bạn đã thay đổi một giá trị vô hướng thành một biểu thức boolean và do đó, trình biên dịch không có cách nào để biết nó tại thời điểm biên dịch.


2

Nếu trình biên dịch có thể xác định một cách chắc chắn rằng boolean sẽ đánh giá vào truelúc chạy, nó sẽ ném ra lỗi đó. Trình biên dịch giả định rằng biến bạn đã khai báo có thể được thay đổi (mặc dù chúng ta biết ở đây như con người, nó sẽ không).

Để nhấn mạnh thực tế này, nếu các biến được khai báo như finaltrong Java, hầu hết các trình biên dịch sẽ gặp lỗi tương tự như khi bạn thay thế giá trị. Điều này là do biến được xác định tại thời điểm biên dịch (và không thể thay đổi khi chạy) và do đó trình biên dịch có thể xác định một cách chắc chắn rằng biểu thức được đánh giá là truetại thời điểm chạy.


2

Câu lệnh đầu tiên luôn dẫn đến một vòng lặp vô hạn vì chúng ta đã chỉ định một hằng số trong điều kiện của vòng lặp while, trong đó như trong trường hợp thứ hai trình biên dịch giả định rằng, có khả năng thay đổi giá trị của b bên trong vòng lặp.

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.