Wenn Sie das Schlüsselwort "static" ohne das Schlüsselwort "final" verwenden, sollte dies ein Zeichen dafür sein, dass Sie Ihren Entwurf sorgfältig überdenken sollten. Selbst das Vorhandensein eines "final" ist kein Freifahrtschein, da ein veränderbares statisches finales Objekt ebenso gefährlich sein kann.
Ich würde schätzen, dass in etwa 85 % der Fälle, in denen ich ein "Static" ohne ein "Final" sehe, es FALSCH ist. Oft finde ich seltsame Umgehungslösungen, um diese Probleme zu verschleiern oder zu verbergen.
Bitte erstellen Sie keine statischen Mutables. Insbesondere Collections. Im Allgemeinen sollten Collections initialisiert werden, wenn ihr enthaltendes Objekt initialisiert wird, und sie sollten so entworfen werden, dass sie zurückgesetzt oder vergessen werden, wenn ihr enthaltendes Objekt vergessen wird.
Die Verwendung von Statik kann zu sehr subtilen Fehlern führen, die den Ingenieuren tagelang zu schaffen machen. Ich weiß das, denn ich habe diese Fehler sowohl erzeugt als auch gejagt.
Wenn Sie weitere Einzelheiten erfahren möchten, lesen Sie bitte weiter
Warum nicht Statik verwenden?
Es gibt viele Probleme mit der Statik, einschließlich des Schreibens und Ausführens von Tests, sowie subtile Fehler, die nicht sofort offensichtlich sind.
Code, der sich auf statische Objekte stützt, kann nicht einfach unitgetestet werden, und statische Objekte können (in der Regel) nicht einfach gemockt werden.
Wenn Sie Statik verwenden, ist es nicht möglich, die Implementierung der Klasse auszulagern, um Komponenten höherer Ebenen zu testen. Stellen Sie sich zum Beispiel eine statische CustomerDAO vor, die Kundenobjekte zurückgibt, die sie aus der Datenbank lädt. Nun habe ich eine Klasse CustomerFilter, die auf einige Kundenobjekte zugreifen muss. Wenn CustomerDAO statisch ist, kann ich keinen Test für CustomerFilter schreiben, ohne zuerst meine Datenbank zu initialisieren und nützliche Informationen einzupflegen.
Und die Population und Initialisierung der Datenbank nimmt viel Zeit in Anspruch. Und meiner Erfahrung nach wird sich Ihr DB-Initialisierungs-Framework im Laufe der Zeit ändern, was bedeutet, dass sich die Daten verändern und die Tests möglicherweise nicht funktionieren. IE, stellen Sie sich vor, Kunde 1 verwendet werden, um ein VIP, aber die DB-Initialisierung Rahmen geändert, und jetzt Kunde 1 ist nicht mehr VIP, aber Ihr Test wurde hart-codiert, um Kunden 1 zu laden
Ein besserer Ansatz ist es, ein CustomerDAO zu instanziieren und es an den CustomerFilter zu übergeben, wenn er erstellt wird. (Ein noch besserer Ansatz wäre die Verwendung von Spring oder einem anderen Framework für die Inversion der Kontrolle.
Sobald Sie dies getan haben, können Sie schnell eine alternative DAO in Ihrem CustomerFilterTest mocken oder stub out, so dass Sie mehr Kontrolle über den Test haben,
Ohne die statische DAO ist der Test schneller (keine Datenbankinitialisierung) und zuverlässiger (da er nicht fehlschlägt, wenn sich der Code der Datenbankinitialisierung ändert). In diesem Fall wird zum Beispiel sichergestellt, dass Kunde 1 immer ein VIP ist und bleiben wird, soweit der Test betroffen ist.
Ausführen von Tests
Statik ist ein echtes Problem, wenn Suiten von Unit-Tests zusammen ausgeführt werden (zum Beispiel mit Ihrem Continuous Integration Server). Stellen Sie sich eine statische Zuordnung von Netzwerk-Socket-Objekten vor, die von einem Test zum anderen offen bleibt. Der erste Test öffnet vielleicht einen Socket am Port 8080, aber Sie haben vergessen, die Map zu löschen, wenn der Test abgebrochen wird. Wenn nun ein zweiter Test gestartet wird, stürzt er wahrscheinlich ab, wenn er versucht, einen neuen Socket für Port 8080 zu erstellen, da der Port noch belegt ist. Stellen Sie sich auch vor, dass die Socket-Referenzen in Ihrer statischen Collection nicht entfernt werden und (mit Ausnahme der WeakHashMap) nie für Garbage Collect in Frage kommen, was zu einem Speicherleck führt.
Dies ist ein übergeneralisiertes Beispiel, aber in großen Systemen tritt dieses Problem IMMER auf. Viele Leute denken nicht daran, dass Unit-Tests ihre Software wiederholt in derselben JVM starten und stoppen, aber es ist ein guter Test für Ihr Softwaredesign, und wenn Sie eine hohe Verfügbarkeit anstreben, müssen Sie sich dessen bewusst sein.
Diese Probleme treten häufig bei Framework-Objekten auf, z. B. beim DB-Zugriff, beim Caching, beim Messaging und bei den Protokollierungsschichten. Wenn Sie Java EE oder einige Best-of-Breed-Frameworks verwenden, verwalten diese wahrscheinlich vieles davon für Sie, aber wenn Sie wie ich mit einem Altsystem zu tun haben, haben Sie möglicherweise viele benutzerdefinierte Frameworks für den Zugriff auf diese Schichten.
Wenn sich die Systemkonfiguration, die für diese Framework-Komponenten gilt, zwischen den Unit-Tests ändert und das Unit-Test-Framework die Komponenten nicht abbaut und neu erstellt, können diese Änderungen nicht wirksam werden, und wenn sich ein Test auf diese Änderungen stützt, schlägt er fehl.
Auch Komponenten, die nicht zu einem Framework gehören, sind von diesem Problem betroffen. Stellen Sie sich eine statische Map namens OpenOrders vor. Sie schreiben einen Test, der ein paar offene Aufträge erstellt und überprüft, ob sie alle im richtigen Zustand sind, dann endet der Test. Ein anderer Entwickler schreibt einen zweiten Test, der die benötigten Aufträge in die OpenOrders-Map einträgt und dann überprüft, ob die Anzahl der Aufträge korrekt ist. Einzeln ausgeführt, würden beide Tests erfolgreich sein, aber wenn sie zusammen in einer Suite ausgeführt werden, schlagen sie fehl.
Schlimmer noch, das Scheitern könnte auf der Reihenfolge der Durchführung der Tests beruhen.
In diesem Fall vermeiden Sie durch den Verzicht auf statische Daten das Risiko, dass Daten über mehrere Testinstanzen hinweg bestehen bleiben, was eine höhere Zuverlässigkeit der Tests gewährleistet.
Subtile Wanzen
Wenn Sie in einer Hochverfügbarkeitsumgebung arbeiten oder überall dort, wo Threads gestartet und gestoppt werden können, können dieselben Bedenken wie bei den Unit-Test-Suites auch dann gelten, wenn Ihr Code in der Produktion ausgeführt wird.
Bei der Arbeit mit Threads ist es besser, ein Objekt zu verwenden, das während der Startphase des Threads initialisiert wird, als ein statisches Objekt zum Speichern von Daten zu verwenden. Auf diese Weise wird jedes Mal, wenn der Thread gestartet wird, eine neue Instanz des Objekts (mit einer potenziell neuen Konfiguration) erstellt, und Sie vermeiden, dass Daten aus einer Instanz des Threads in die nächste Instanz übergehen.
Wenn ein Thread stirbt, wird ein statisches Objekt nicht zurückgesetzt oder garbage collected. Stellen Sie sich vor, Sie haben einen Thread mit dem Namen "EmailCustomers", der beim Start eine statische String-Sammlung mit einer Liste von E-Mail-Adressen auffüllt und dann beginnt, E-Mails an jede dieser Adressen zu senden. Angenommen, der Thread wird irgendwie unterbrochen oder abgebrochen, so dass Ihr Hochverfügbarkeits-Framework den Thread neu startet. Wenn der Thread dann startet, wird die Liste der Kunden neu geladen. Da die Sammlung jedoch statisch ist, könnte die Liste der E-Mail-Adressen aus der vorherigen Sammlung beibehalten werden. Nun erhalten einige Kunden möglicherweise doppelte E-Mails.
Eine Randbemerkung: Statisches Finale
Die Verwendung von "static final" ist praktisch das Java-Äquivalent eines C #define, obwohl es technische Implementierungsunterschiede gibt. Ein C/C++ #define wird vom Präprozessor vor der Kompilierung aus dem Code ausgelagert. Ein "static final" in Java landet im Klassenspeicher der JVM und ist damit (normalerweise) dauerhaft im Arbeitsspeicher gespeichert. In dieser Hinsicht ähnelt sie eher einer "static const"-Variablen in C++ als einer #define.
Zusammenfassung
Ich hoffe, dass dies dazu beiträgt, ein paar grundlegende Gründe zu erklären, warum die Statik problematisch ist. Wenn Sie ein modernes Java-Framework wie Java EE oder Spring usw. verwenden, werden Sie vielleicht nicht auf viele dieser Situationen stoßen, aber wenn Sie mit einem großen Bestand an Legacy-Code arbeiten, können sie viel häufiger auftreten.