Nieskończone pętle w Javie


82

Spójrz na następującą nieskończoną whilepętlę w Javie. Powoduje to błąd w czasie kompilacji dla instrukcji poniżej.

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

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

Poniższa sama nieskończona whilepętla działa jednak dobrze i nie powoduje żadnych błędów, w których właśnie zastąpiłem warunek zmienną boolowską.

boolean b=true;

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

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

Również w drugim przypadku instrukcja po pętli jest oczywiście nieosiągalna, ponieważ zmienna boolowska bjest prawdziwa, a kompilator w ogóle nie narzeka. Czemu?


Edycja: Następująca wersja whileutknęła w nieskończonej pętli, co jest oczywiste, ale nie powoduje błędów kompilatora dla instrukcji poniżej, mimo że ifwarunek w pętli jest zawsze falseiw konsekwencji pętla nigdy nie może powrócić i może zostać określona przez kompilator w sam czas kompilacji.

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.

Edycja: to samo z ifi while.

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.
}      

Następująca wersja whilerównież utknęła w nieskończonej pętli.

while(true) {

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

To dlatego, że finally blok jest zawsze wykonywany, mimo że returninstrukcja napotyka go wcześniej w samym trybloku.


46
Kogo to obchodzi? To oczywiście tylko funkcja kompilatora. Nie przejmuj się tego typu rzeczami.
CJ7

17
Jak inny wątek mógłby zmienić lokalną zmienną niestatyczną?
CJ7

4
Stan wewnętrzny obiektu może być zmieniany równolegle poprzez refleksję. Dlatego JLS nakazuje sprawdzanie tylko ostatecznych (stałych) wyrażeń.
lsoliveira,

6
Nienawidzę tych głupich błędów. Nieosiągalny kod powinien być ostrzeżeniem, a nie błędem.
user606723

2
@ CJ7: Nie nazwałbym tego „funkcją”, a implementowanie zgodnego kompilatora Java jest bardzo uciążliwe (bez powodu). Korzystaj z przywiązania do dostawcy zgodnie z projektem.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Odpowiedzi:


105

Kompilator może łatwo i jednoznacznie udowodnić, że pierwsze wyrażenie zawsze skutkuje nieskończoną pętlą, ale dla drugiego nie jest to takie łatwe. W twoim przykładzie zabawki jest to proste, ale co jeśli:

  • zawartość zmiennej została odczytana z pliku?
  • zmienna nie była lokalna i mogła zostać zmodyfikowana przez inny wątek?
  • zmienna opierała się na danych wejściowych użytkownika?

Kompilator najwyraźniej nie sprawdza prostszego przypadku, ponieważ całkowicie rezygnuje z tej drogi. Czemu? Ponieważ specyfikacja jest o wiele trudniejsza . Patrz sekcja 14.21 :

(Nawiasem mówiąc, mój kompilator nie skarżą się, gdy zmienna jest zadeklarowana final).


26
-1 - Nie chodzi o to, co może zrobić kompilator. Nie chodzi o to, aby kontrole były łatwiejsze lub trudniejsze. Chodzi o to, co kompilator może zrobić ... przez specyfikację języka Java. Druga wersja kodu jest poprawną Javą zgodnie z JLS, więc błąd kompilacji byłby nieprawidłowy.
Stephen C

10
@StephenC - Dzięki za te informacje. Na szczęście zaktualizowany, aby odzwierciedlić to samo.
Wayne,

Wciąż -1; żadne z twoich „a co jeśli” nie ma tutaj zastosowania. W rzeczywistości kompilator może rzeczywiście rozwiązać problem - w kategoriach analizy statycznej nazywa się to ciągłą propagacją i jest szeroko stosowane z powodzeniem w wielu innych sytuacjach. JLS jest jedynym powodem, a to, jak trudny jest problem do rozwiązania, jest nieistotne. Oczywiście, powód, dla którego JLS jest napisany w ten sposób, może być związany z trudnością tego problemu, chociaż osobiście podejrzewam, że w rzeczywistości jest to spowodowane tym, że trudno jest wymusić ustandaryzowane rozwiązanie stałej propagacji w różnych kompilatorach.
Oak,

2
@Oak - w mojej odpowiedzi przyznałem, że „W przykładzie zabawki [OP] jest to proste” i opierając się na komentarzu Stephena, że ​​JLS jest czynnikiem ograniczającym. Jestem pewien, że się zgadzamy.
Wayne,

55

Zgodnie ze specyfikacjami , o oświadczeniach while mówi się o tym.

Instrukcja while może normalnie zakończyć się, jeśli przynajmniej jedno z poniższych jest prawdziwe:

  • Instrukcja while jest osiągalna, a wyrażenie warunku nie jest wyrażeniem stałym o wartości true.
  • Istnieje osiągalna instrukcja przerwania, która zamyka instrukcję while. \

Tak więc kompilator powie, że kod następujący po instrukcji while jest nieosiągalny, jeśli warunek while jest stałą o wartości true lub jeśli w tym czasie znajduje się instrukcja break. W drugim przypadku, ponieważ wartość b nie jest stała, kod następujący po niej nie jest uznawany za nieosiągalny. Za tym łączem znajduje się znacznie więcej informacji, aby uzyskać więcej informacji na temat tego, co jest, a co nie jest uważane za nieosiągalne.


3
+1 za zauważenie, że to nie tylko to, co twórcy kompilatorów mogą lub nie mogą wymyślić - JLS mówi im, co mogą, a czego nie mogą uznać za nieosiągalne.
yshavit

14

Ponieważ prawda jest stała, a b można zmienić w pętli.


1
Prawidłowe, ale nieistotne, ponieważ b NIE jest zmieniane w pętli. Możesz również argumentować, że pętla używająca wartości true może zawierać instrukcję przerwania.
deworde,

@deworde - o ile istnieje szansa, że ​​może to zostać zmienione (powiedzmy przez inny wątek), kompilator nie może nazwać tego błędem. Nie można stwierdzić po prostu patrząc na samą pętlę, czy b zostanie zmienione, czy nie, podczas wykonywania pętli.
Peter Recore

10

Ponieważ analiza stanu zmiennej jest trudna, więc kompilator po prostu się poddał i pozwolił ci robić, co chcesz. Ponadto specyfikacja języka Java zawiera jasne zasady dotyczące tego, jak kompilator może wykryć nieosiągalny kod .

Istnieje wiele sposobów na oszukanie kompilatora - inny typowy przykład to

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

co by nie zadziałało, ponieważ kompilator zdałby sobie sprawę, że obszar jest nie do naprawienia. Zamiast tego możesz to zrobić

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

To zadziałałoby, ponieważ kompilator nie jest w stanie zrozumieć, że wyrażenie nigdy nie będzie fałszywe.


6
Jak stwierdziły inne odpowiedzi, nie jest to (bezpośrednio) związane z tym, co może być łatwe lub trudne dla kompilatora do wykrycia nieosiągalnego kodu. JLS dokłada wszelkich starań, aby określić zasady wykrywania nieosiągalnych instrukcji. Zgodnie z tymi regułami pierwszy przykład to nieprawidłowa Java, a drugi przykład to poprawna Java. Otóż ​​to. Kompilator po prostu implementuje określone reguły.
Stephen C

Nie podążam za argumentem JLS. Czy kompilatory są naprawdę zobowiązane do przekształcenia całej legalnej Javy w kod bajtowy? nawet jeśli można udowodnić, że jest to bezsensowne.
emory

@emory Tak, to byłaby definicja przeglądarki zgodnej ze standardami, że jest ona zgodna ze standardem i kompiluje legalny kod Java. A jeśli używasz przeglądarki niezgodnej ze standardami, chociaż może ona mieć kilka fajnych funkcji, teraz polegasz na teorii zapracowanego projektanta kompilatorów na temat tego, co jest „rozsądną” regułą. Co jest naprawdę okropnym scenariuszem: „Dlaczego ktoś miałby chcieć używać DWÓCH warunków warunkowych w tym samym, skoro? Nigdy tego nie robiłem”)
deworde,

Ten przykład użycia ifróżni się nieco od a while, ponieważ kompilator skompiluje nawet sekwencję if(true) return; System.out.println("Hello");bez narzekania. IIRC, jest to specjalny wyjątek w JLS. Kompilator byłby w stanie wykryć nieosiągalny kod po iftak łatwym, jak z while.
Christian Semrau,

2
@emory - kiedy przesyłasz program w Javie przez procesor adnotacji innej firmy, jest on ... w bardzo realnym sensie ... już nie Java. Jest to raczej Java, na którą nałożono wszelkie rozszerzenia językowe i modyfikacje, które implementuje procesor adnotacji. Ale to NIE jest to, co się tutaj dzieje. Pytania OP dotyczą zwykłej Javy, a reguły JLS określają, co jest, a co nie jest prawidłowym programem w języku Java.
Stephen C

6

Ta ostatnia nie jest nieosiągalna. Wartość logiczna b nadal może zostać zmieniona na fałsz gdzieś wewnątrz pętli, powodując warunek końcowy.


Prawidłowe, ale nieistotne, ponieważ b NIE jest zmieniane w pętli. Można również argumentować, że pętla używająca wartości true może zawierać instrukcję przerwania, która daje warunek końcowy.
deworde,

4

Domyślam się, że zmienna "b" ma możliwość zmiany swojej wartości, więc myśl kompilatora System.out.println("while terminated"); może zostać osiągnięta.


4

Kompilatory nie są doskonałe - ani nie powinny

Obowiązkiem kompilatora jest potwierdzenie składni, a nie potwierdzenie wykonania. Kompilatory mogą ostatecznie wychwycić i zapobiec wielu problemom w czasie wykonywania w języku z silną typizacją - ale nie są w stanie wychwycić wszystkich takich błędów.

Praktycznym rozwiązaniem jest posiadanie baterii testów jednostkowych w celu uzupełnienia sprawdzeń kompilatorów LUB użycie komponentów zorientowanych obiektowo do implementacji logiki, o której wiadomo, że jest niezawodna, zamiast polegać na pierwotnych zmiennych i warunkach zatrzymania.

Silne typowanie i OO: zwiększenie wydajności kompilatora

Niektóre błędy mają charakter składniowy - aw Javie mocne pisanie sprawia, że ​​można złapać wiele wyjątków w czasie wykonywania. Ale używając lepszych typów, możesz pomóc kompilatorowi wymusić lepszą logikę.

Jeśli chcesz, aby kompilator skuteczniej wymuszał logikę, w Javie rozwiązaniem jest zbudowanie solidnych, wymaganych obiektów, które mogą wymusić taką logikę, i użycie tych obiektów do zbudowania aplikacji, a nie prymitywów.

Klasycznym tego przykładem jest użycie wzorca iteratora w połączeniu z pętlą foreach w Javie, ta konstrukcja jest mniej podatna na typ błędu, który ilustrujesz, niż uproszczona pętla while.


Zauważ, że OO nie jest jedynym sposobem, aby pomóc statycznym silnym mechanizmom pisania w znajdowaniu błędów. Typy parametryczne i klasy typów Haskella (które są nieco inne od tego, co nazywa się klasami w językach OO) są w tym prawdopodobnie lepsze.
leftaroundokoło

3

Kompilator nie jest wystarczająco zaawansowany, aby przeglądać wartości, które bmogą zawierać (chociaż przypisuje się je tylko raz). Pierwszy przykład jest łatwy do zrozumienia dla kompilatora, że ​​będzie to nieskończona pętla, ponieważ warunek nie jest zmienny.


4
Nie ma to związku z „wyrafinowaniem” kompilatora. JLS dokłada wszelkich starań, aby określić zasady wykrywania nieosiągalnych instrukcji. Zgodnie z tymi regułami pierwszy przykład to nieprawidłowa Java, a drugi przykład to poprawna Java. Otóż ​​to. Twórca kompilatora musi zaimplementować określone reguły ... w przeciwnym razie jego kompilator jest niezgodny.
Stephen C

3

Dziwię się, że Twój kompilator odmówił skompilowania pierwszego przypadku. Wydaje mi się to dziwne.

Ale drugi przypadek nie jest zoptymalizowany do pierwszego przypadku, ponieważ (a) inny wątek może zaktualizować wartość b(b) wywoływana funkcja może zmodyfikować wartość bjako efekt uboczny.


2
Jeśli jesteś zaskoczony, to nie przeczytałeś wystarczająco dużo specyfikacji języka Java :-)
Stephen C,

1
Haha :) Miło jest wiedzieć, na czym stoję. Dzięki!
sarnold

3

Właściwie nie sądzę, żeby ktokolwiek zrozumiał to DOKŁADNIE dobrze (przynajmniej nie w sensie oryginalnego pytającego). OQ ciągle wspomina:

Prawidłowe, ale nieistotne, ponieważ b NIE jest zmieniane w pętli

Ale to nie ma znaczenia, ponieważ ostatnia linia JEST osiągalna. Gdybyś wziął ten kod, skompilował go do pliku klasy i przekazał plik klasy komuś innemu (powiedzmy jako biblioteka), mogliby połączyć skompilowaną klasę z kodem, który modyfikuje „b” poprzez odbicie, opuszczając pętlę i powodując ostatnią linia do wykonania.

Dotyczy to każdej zmiennej, która nie jest stałą (lub finalnej, która kompiluje się do stałej w miejscu, w którym jest używana - czasami powoduje dziwne błędy, jeśli przekompilujesz klasę z wersją final, a nie klasą, która się do niej odwołuje, odwołanie klasa będzie nadal przechowywać starą wartość bez żadnych błędów)

Użyłem zdolności refleksji, aby zmodyfikować nie ostateczne zmienne prywatne innej klasy, aby małpa załatać klasę w zakupionej bibliotece - naprawiłem błąd, abyśmy mogli kontynuować rozwój, czekając na oficjalne poprawki od dostawcy.

Nawiasem mówiąc, w dzisiejszych czasach może to nie działać - chociaż robiłem to wcześniej, jest szansa, że ​​taka mała pętla zostanie zapisana w pamięci podręcznej procesora, a ponieważ zmienna nie jest oznaczona jako ulotna, buforowany kod może nigdy nie być podnieś nową wartość. Nigdy nie widziałem tego w akcji, ale uważam, że jest to teoretycznie prawda.


Słuszna uwaga. Zakładam, że bjest to zmienna metody. Czy naprawdę możesz zmodyfikować zmienną metody za pomocą odbicia? Bez względu na ogólny punkt widzenia nie powinniśmy zakładać, że ostatnia linia jest nieosiągalna.
emory

Ouch, masz rację, założyłem, że b był członkiem. Jeśli b jest zmienną metody, to uważam, że kompilator „Może” wie, że się nie zmieni, ale nie (co sprawia, że ​​wszystkie inne odpowiedzi są W PEŁNI poprawne)
Bill K

3

Dzieje się tak po prostu dlatego, że kompilator nie zajmuje się zbytnio opieką nad dzieckiem, chociaż jest to możliwe.

Pokazany przykład jest prosty i rozsądny dla kompilatora do wykrywania nieskończonej pętli. Ale co powiesz na wstawienie 1000 linii kodu bez żadnego związku ze zmienną b? A co z tymi wszystkimi stwierdzeniami b = true;? Kompilator z pewnością może ocenić wynik i powiedzieć, że ostatecznie w whilepętli jest to prawda , ale jak wolno będzie kompilować prawdziwy projekt?

PS, narzędzie lint zdecydowanie powinno zrobić to za Ciebie.


2

Z punktu widzenia kompilatora bin while(b)może gdzieś zmienić się na false. Kompilator po prostu nie zawraca sobie głowy sprawdzaniem.

Na próbie zabawy while(1 < 2), for(int i = 0; i < 1; i--)etc.


2

Wyrażenia są obliczane w czasie wykonywania, więc zastępując wartość skalarną „true” czymś w rodzaju zmiennej boolowskiej, zmieniasz wartość skalarną na wyrażenie boolowskie, a zatem kompilator nie ma możliwości poznania jej w czasie kompilacji.


2

Jeśli kompilator może jednoznacznie określić, że wartość logiczna zostanie oszacowana truew czasie wykonywania, zgłosi ten błąd. Kompilator zakłada, że ​​zadeklarowana zmienna może zostać zmieniona (chociaż wiemy, że jako ludzie, nie będzie).

Aby podkreślić ten fakt, jeśli zmienne są zadeklarowane tak jak finalw Javie, większość kompilatorów zgłosi ten sam błąd, co w przypadku podstawienia wartości. Dzieje się tak, ponieważ zmienna jest definiowana w czasie kompilacji (i nie można jej zmienić w czasie wykonywania), a kompilator może w związku z tym jednoznacznie określić, że wyrażenie jest obliczane truew czasie wykonywania.


2

Pierwsza instrukcja zawsze skutkuje nieskończoną pętlą, ponieważ określiliśmy stałą w warunku pętli while, gdzie tak jak w drugim przypadku kompilator zakłada, że ​​istnieje możliwość zmiany wartości b wewnątrz pętli.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.