Endlosschleifen in Java

82

Schauen Sie sich die folgende whileEndlosschleife in Java an. Es verursacht einen Kompilierungsfehler für die Anweisung darunter.

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

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

Die folgende whileEndlosschleife funktioniert jedoch einwandfrei und gibt keine Fehler aus, bei denen ich die Bedingung nur durch eine boolesche Variable ersetzt habe.

boolean b=true;

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

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

Auch im zweiten Fall ist die Anweisung nach der Schleife offensichtlich nicht erreichbar, da die boolesche Variable bwahr ist und der Compiler sich überhaupt nicht beschwert. Warum?


Bearbeiten: Die folgende Version von whilebleibt als offensichtlich in einer Endlosschleife stecken, gibt jedoch keine Compilerfehler für die Anweisung darunter aus, obwohl die ifBedingung innerhalb der Schleife immer ist falseund folglich die Schleife niemals zurückkehren kann und vom Compiler am festgelegt werden kann Kompilierungszeit selbst.

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.

Edit: Gleiches mit ifund 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.
}      

Die folgende Version von whilebleibt auch in einer Endlosschleife stecken.

while(true) {

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

Dies liegt daran, dass der finallyBlock immer ausgeführt wird, obwohl die returnAnweisung innerhalb des tryBlocks selbst auf ihn trifft .

Löwe
quelle
46
Wen interessiert das? Es ist offensichtlich nur eine Funktion des Compilers. Kümmere dich nicht um solche Sachen.
CJ7
17
Wie könnte ein anderer Thread eine lokale nicht statische Variable ändern?
CJ7
4
Der interne Zustand des Objekts kann gleichzeitig durch Reflexion geändert werden. Aus diesem Grund schreibt das JLS vor, dass nur endgültige (konstante) Ausdrücke überprüft werden.
lsoliveira
5
Ich hasse diese dummen Fehler. Nicht erreichbarer Code sollte eine Warnung und kein Fehler sein.
user606723
2
@ CJ7: Ich würde dies nicht als "Feature" bezeichnen, und es macht es sehr mühsam (ohne Grund), einen konformen Java-Compiler zu implementieren. Genießen Sie Ihre Lieferantenbindung.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Antworten:

105

Der Compiler kann leicht und eindeutig beweisen, dass der erste Ausdruck immer zu einer Endlosschleife führt, für den zweiten jedoch nicht so einfach. In Ihrem Spielzeugbeispiel ist es einfach, aber was ist, wenn:

  • Der Inhalt der Variablen wurde aus einer Datei gelesen.
  • Die Variable war nicht lokal und konnte von einem anderen Thread geändert werden.
  • Die Variable stützte sich auf Benutzereingaben.

Der Compiler sucht eindeutig nicht nach Ihrem einfacheren Fall, da er auf diese Straße insgesamt verzichtet. Warum? Weil es durch die Spezifikation viel schwieriger verboten ist. Siehe Abschnitt 14.21 :

(By the way, mein Compiler nicht beschweren , wenn die Variable deklariert ist final.)

Wayne
quelle
25
-1 - Es geht nicht darum, was der Compiler tun kann. Es geht nicht darum, dass Schecks einfacher oder schwieriger sind. Es geht darum, was der Compiler tun darf ... von der Java-Sprachspezifikation. Die zweite Version des Codes ist laut JLS gültiges Java , daher wäre ein Kompilierungsfehler falsch.
Stephen C
10
@ StephenC - Danke für diese Info. Glücklicherweise aktualisiert, um so viel zu reflektieren.
Wayne
Immer noch -1; Keines Ihrer "Was wäre wenn" trifft hier zu. Wie es ist, der Compiler kann in der Tat das Problem lösen - in der statischen Analyse hinsichtlich dieser konstanten-Vermehrung genannt wird, und ausgiebig erfolgreich in vielen anderen Situationen verwendet wird. Das JLS ist hier der einzige Grund, und wie schwer das Problem zu lösen ist, ist unerheblich. Natürlich könnte der Grund, warum das JLS in erster Linie so geschrieben ist, mit der Schwierigkeit dieses Problems zusammenhängen, obwohl ich persönlich vermute, dass es tatsächlich daran liegt, dass es schwierig ist, eine standardisierte Lösung mit konstanter Verbreitung über verschiedene Compiler hinweg durchzusetzen .
Eiche
2
@Oak - Ich räumte in meiner Antwort ein, dass "im Spielzeugbeispiel [des OP] einfach ist" und, basierend auf Stephens Kommentar, dass der JLS der begrenzende Faktor ist. Ich bin mir ziemlich sicher, dass wir uns einig sind.
Wayne
55

Gemäß den Spezifikationen wird das Folgende über while-Anweisungen gesagt.

Eine while-Anweisung kann normal ausgeführt werden, wenn mindestens eine der folgenden Aussagen zutrifft:

  • Die while-Anweisung ist erreichbar und der Bedingungsausdruck ist kein konstanter Ausdruck mit dem Wert true.
  • Es gibt eine erreichbare break-Anweisung, die die while-Anweisung beendet. \

Der Compiler sagt also nur, dass der Code nach einer while-Anweisung nicht erreichbar ist, wenn die while-Bedingung eine Konstante mit einem wahren Wert ist oder innerhalb der while-Anweisung eine break-Anweisung vorhanden ist. Im zweiten Fall wird der darauf folgende Code nicht als nicht erreichbar angesehen, da der Wert von b keine Konstante ist. Hinter diesem Link stecken viel mehr Informationen, um Ihnen mehr Details darüber zu geben, was nicht erreichbar ist und was nicht.

Kibbee
quelle
3
+1 für die Feststellung, dass es nicht nur das ist, was sich die Compiler-Autoren ausdenken können oder nicht - das JLS sagt ihnen, was sie für unerreichbar halten können und was nicht.
Yshavit
14

Weil true konstant ist und b in der Schleife geändert werden kann.

Hope_is_grim
quelle
1
Richtig, aber irrelevant, da b in der Schleife NICHT geändert wird. Sie könnten auch argumentieren, dass die Schleife, die true verwendet, eine break-Anweisung enthalten könnte.
Deworde
@deworde - Solange die Möglichkeit besteht, dass es geändert wird (z. B. durch einen anderen Thread), kann der Compiler es nicht als Fehler bezeichnen. Sie können nicht einfach anhand der Schleife selbst erkennen, ob b geändert wird, während die Schleife ausgeführt wird.
Peter Recore
10

Da die Analyse des variablen Status schwierig ist, hat der Compiler so gut wie aufgegeben und Sie können tun, was Sie möchten. Darüber hinaus enthält die Java-Sprachspezifikation klare Regeln dafür, wie der Compiler nicht erreichbaren Code erkennen darf .

Es gibt viele Möglichkeiten, den Compiler auszutricksen - ein weiteres häufiges Beispiel ist

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

Das würde nicht funktionieren, da der Compiler erkennen würde, dass der Bereich nicht erreichbar ist. Stattdessen könnten Sie tun

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

Dies würde funktionieren, da der Compiler nicht erkennen kann, dass der Ausdruck niemals falsch sein wird.

kba
quelle
6
Wie in anderen Antworten angegeben, hängt dies nicht (direkt) damit zusammen, was für den Compiler möglicherweise einfach oder schwierig ist, den nicht erreichbaren Code zu erkennen. Das JLS unternimmt große Anstrengungen, um die Regeln für die Erkennung nicht erreichbarer Anweisungen festzulegen . Nach diesen Regeln ist das erste Beispiel kein gültiges Java und das zweite Beispiel ist gültiges Java. Das ist es. Der Compiler implementiert nur die angegebenen Regeln.
Stephen C
Ich folge nicht dem JLS-Argument. Sind Compiler wirklich verpflichtet, alles legale Java in Bytecode umzuwandeln? auch wenn sich herausstellen kann, dass es unsinnig ist.
Emory
@emory Ja, das wäre die Definition eines standardkonformen Browsers, der dem Standard entspricht und legalen Java-Code kompiliert. Und wenn Sie einen nicht standardkonformen Browser verwenden, der möglicherweise einige nette Funktionen bietet, verlassen Sie sich jetzt auf die Theorie eines vielbeschäftigten Compiler-Designers, was eine "vernünftige" Regel ist. Welches ist ein wirklich schreckliches Szenario: "Warum sollte jemand ZWEI Bedingungen in der gleichen verwenden wollen, wenn? Ich habe nie")
Deworde
Dieses Beispiel unterscheidet ifsich ein wenig von a while, da der Compiler sogar die Sequenz kompiliert, if(true) return; System.out.println("Hello");ohne sich zu beschweren. IIRC, dies ist eine besondere Ausnahme in der JLS. Der Compiler wäre in der Lage, den nicht erreichbaren Code nach dem ifso einfach wie mit zu erkennen while.
Christian Semrau
2
@emory - Wenn Sie ein Java-Programm über einen Annotation-Prozessor eines Drittanbieters einspeisen, ist es ... im wahrsten Sinne des Wortes ... kein Java mehr. Vielmehr handelt es sich um Java, das mit allen sprachlichen Erweiterungen und Änderungen überlagert ist , die der Annotationsprozessor implementiert. Aber das ist hier NICHT der Fall. Die Fragen des OP beziehen sich auf Vanilla Java, und die JLS-Regeln regeln, was ein gültiges Vanilla Java-Programm ist und was nicht.
Stephen C
6

Letzteres ist nicht unerreichbar. Der boolesche Wert b hat immer noch die Möglichkeit, irgendwo innerhalb der Schleife in false geändert zu werden, was zu einer Endbedingung führt.

Cheesegraterr
quelle
Richtig, aber irrelevant, da b in der Schleife NICHT geändert wird. Sie könnten auch argumentieren, dass die Schleife, die true verwendet, eine break-Anweisung enthalten könnte, die eine Endbedingung ergeben würde.
Deworde
4

Ich vermute, dass die Variable "b" die Möglichkeit hat, ihren Wert zu ändern, damit der Compiler denken System.out.println("while terminated"); kann.

xuanyuanzhiyuan
quelle
4

Compiler sind nicht perfekt - und sollten es auch nicht sein

Der Compiler ist dafür verantwortlich, die Syntax zu bestätigen und nicht die Ausführung zu bestätigen. Compiler können letztendlich viele Laufzeitprobleme in einer stark typisierten Sprache abfangen und verhindern - aber sie können nicht alle derartigen Fehler abfangen.

Die praktische Lösung besteht darin, Batterien mit Komponententests zu haben, um Ihre Compilerprüfungen zu ergänzen, ODER objektorientierte Komponenten für die Implementierung von Logik zu verwenden, die als robust bekannt sind, anstatt sich auf primitive Variablen und Stoppbedingungen zu verlassen.

Starke Typisierung und OO: Steigerung der Effizienz des Compilers

Einige Fehler sind syntaktischer Natur - und in Java macht die starke Typisierung viele Laufzeitausnahmen abfangbar. Durch die Verwendung besserer Typen können Sie Ihrem Compiler jedoch helfen, eine bessere Logik durchzusetzen.

Wenn der Compiler die Logik in Java effektiver erzwingen soll, besteht die Lösung darin, robuste, erforderliche Objekte zu erstellen, die diese Logik erzwingen können, und diese Objekte zum Erstellen Ihrer Anwendung anstelle von Grundelementen zu verwenden.

Ein klassisches Beispiel hierfür ist die Verwendung des Iteratormusters in Kombination mit der foreach-Schleife von Java. Dieses Konstrukt ist weniger anfällig für den von Ihnen dargestellten Fehlertyp als eine vereinfachte while-Schleife.

jayunit100
quelle
Beachten Sie, dass OO nicht die einzige Möglichkeit ist, statisch starken Tippmechanismen beim Auffinden von Fehlern zu helfen. Haskells parametrische Typen und Typklassen (die sich ein wenig von den sogenannten Klassen in OO-Sprachen unterscheiden) sind hier wohl besser.
links um den
3

Der Compiler ist nicht hoch genug, um die bmöglicherweise enthaltenen Werte zu durchlaufen (obwohl Sie ihn nur einmal zuweisen). Das erste Beispiel ist für den Compiler leicht zu erkennen, dass es sich um eine Endlosschleife handelt, da die Bedingung nicht variabel ist.

Jonathan M.
quelle
4
Dies hängt nicht mit der "Raffinesse" des Compilers zusammen. Das JLS unternimmt große Anstrengungen, um die Regeln für die Erkennung nicht erreichbarer Anweisungen festzulegen. Nach diesen Regeln ist das erste Beispiel kein gültiges Java und das zweite Beispiel ist gültiges Java. Das ist es. Der Compiler-Writer muss die angegebenen Regeln implementieren ... andernfalls ist sein Compiler nicht konform.
Stephen C
3

Ich bin überrascht, dass Ihr Compiler sich geweigert hat, den ersten Fall zu kompilieren. Das kommt mir komisch vor.

Der zweite Fall ist jedoch nicht für den ersten Fall optimiert, da (a) ein anderer Thread den Wert von b(b) die aufgerufene Funktion möglicherweise den Wert von bals Nebeneffekt ändert .

Sarnold
quelle
2
Wenn Sie überrascht sind, dann haben Sie nicht genug von der Java-Sprachspezifikation gelesen :-)
Stephen C
1
Haha :) Es ist schön zu wissen, wo ich stehe. Vielen Dank!
Sarnold
3

Eigentlich glaube ich nicht, dass irgendjemand es ganz richtig verstanden hat (zumindest nicht im Sinne des ursprünglichen Fragestellers). Das OQ erwähnt immer wieder:

Richtig, aber irrelevant, da b in der Schleife NICHT geändert wird

Aber es spielt keine Rolle, da die letzte Zeile erreichbar ist. Wenn Sie diesen Code genommen, in eine Klassendatei kompiliert und die Klassendatei an eine andere Person (z. B. als Bibliothek) übergeben haben, kann diese die kompilierte Klasse mit Code verknüpfen, der "b" durch Reflexion ändert, die Schleife verlässt und die letzte verursacht Zeile auszuführen.

Dies gilt für alle Variablen, die keine Konstante sind (oder final, die an dem Ort, an dem sie verwendet werden, zu einer Konstanten kompiliert werden. Dies führt manchmal zu bizarren Fehlern, wenn Sie die Klasse mit dem final neu kompilieren und nicht mit einer Klasse, die darauf verweist, der Referenzierung Die Klasse behält weiterhin den alten Wert ohne Fehler bei.

Ich habe die Fähigkeit der Reflexion genutzt, um nicht endgültige private Variablen einer anderen Klasse zu ändern, um eine Klasse in einer gekauften Bibliothek mit Affen zu patchen. So wurde ein Fehler behoben, damit wir uns weiterentwickeln konnten, während wir auf offizielle Patches des Anbieters warteten.

Übrigens funktioniert dies heutzutage möglicherweise nicht mehr - obwohl ich es bereits getan habe, besteht die Möglichkeit, dass eine so kleine Schleife im CPU-Cache zwischengespeichert wird, und da die Variable nicht als flüchtig markiert ist, wird der zwischengespeicherte Code möglicherweise nie zwischengespeichert nimm den neuen Wert auf. Ich habe das noch nie in Aktion gesehen, aber ich glaube, es ist theoretisch wahr.

Bill K.
quelle
Guter Punkt. Ich gehe davon aus, dass dies beine Methodenvariable ist. Können Sie eine Methodenvariable mithilfe von Reflektion wirklich ändern? Unabhängig davon ist der allgemeine Punkt, dass wir nicht davon ausgehen sollten, dass die letzte Linie nicht erreichbar ist.
Emory
Autsch, Sie haben Recht, ich nahm an, dass b ein Mitglied war. Wenn b eine Methodenvariable ist, dann glaube ich, dass der Compiler "Can" weiß, dass es sich nicht ändert, aber nicht (was alle anderen Antworten doch ziemlich richtig macht)
Bill K
3

Es ist einfach, weil der Compiler nicht zu viel Babysitting-Arbeit macht, obwohl es möglich ist.

Das gezeigte Beispiel ist für den Compiler einfach und sinnvoll, um die Endlosschleife zu erkennen. Aber wie wäre es, wenn wir 1000 Codezeilen ohne Beziehung zur Variablen einfügen b? Und wie wäre es mit diesen Aussagen b = true;? Der Compiler kann das Ergebnis definitiv auswerten und Ihnen sagen, dass es irgendwann in einer whileSchleife wahr ist , aber wie langsam wird es sein, ein reales Projekt zu kompilieren?

PS, Fusselwerkzeug sollte es auf jeden Fall für Sie tun.

Pinxue
quelle
2

Aus Sicht der Compiler die bin while(b)könnte falsch irgendwo ändern. Der Compiler macht sich einfach nicht die Mühe zu überprüfen.

Zum Spaß versuchen while(1 < 2), for(int i = 0; i < 1; i--)etc.

Sonntag Montag
quelle
2

Ausdrücke werden zur Laufzeit ausgewertet. Wenn Sie also den Skalarwert "true" durch eine boolesche Variable ersetzen, haben Sie einen Skalarwert in einen booleschen Ausdruck geändert, sodass der Compiler ihn zur Kompilierungszeit nicht erkennen kann.

Wilmer
quelle
2

Wenn der Compiler endgültig feststellen kann, dass der Boolesche Wert zur trueLaufzeit ausgewertet wird, wird dieser Fehler ausgegeben. Der Compiler geht davon aus, dass die Variable , die Sie deklariert kann geändert werden (wenn auch wir hier als Menschen wissen es nicht).

Um diese Tatsache zu betonen: Wenn Variablen wie finalin Java deklariert werden , geben die meisten Compiler denselben Fehler aus, als hätten Sie den Wert ersetzt. Dies liegt daran, dass die Variable zur Kompilierungszeit definiert ist (und zur Laufzeit nicht geändert werden kann) und der Compiler daher endgültig bestimmen kann, wie der Ausdruck zur trueLaufzeit ausgewertet wird.

Cameron S.
quelle
2

Die erste Anweisung führt immer zu einer Endlosschleife, da wir eine Konstante im Zustand der while-Schleife angegeben haben, wobei der Compiler wie im zweiten Fall davon ausgeht, dass die Möglichkeit besteht, den Wert von b innerhalb der Schleife zu ändern.

Sheo
quelle