Undefiniertes Verhalten in Java

14

Ich habe diese Frage zu SO gelesen , in der ein allgemeines undefiniertes Verhalten in C ++ erörtert wird, und ich habe mich gefragt: Hat Java auch ein undefiniertes Verhalten?

Wenn dies der Fall ist, was sind dann einige häufige Ursachen für undefiniertes Verhalten in Java?

Wenn nicht, welche Features von Java machen es dann frei von solchen Verhaltensweisen und warum wurden die neuesten Versionen von C und C ++ nicht mit diesen Eigenschaften implementiert?

Acht
quelle
4
Java ist sehr streng definiert. Überprüfen Sie die Java-Sprachspezifikation.
4
@ user1249, "undefiniertes Verhalten" ist eigentlich auch ziemlich starr definiert.
Pacerier
Möglicherweise auch auf SO: stackoverflow.com/questions/376338/…
Ciro Santilli am
Was sagt Java, wenn Sie einen "Vertrag" verletzen? Zum Beispiel, wenn Sie .equals überladen, um mit .hashCode nicht kompatibel zu sein? docs.oracle.com/javase/7/docs/api/java/lang/… Ist das umgangssprachlich undefiniert, aber technisch nicht so wie in C ++?
Mooing Duck

Antworten:

18

In Java können Sie das Verhalten eines falsch synchronisierten Programms als undefiniert betrachten.

Java 7 JLS verwendet das Wort "undefined" in 17.4.8 einmal. Ausführungs- und Kausalitätsanforderungen :

Wir f|dbezeichnen die gegebene Funktion mit der Einschränkung der Domäne von fto d. Für alle xin d, f|d(x) = f(x)und für alle xnicht in d, f|d(x)ist undefiniert ...

In der Java-API-Dokumentation werden einige Fälle angegeben, in denen die Ergebnisse nicht definiert sind, z. B. im (veralteten) Konstruktor Datum (int Jahr, int Monat, int Tag) :

Das Ergebnis ist undefiniert, wenn ein gegebenes Argument außerhalb der Grenzen liegt ...

Javadocs für ExecutorService.invokeAll (Collection) -Status :

Die Ergebnisse dieser Methode sind nicht definiert, wenn die angegebene Auflistung geändert wird, während dieser Vorgang ausgeführt wird.

Weniger formales "undefiniertes" Verhalten ist beispielsweise in ConcurrentModificationException zu finden , wo API-Dokumente den Begriff "best effort" verwenden:

Beachten Sie, dass das Fail-Fast-Verhalten nicht garantiert werden kann, da es im Allgemeinen unmöglich ist, bei nicht synchronisierten gleichzeitigen Änderungen eine harte Garantie zu geben. Fehlgeschlagene Operationen werden ConcurrentModificationExceptionnach besten Kräften ausgeführt . Daher wäre es falsch, ein Programm zu schreiben, dessen Korrektheit von dieser Ausnahme abhängt ...


Blinddarm

Eines der Frage Kommentare bezieht sich auf einen Artikel von Eric Lippert , die in Thema Fragen hilfreiche Einführung bietet: die Implementierung definiert Verhalten .

Ich empfehle diesen Artikel für sprachunabhängige Überlegungen, obwohl es sich zu bedenken lohnt, dass der Autor auf C # und nicht auf Java abzielt.

Traditionell sagen wir, dass ein Programmiersprachen-Idiom undefiniertes Verhalten hat, wenn die Verwendung dieses Idioms irgendeinen Effekt haben kann; Es kann so funktionieren, wie Sie es erwarten, oder es kann Ihre Festplatte löschen oder Ihren Computer zum Absturz bringen. Darüber hinaus ist der Autor des Compilers nicht verpflichtet, Sie vor dem undefinierten Verhalten zu warnen. (Tatsächlich gibt es einige Sprachen, in denen die Sprachspezifikation zulässt, dass Programme, die "undefiniertes Verhalten" verwenden, den Compiler zum Absturz bringen!) ...

Im Gegensatz dazu ist ein Idiom mit implementierungsdefiniertem Verhalten ein Verhalten, bei dem der Compilerautor mehrere Möglichkeiten zur Implementierung des Features hat und eine auswählen muss. Wie der Name schon sagt, ist das implementierungsdefinierte Verhalten zumindest definiert. Mit C # kann eine Implementierung beispielsweise eine Ausnahme auslösen oder einen Wert erzeugen, wenn eine Ganzzahldivision überläuft, die Implementierung muss jedoch eine auswählen. Es kann Ihre Festplatte nicht löschen ...

Was sind einige der Faktoren, die ein Sprachdesign-Komitee veranlassen, bestimmte Sprachidiome als undefiniertes oder durch die Implementierung definiertes Verhalten zu belassen?

Der erste Hauptfaktor ist: Gibt es zwei auf dem Markt vorhandene Implementierungen der Sprache, die sich nicht auf das Verhalten eines bestimmten Programms einigen? ...

Der nächste wichtige Faktor ist: Bietet das Feature natürlich viele verschiedene Implementierungsmöglichkeiten, von denen einige deutlich besser sind als andere? ...

Ein dritter Faktor ist: Ist das Merkmal so komplex, dass eine detaillierte Aufschlüsselung seines genauen Verhaltens schwierig oder teuer zu spezifizieren wäre? ...

Ein vierter Faktor ist: Bedeutet das Feature eine hohe Belastung für den Compiler bei der Analyse? ...

Ein fünfter Faktor ist: Belastet die Funktion die Laufzeitumgebung in hohem Maße? ...

Ein sechster Faktor ist: Schliesst das Definieren des Verhaltens eine wesentliche Optimierung aus? ...

Dies sind nur einige Faktoren, die mir einfallen. Es gibt natürlich viele, viele andere Faktoren, die von Sprachdesign-Komitees diskutiert werden, bevor ein Feature als "Implementierung definiert" oder "undefiniert" definiert wird.

Oben ist nur eine sehr kurze Darstellung; Der vollständige Artikel enthält Erläuterungen und Beispiele zu den in diesem Auszug genannten Punkten. es ist viel lesenswert. Zum Beispiel können Details, die für den "sechsten Faktor" angegeben wurden, einen Einblick in die Motivation für viele Aussagen im Java Memory Model ( JSR 133 ) geben und dabei helfen, zu verstehen, warum einige Optimierungen zulässig sind, was zu undefiniertem Verhalten führt, während andere verboten sind und dazu führen Einschränkungen wie Vorgängervorgänge und Kausalitätsanforderungen .

Keines der Artikelmaterialien ist für mich besonders neu, aber ich werde verdammt sein, wenn ich es jemals so elegant, präzise und verständlich präsentiert habe. Tolle.

Mücke
quelle
Ich werde hinzufügen, dass die JMM! = Zugrunde liegende Hardware und das Endergebnis eines ausgeführten Programms in Bezug auf die Parallelität von WinIntel vs Solaris abweichen können
Martijn Verburg
2
@MartijnVerburg das ist ein ziemlich guter Punkt. Der einzige Grund, warum ich zögere, es als "undefiniert" zu kennzeichnen, ist, dass das Speichermodell Einschränkungen wie " happen-before" und Kausalität bei der Ausführung eines korrekt synchronisierten Programms aufwirft
gnat
Die Spezifikation definiert zwar, wie es sich unter dem JMM verhalten soll, Intel und andere sind sich jedoch nicht immer einig ;-)
Martijn Verburg
@MartijnVerburg Ich denke , der wichtigste Punkt der JMM ist zu verhindern , dass über optimier Lecks „nicht einverstanden“ Prozessorhersteller. Soweit ich weiß, hatte Java vor 5.0 mit DEC Alpha solche Kopfschmerzen, als spekulative Schreibvorgänge unter der Haube in ein Programm wie "out of thin air" eindrangen konnten - daher ging die Kausalitätsanforderung in JSR 133 (JMM)
gnat
9
@MartinVerburg - Es ist Aufgabe eines JVM-Implementierers, sicherzustellen, dass sich die JVM gemäß der JLS / JMM-Spezifikation auf jeder unterstützten Hardwareplattform verhält. Wenn sich unterschiedliche Hardware unterschiedlich verhält, ist es die Aufgabe des JVM-Implementierers, sich damit zu befassen ... und dafür zu sorgen, dass es funktioniert.
Stephen C
10

Ich glaube nicht, dass es in Java ein undefiniertes Verhalten gibt, zumindest nicht im gleichen Sinne wie in C ++.

Der Grund dafür ist, dass Java eine andere Philosophie hat als C ++. Ein zentrales Entwurfsziel von Java bestand darin, dass Programme plattformübergreifend unverändert ausgeführt werden können. Daher definiert die Spezifikation alles sehr explizit.

Im Gegensatz dazu ist Effizienz ein zentrales Entwurfsziel von C und C ++: Es sollte keine Funktionen (einschließlich Plattformunabhängigkeit) geben, die die Leistung beeinträchtigen, auch wenn Sie sie nicht benötigen. Zu diesem Zweck werden in der Spezifikation einige Verhaltensweisen bewusst nicht definiert, da deren Definition auf einigen Plattformen zusätzliche Arbeit verursachen und somit die Leistung verringern würde, selbst für Personen, die Programme speziell für eine Plattform schreiben und alle ihre Eigenheiten kennen.

Es gibt sogar ein Beispiel, in dem Java aus genau diesem Grund gezwungen war, eine begrenzte Form von undefiniertem Verhalten rückwirkend einzuführen: Das Schlüsselwort strictfp wurde in Java 1.2 eingeführt, damit Gleitkommaberechnungen nicht genau dem IEEE 754-Standard folgen, wie es die Spezifikation zuvor gefordert hatte Dies erforderte zusätzlichen Arbeitsaufwand und verlangsamte alle Gleitkommaberechnungen auf einigen gängigen CPUs, wobei in einigen Fällen sogar schlechtere Ergebnisse erzielt wurden.

Michael Borgwardt
quelle
2
Ich denke, es ist wichtig, das andere Hauptziel von Java zu beachten: Sicherheit und Isolation. Ich denke, dies ist auch ein Grund für das Fehlen von "undefiniertem" Verhalten (wie in C ++).
K.Steff
3
@ K.Steff: Hyper-modernes C / C ++ ist für alle Sicherheitsaspekte aus der Ferne völlig ungeeignet. Angesichts der int x=-1; foo(); x<<=1;hypermodernen Philosophie würde es vorziehen, neu zu schreiben, foosodass jeder Pfad, der nicht endet, nicht erreichbar sein muss. Dies, wenn fooes sich um if (should_launch_missiles) { launch_missiles(); exit(1); }einen Compiler handelt, könnte (und sollte nach Ansicht mancher Leute) dies einfach vereinfachen launch_missiles(); exit(1);. Die traditionelle UB war zufällige Codeausführung, aber diese war früher an die Gesetze der Zeit und der Kausalität gebunden. New verbesserte UB ist von beiden nicht gebunden.
Supercat
3

Java ist sehr bemüht, undefiniertes Verhalten auszurotten, gerade wegen der Lehren aus früheren Sprachen. Beispielsweise werden Variablen auf Klassenebene automatisch initialisiert. lokale Variablen werden aus Performancegründen nicht automatisch initialisiert, es gibt jedoch eine ausgeklügelte Datenflussanalyse, um zu verhindern, dass jemand ein Programm schreibt, das dies erkennen kann. Verweise sind keine Zeiger, daher können keine ungültigen Verweise vorhanden sein, und die Dereferenzierung nullführt zu einer bestimmten Ausnahme.

Natürlich gibt es einige Verhaltensweisen, die nicht vollständig spezifiziert sind, und Sie können unzuverlässige Programme schreiben, wenn Sie davon ausgehen, dass dies der Fall ist. Wenn Sie beispielsweise über ein normales (nicht sortiertes) SetElement iterieren , garantiert die Sprache, dass Sie jedes Element genau einmal sehen, jedoch nicht in welcher Reihenfolge. Die Reihenfolge kann bei aufeinanderfolgenden Läufen gleich sein oder sich ändern. oder es kann gleich bleiben, solange keine anderen Zuordnungen vorgenommen werden, oder solange Sie Ihr JDK nicht aktualisieren usw. Es ist nahezu unmöglich, alle derartigen Effekte zu beseitigen. Zum Beispiel müssten Sie alle Kollektionsoperationen explizit anordnen oder randomisieren , und das ist die kleine zusätzliche Unbestimmtheit einfach nicht wert.

Kilian Foth
quelle
Referenzen sind Zeiger unter einem anderen Namen
curiousguy
@curiousguy - "Referenzen" erlauben im Allgemeinen keine arithmetische Manipulation ihres numerischen Wertes, was häufig für "Zeiger" erlaubt ist. Ersteres ist daher ein sichereres Konstrukt als Letzteres; In Kombination mit einem Speicherverwaltungssystem, bei dem der Speicher eines Objekts nicht wiederverwendet werden kann, solange ein gültiger Verweis darauf vorhanden ist, verhindern Verweise Fehler bei der Speichernutzung. Zeiger können dies nicht, selbst wenn eine geeignete Speicherverwaltung verwendet wird.
Jules
@Jules Dann ist es eine Frage der Terminologie: Sie können eine Sache als Zeiger oder Referenz bezeichnen und entscheiden, "Referenz" in "sicheren" Sprachen und "Zeiger" in Sprachen zu verwenden, die die Verwendung von Zeigerarithmetik und manueller Speicherverwaltung ermöglichen. (AFAIK "Zeigerarithmetik" wird nur in C / C ++ durchgeführt.)
curiousguy
2

Sie müssen das "Undefinierte Verhalten" und seinen Ursprung verstehen.

Undefiniertes Verhalten bedeutet ein Verhalten, das nicht durch die Standards definiert ist. C / C ++ hat zu viele verschiedene Compiler-Implementierungen und zusätzliche Funktionen. Diese zusätzlichen Funktionen banden den Code an den Compiler. Dies lag daran, dass es keine zentralisierte Sprachentwicklung gab. So wurden einige der erweiterten Funktionen einiger Compiler zu undefinierten Verhaltensweisen.

Während in Java die Sprachspezifikation von Sun-Oracle gesteuert wird und niemand sonst versucht, Spezifikationen und damit keine undefinierten Verhaltensweisen festzulegen.

Bearbeitet Speziell die Beantwortung der Frage

  1. Java ist frei von undefiniertem Verhalten, da Standards vor Compilern erstellt wurden
  2. Moderne C / C ++ - Compiler haben die Implementierungen mehr oder weniger standardisiert, aber die Funktionen, die vor der Standardisierung implementiert wurden, bleiben weiterhin als "undefiniertes Verhalten" gekennzeichnet, da die ISO diese Aspekte eingehalten hat.
Sarvex
quelle
2
Sie mögen Recht haben, dass es in Java kein UB gibt, aber selbst wenn eine Entität alles kontrolliert, kann es Gründe geben, UB zu haben, sodass der von Ihnen angegebene Grund nicht zu der Schlussfolgerung führt.
AProgrammer
2
Außerdem sind sowohl C als auch C ++ von ISO standardisiert. Es kann zwar mehrere Compiler geben, es gibt jedoch immer nur einen Standard.
MSalters
1
@ SarvexJatasra, ich stimme nicht zu, dass es die einzige Quelle von UB ist. Beispielsweise ist ein UB dereferenzierend und es gibt gute Gründe, einen UB in einer Sprache ohne GC zu belassen, selbst wenn Sie Ihre Spezifikation jetzt starten. Und diese Gründe haben nichts mit bestehender Praxis oder bestehenden Compilern zu tun.
AProgrammer
2
@SarvexJatasra, signierter Überlauf ist UB, weil der Standard dies ausdrücklich sagt (es ist sogar das Beispiel, das mit der Definition von UB angegeben wurde). Das Dereferenzieren eines ungültigen Zeigers ist aus dem gleichen Grund auch ein UB, wie der Standard sagt.
AProgrammer
2
@ bames53: Keiner der genannten Vorteile würde es erfordern, dass die Compiler die Breite der hypermodernen Breitengrade mit UB erreichen. Mit Ausnahme von Speicherzugriffen außerhalb der Grenzen und Stapelüberläufen, die "natürlich" eine zufällige Codeausführung auslösen können, kann ich mir keine sinnvolle Optimierung vorstellen, die einen größeren Spielraum erfordern würde, als zu sagen, dass die meisten UB-ischen Operationen unbestimmt ergeben Werte (die sich möglicherweise so verhalten, als hätten sie "zusätzliche Bits") und nur dann Konsequenzen, wenn sich die Dokumentation einer Implementierung ausdrücklich das Recht vorbehält, solche aufzuerlegen. Dokumente können "uneingeschränktes Verhalten" geben ...
Supercat
1

Java beseitigt im Wesentlichen das gesamte undefinierte Verhalten in C / C ++. (Zum Beispiel: Überlauf von Ganzzahlen mit Vorzeichen, Division durch Null, nicht initialisierte Variablen, Null-Zeiger-Dereferenzierung, Verschiebung um mehr als die Bitbreite, doppelte Freiheit, sogar "keine neue Zeile am Ende des Quellcodes") werden selten von Programmierern angetroffen.

  • Java Native Interface (JNI), eine Möglichkeit für Java, C- oder C ++ - Code aufzurufen. Es gibt viele Möglichkeiten, Fehler in JNI zu machen, z. B. die Funktionssignatur falsch zu machen, ungültige Aufrufe an JVM-Services auszuführen, Speicher zu beschädigen, Inhalte falsch zuzuweisen / freizugeben und vieles mehr. Ich habe diese Fehler bereits gemacht und im Allgemeinen stürzt die gesamte JVM ab, wenn ein Thread, der JNI-Code ausführt, einen Fehler feststellt.

  • Thread.stop(), die veraltet ist. Zitat:

    Warum ist Thread.stopveraltet?

    Weil es von Natur aus unsicher ist. Durch das Stoppen eines Threads werden alle gesperrten Monitore entsperrt. (Die Monitore werden entsperrt, da die ThreadDeathAusnahme den Stapel weitergibt.) Wenn sich eines der zuvor durch diese Monitore geschützten Objekte in einem inkonsistenten Zustand befand, zeigen andere Threads diese Objekte möglicherweise jetzt in einem inkonsistenten Zustand an. Solche Gegenstände sollen beschädigt sein. Wenn Threads mit beschädigten Objekten arbeiten, kann dies zu willkürlichem Verhalten führen. Dieses Verhalten kann subtil und schwer zu erkennen sein oder es kann ausgeprägt sein. Im Gegensatz zu anderen ungeprüften Ausnahmen werden ThreadDeathThreads stillschweigend beendet. Daher hat der Benutzer keine Warnung, dass sein Programm möglicherweise beschädigt ist. Die Korruption kann sich jederzeit nach dem tatsächlichen Schaden manifestieren, auch nach Stunden oder Tagen in der Zukunft.

    https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

Nayuki
quelle