Was sind einige Beispiele aus der Praxis die Schlüsselrolle von Behauptungen zu verstehen?
Antworten
Zu viele Anzeigen?Behauptungen (im Wege des behaupten. Schlüsselwort) wurden in Java 1.4 hinzugefügt. Sie werden verwendet, um die Korrektheit einer Invariante im Code zu überprüfen. Sie sollten niemals im Produktionscode ausgelöst werden und sind ein Hinweis auf einen Fehler oder den Missbrauch eines Codepfads. Sie können zur Laufzeit über die Funktion -ea
Option auf der java
Befehl, sind aber nicht standardmäßig aktiviert.
Ein Beispiel:
public Foo acquireFoo(int id) {
Foo result = null;
if (id > 50) {
result = fooService.read(id);
} else {
result = new Foo(id);
}
assert result != null;
return result;
}
Nehmen wir an, Sie sollen ein Programm zur Steuerung eines Kernkraftwerks schreiben. Es ist ziemlich offensichtlich, dass selbst der kleinste Fehler katastrophale Folgen haben könnte, daher ist Ihr Code muss sein fehlerfrei (unter der Annahme, dass die JVM fehlerfrei ist, um des Arguments willen).
Java ist keine überprüfbare Sprache, d.h. Sie können nicht berechnen, dass das Ergebnis Ihrer Operation perfekt sein wird. Der Hauptgrund dafür sind Zeiger: Sie können überall und nirgends hinzeigen, daher kann man nicht berechnen, dass sie genau diesen Wert haben, zumindest nicht innerhalb einer vernünftigen Spanne von Code. Angesichts dieses Problems gibt es keine Möglichkeit zu beweisen, dass Ihr Code im Ganzen korrekt ist. Was Sie jedoch tun können, ist zu beweisen, dass Sie zumindest jeden Fehler finden, wenn er auftritt.
Diese Idee beruht auf der Vertragsmäßiges Design (DbC)-Paradigma: Sie definieren zunächst (mit mathematischer Präzision), was Ihre Methode tun soll, und verifizieren dies dann, indem Sie sie während der tatsächlichen Ausführung testen. Beispiel:
// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
return a + b;
}
Obwohl es ziemlich offensichtlich ist, dass dies gut funktioniert, werden die meisten Programmierer den darin versteckten Fehler nicht sehen (Hinweis: die Ariane V stürzte aufgrund eines ähnlichen Fehlers ab). Nun definiert DbC, dass Sie immer die Ein- und Ausgabe einer Funktion überprüfen, um sicherzustellen, dass sie korrekt funktioniert. Java kann dies durch Assertions erreichen:
// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
assert (Integer.MAX_VALUE - a >= b) : "Value of " + a + " + " + b + " is too large to add.";
final int result = a + b;
assert (result - a == b) : "Sum of " + a + " + " + b + " returned wrong sum " + result;
return result;
}
Sollte diese Funktion nun einmal ausfallen, werden Sie es merken. Sie wissen, dass es ein Problem in Ihrem Code gibt, Sie wissen, wo es liegt, und Sie wissen, was es verursacht hat (ähnlich wie bei Exceptions). Und was noch wichtiger ist: Sie stoppen die Ausführung genau dann, wenn das Problem auftritt, um zu verhindern, dass weiterer Code mit falschen Werten arbeitet und möglicherweise Schaden an dem verursacht, was er kontrolliert.
Java Exceptions sind ein ähnliches Konzept, aber sie können nicht alles überprüfen. Wenn Sie noch mehr Überprüfungen wünschen (auf Kosten der Ausführungsgeschwindigkeit), müssen Sie Assertions verwenden. Dadurch wird Ihr Code zwar aufgebläht, aber Sie können am Ende ein Produkt mit einer überraschend kurzen Entwicklungszeit liefern (je früher Sie einen Fehler beheben, desto geringer sind die Kosten). Und außerdem: Wenn es in Ihrem Code einen Fehler gibt, werden Sie ihn entdecken. Es gibt keine Möglichkeit, dass ein Fehler durchschlüpft und später Probleme verursacht.
Das ist zwar immer noch keine Garantie für fehlerfreien Code, aber es kommt dem sehr viel näher als herkömmliche Programme.
Assertions sind ein Werkzeug in der Entwicklungsphase, um Fehler in Ihrem Code zu finden. Sie sind so konzipiert, dass sie leicht entfernt werden können, so dass sie im Produktionscode nicht existieren. Assertions sind also nicht Teil der "Lösung", die Sie an den Kunden liefern. Sie sind interne Prüfungen, um sicherzustellen, dass die Annahmen, die Sie treffen, korrekt sind. Das häufigste Beispiel ist der Test auf Null. Viele Methoden sind auf diese Weise geschrieben:
void doSomething(Widget widget) {
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
Sehr oft sollte das Widget in einer Methode wie dieser einfach nie null sein. Wenn es also null ist, gibt es irgendwo einen Fehler in Ihrem Code, den Sie aufspüren müssen. Aber der obige Code wird Ihnen das nie sagen. In dem gut gemeinten Bemühen, "sicheren" Code zu schreiben, verstecken Sie also auch einen Fehler. Es ist viel besser, Code wie diesen zu schreiben:
/**
* @param Widget widget Should never be null
*/
void doSomething(Widget widget) {
assert widget != null;
widget.someMethod(); // ...
... // do more stuff with this widget
}
Auf diese Weise können Sie sicher sein, dass Sie diesen Fehler frühzeitig erkennen. (Es ist auch sinnvoll, im Vertrag festzulegen, dass dieser Parameter niemals null sein darf.) Schalten Sie unbedingt Assertions ein, wenn Sie Ihren Code während der Entwicklung testen. (Und Ihre Kollegen davon zu überzeugen, dies ebenfalls zu tun, ist oft schwierig, was ich sehr ärgerlich finde.)
Nun werden einige Ihrer Kollegen diesen Code beanstanden und argumentieren, dass Sie trotzdem die Nullprüfung einbauen sollten, um eine Ausnahme in der Produktion zu verhindern. In diesem Fall ist die Behauptung immer noch nützlich. Sie können sie wie folgt schreiben:
void doSomething(Widget widget) {
assert widget != null;
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
Auf diese Weise werden Ihre Kollegen froh sein, dass die Null-Prüfung für den Produktionscode vorhanden ist, aber während der Entwicklung verstecken Sie den Fehler nicht mehr, wenn das Widget Null ist.
Hier ist ein Beispiel aus der Praxis: Ich habe einmal eine Methode geschrieben, die zwei beliebige Werte auf Gleichheit vergleicht, wobei jeder der beiden Werte null sein kann:
/**
* Compare two values using equals(), after checking for null.
* @param thisValue (may be null)
* @param otherValue (may be null)
* @return True if they are both null or if equals() returns true
*/
public static boolean compare(final Object thisValue, final Object otherValue) {
boolean result;
if (thisValue == null) {
result = otherValue == null;
} else {
result = thisValue.equals(otherValue);
}
return result;
}
Dieser Code delegiert die Arbeit der equals()
in dem Fall, dass thisValue nicht null ist. Aber sie nimmt an, dass die equals()
Methode erfüllt korrekt den Vertrag von equals()
durch die korrekte Behandlung eines Null-Parameters.
Ein Kollege beanstandete meinen Code und sagte mir, dass viele unserer Klassen fehlerhaft seien equals()
Methoden, die nicht auf null testen, also sollte ich diese Prüfung in diese Methode aufnehmen. Es ist fraglich, ob das klug ist oder ob wir den Fehler erzwingen sollten, damit wir ihn erkennen und beheben können, aber ich habe auf meinen Kollegen gehört und eine Null-Prüfung eingebaut, die ich mit einem Kommentar versehen habe:
public static boolean compare(final Object thisValue, final Object otherValue) {
boolean result;
if (thisValue == null) {
result = otherValue == null;
} else {
result = otherValue != null && thisValue.equals(otherValue); // questionable null check
}
return result;
}
Die zusätzliche Prüfung hier, other != null
ist nur erforderlich, wenn die equals()
Methode prüft nicht auf null, wie es der Vertrag verlangt.
Anstatt mich auf eine fruchtlose Debatte mit meinem Kollegen darüber einzulassen, ob es klug ist, den fehlerhaften Code in unserer Codebasis zu belassen, fügte ich einfach zwei Assertions in den Code ein. Diese Behauptungen lassen mich während der Entwicklungsphase wissen, wenn eine unserer Klassen nicht implementiert equals()
damit ich das Problem beheben kann:
public static boolean compare(final Object thisValue, final Object otherValue) {
boolean result;
if (thisValue == null) {
result = otherValue == null;
assert otherValue == null || otherValue.equals(null) == false;
} else {
result = otherValue != null && thisValue.equals(otherValue);
assert thisValue.equals(null) == false;
}
return result;
}
Die wichtigsten Punkte, die es zu beachten gilt, sind die folgenden:
-
Behauptungen sind nur Werkzeuge für die Entwicklungsphase.
-
Der Sinn einer Assertion ist es, Sie wissen zu lassen, ob es einen Fehler gibt, nicht nur in Ihrem Code, sondern auch in Ihrer Codebasis . (Die Behauptungen hier werden tatsächlich Fehler in anderen Klassen aufzeigen).
-
Selbst wenn mein Kollege davon überzeugt wäre, dass unsere Klassen richtig geschrieben sind, wären die Aussagen hier dennoch nützlich. Es werden neue Klassen hinzugefügt, die möglicherweise nicht auf null testen, und diese Methode kann diese Fehler für uns kennzeichnen.
-
Bei der Entwicklung sollten Sie Assertions immer einschalten, auch wenn der von Ihnen geschriebene Code keine Assertions verwendet. Meine IDE ist so eingestellt, dass sie dies standardmäßig für jede neue ausführbare Datei tut.
-
Die Assertions ändern das Verhalten des Codes in der Produktion nicht, so dass mein Kollege froh ist, dass die Null-Prüfung vorhanden ist und dass diese Methode auch dann ordnungsgemäß ausgeführt wird, wenn die
equals()
Methode fehlerhaft ist. Ich bin glücklich, denn ich werde jede fehlerhafteequals()
Methode in der Entwicklung.
Außerdem sollten Sie Ihre Assertion-Policy testen, indem Sie eine temporäre Assertion einfügen, die fehlschlägt, so dass Sie sicher sein können, dass Sie entweder über die Protokolldatei oder einen Stack-Trace im Ausgabestrom informiert werden.
Wann sollte Assert verwendet werden?
Viele gute Antworten, die erklären, was die assert
Stichwort tut, aber nur wenige beantworten die eigentliche Frage, "Wann sollte die assert
Schlüsselwort im wirklichen Leben verwendet werden?" Die Antwort:
Fast nie
Behauptungen als Konzept sind wunderbar. Guter Code hat viele if (...) throw ...
Anweisungen (und deren Verwandte wie Objects.requireNonNull
et Math.addExact
). Bestimmte konzeptionelle Entscheidungen haben jedoch den Nutzen des Systems stark eingeschränkt assert
Stichwort selbst.
Die treibende Idee hinter dem assert
Das Schlüsselwort ist die vorzeitige Optimierung, und das Hauptmerkmal ist die Möglichkeit, alle Kontrollen einfach abzuschalten. In der Tat, die assert
Prüfungen sind standardmäßig ausgeschaltet.
Es ist jedoch von entscheidender Bedeutung, dass in der Produktion weiterhin invariante Prüfungen durchgeführt werden. Das liegt daran, dass eine perfekte Testabdeckung unmöglich ist und jeder Produktionscode Fehler aufweist, die mit Hilfe von Assertions diagnostiziert und entschärft werden sollten.
Daher ist die Verwendung von if (...) throw ...
sollte bevorzugt werden, ebenso wie es für die Überprüfung von Parameterwerten öffentlicher Methoden und für das Auslösen von IllegalArgumentException
.
Gelegentlich könnte man versucht sein, eine Invarianzprüfung zu schreiben, deren Verarbeitung unerwünscht viel Zeit in Anspruch nimmt (und die oft genug aufgerufen wird, um von Bedeutung zu sein). Solche Prüfungen verlangsamen jedoch das Testen, was ebenfalls unerwünscht ist. Solche zeitaufwändigen Prüfungen werden normalerweise als Unit-Tests geschrieben. Dennoch kann es manchmal sinnvoll sein, die assert
aus diesem Grund.
Nicht verwenden assert
weil es einfach sauberer und schöner ist als if (...) throw ...
(und das sage ich mit großem Schmerz, denn ich mag es sauber und schön). Wenn Sie einfach nicht anders können und selbst bestimmen können, wie Ihre Anwendung gestartet wird, dann verwenden Sie einfach assert
sondern immer Assertions in der Produktion aktivieren. Zugegebenermaßen neige ich dazu, dies zu tun. Ich setze mich für eine Lombok-Anmerkung ein, die Folgendes bewirkt assert
mehr zu handeln wie if (...) throw ...
. Stimmen Sie hier für ihn ab.
(Tadel: Die JVM-Entwickler waren ein Haufen furchtbarer, vorschnell optimierender Programmierer. Deshalb hört man von so vielen Sicherheitsproblemen im Java-Plugin und der JVM. Sie weigerten sich, grundlegende Überprüfungen und Zusicherungen in den Produktionscode aufzunehmen, und wir zahlen weiterhin den Preis dafür).
Das ist der häufigste Anwendungsfall. Angenommen, Sie schalten einen Enum-Wert ein:
switch (fruit) {
case apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
}
Solange Sie jeden Fall bearbeiten, ist alles in Ordnung. Aber eines Tages wird jemand fig zu Ihrer Aufzählung hinzufügen und vergessen, es zu Ihrer switch-Anweisung hinzuzufügen. Das führt zu einem Fehler, der schwer zu finden ist, weil die Auswirkungen erst spürbar werden, wenn Sie die switch-Anweisung verlassen haben. Wenn Sie aber Ihre switch-Anweisung so schreiben, können Sie den Fehler sofort erkennen:
switch (fruit) {
case apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
default:
assert false : "Missing enum value: " + fruit;
}
- See previous answers
- Weitere Antworten anzeigen