Wenn Sie das Schlüsselwort 'static' ohne das Schlüsselwort 'final' verwenden, sollte dies ein Signal sein, um Ihr Design sorgfältig zu prüfen. Selbst das Vorhandensein eines "Finales" ist kein Freipass, da ein veränderbares statisches Endobjekt genauso gefährlich sein kann.
Ich würde schätzen, dass ungefähr 85% der Zeit, in der ich eine "Statik" ohne "Finale" sehe, FALSCH ist. Oft finde ich seltsame Problemumgehungen, um diese Probleme zu maskieren oder zu verbergen.
Bitte erstellen Sie keine statischen Mutables. Besonders Sammlungen. Im Allgemeinen sollten Sammlungen initialisiert werden, wenn ihr enthaltendes Objekt initialisiert wird, und sollten so gestaltet sein, 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 Schmerzen bereiten. Ich weiß, weil ich diese Bugs sowohl erstellt als auch gejagt habe.
Wenn Sie weitere Informationen wünschen, 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 auf statischen Objekten basiert, kann nicht einfach in Einheiten getestet werden, und Statik kann (normalerweise) nicht einfach verspottet werden.
Wenn Sie Statik verwenden, ist es nicht möglich, die Implementierung der Klasse auszutauschen, um übergeordnete Komponenten zu testen. Stellen Sie sich beispielsweise ein statisches CustomerDAO vor, das Kundenobjekte zurückgibt, die es aus der Datenbank lädt. Jetzt 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 auszufüllen.
Das Auffüllen und Initialisieren der Datenbank dauert lange. Und meiner Erfahrung nach wird sich Ihr DB-Initialisierungsframework im Laufe der Zeit ändern, was bedeutet, dass sich Daten ändern und Tests möglicherweise unterbrochen werden. Stellen Sie sich vor, Kunde 1 war früher ein VIP, aber das DB-Initialisierungsframework hat sich geändert, und jetzt ist Kunde 1 kein VIP mehr, aber Ihr Test war fest codiert, um Kunde 1 zu laden.
Ein besserer Ansatz besteht darin, ein CustomerDAO zu instanziieren und es beim Erstellen an den CustomerFilter zu übergeben. (Ein noch besserer Ansatz wäre die Verwendung von Spring oder eines anderen Inversion of Control-Frameworks.
Sobald Sie dies getan haben, können Sie schnell ein alternatives DAO in Ihrem CustomerFilterTest verspotten oder auslöschen, sodass Sie mehr Kontrolle über den Test haben.
Ohne das statische DAO ist der Test schneller (keine Datenbankinitialisierung) und zuverlässiger (da er nicht fehlschlägt, wenn sich der Datenbankinitialisierungscode ändert). In diesem Fall muss beispielsweise sichergestellt werden, dass Kunde 1 ein VIP ist und immer sein wird, was den Test betrifft.
Ausführen von Tests
Die Statik verursacht ein echtes Problem, wenn mehrere Komponententests zusammen ausgeführt werden (z. B. mit Ihrem Continuous Integration-Server). Stellen Sie sich eine statische Karte von Netzwerk-Socket-Objekten vor, die von einem Test zum anderen geöffnet bleibt. Der erste Test öffnet möglicherweise einen Socket an Port 8080, aber Sie haben vergessen, die Karte zu löschen, wenn der Test abgerissen wird. Wenn jetzt ein zweiter Test gestartet wird, stürzt er wahrscheinlich ab, wenn versucht wird, einen neuen Socket für Port 8080 zu erstellen, da der Port noch belegt ist. Stellen Sie sich außerdem vor, dass Socket-Referenzen in Ihrer statischen Sammlung nicht entfernt werden und (mit Ausnahme von WeakHashMap) niemals zur Speicherbereinigung berechtigt sind, was zu einem Speicherverlust führt.
Dies ist ein übermäßig verallgemeinertes Beispiel, aber in großen Systemen tritt dieses Problem STÄNDIG auf. Die 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 Software-Design, und wenn Sie Bestrebungen nach hoher Verfügbarkeit haben, müssen Sie sich dessen bewusst sein.
Diese Probleme treten häufig bei Framework-Objekten auf, z. B. bei den Ebenen DB-Zugriff, Caching, Messaging und Protokollierung. Wenn Sie Java EE oder einige der besten Frameworks verwenden, verwalten diese wahrscheinlich viel davon für Sie, aber wenn Sie wie ich mit einem Legacy-System arbeiten, haben Sie möglicherweise viele benutzerdefinierte Frameworks, um auf diese Ebenen zuzugreifen.
Wenn sich die Systemkonfiguration, die für diese Framework-Komponenten gilt, zwischen Komponententests ändert und das Unit-Test-Framework die Komponenten nicht herunterfährt und neu erstellt, können diese Änderungen nicht wirksam werden. Wenn ein Test auf diesen Änderungen beruht, schlagen sie fehl .
Auch Nicht-Framework-Komponenten sind diesem Problem ausgesetzt. Stellen Sie sich eine statische Karte namens OpenOrders vor. Sie schreiben einen Test, der einige offene Aufträge erstellt, und prüfen, ob alle im richtigen Zustand sind. Anschließend endet der Test. Ein anderer Entwickler schreibt einen zweiten Test, der die benötigten Bestellungen in die OpenOrders-Karte einfügt und dann bestätigt, dass die Anzahl der Bestellungen korrekt ist. Diese Tests werden einzeln ausgeführt und bestehen beide. Wenn sie jedoch zusammen in einer Suite ausgeführt werden, schlagen sie fehl.
Schlimmer noch, ein Fehler kann auf der Reihenfolge basieren, in der die Tests ausgeführt wurden.
In diesem Fall vermeiden Sie durch die Vermeidung von Statik das Risiko, dass Daten über Testinstanzen hinweg erhalten bleiben, und gewährleisten so eine bessere Testzuverlässigkeit.
Subtile Fehler
Wenn Sie in einer Hochverfügbarkeitsumgebung oder an einem Ort arbeiten, an dem Threads möglicherweise gestartet und gestoppt werden, kann das oben erwähnte Problem mit Unit-Test-Suites auch dann auftreten, wenn Ihr Code in der Produktion ausgeführt wird.
Wenn Sie mit Threads arbeiten, anstatt ein statisches Objekt zum Speichern von Daten zu verwenden, ist es besser, ein Objekt zu verwenden, das während der Startphase des Threads initialisiert wurde. Auf diese Weise wird jedes Mal, wenn der Thread gestartet wird, eine neue Instanz des Objekts (mit einer möglicherweise neuen Konfiguration) erstellt, und Sie vermeiden, dass Daten von einer Instanz des Threads zur nächsten Instanz durchbluten.
Wenn ein Thread stirbt, wird ein statisches Objekt nicht zurückgesetzt oder Müll gesammelt. Stellen Sie sich vor, Sie haben einen Thread namens "EmailCustomers". Wenn er gestartet wird, wird eine statische String-Sammlung mit einer Liste von E-Mail-Adressen gefüllt und anschließend jede der Adressen per E-Mail gesendet. Nehmen wir an, der Thread wird unterbrochen oder irgendwie abgebrochen, sodass Ihr Hochverfügbarkeits-Framework den Thread neu startet. Wenn der Thread gestartet wird, wird die Kundenliste neu geladen. Da die Sammlung jedoch statisch ist, wird möglicherweise die Liste der E-Mail-Adressen aus der vorherigen Sammlung beibehalten. Jetzt erhalten einige Kunden möglicherweise doppelte E-Mails.
Nebenbei: Statisches Finale
Die Verwendung von "static final" ist effektiv das Java-Äquivalent einer C # -Definition, obwohl es technische Implementierungsunterschiede gibt. AC / C ++ #define wird vor der Kompilierung vom Vorprozessor aus dem Code ausgelagert. Ein Java "statisches Finale" wird am Ende auf dem Stapel gespeichert. Auf diese Weise ähnelt es eher einer "statischen const" -Variablen in C ++ als einer #define.
Zusammenfassung
Ich hoffe, dies erklärt einige grundlegende Gründe, warum Statik problematisch ist. Wenn Sie ein modernes Java-Framework wie Java EE oder Spring usw. verwenden, treten möglicherweise nicht viele dieser Situationen auf. Wenn Sie jedoch mit einer großen Anzahl von Legacy-Code arbeiten, können diese viel häufiger auftreten.