Ich kann nicht verstehen, in welchen Szenarien wir eine unveränderliche Klasse brauchen.
Haben Sie jemals eine solche Anforderung gestellt? oder können Sie uns bitte ein reales Beispiel geben, wo wir dieses Muster verwenden sollten.
java
design-patterns
immutability
Rakesh Juyal
quelle
quelle
Antworten:
Die anderen Antworten scheinen sich zu sehr darauf zu konzentrieren, zu erklären, warum Unveränderlichkeit gut ist. Es ist sehr gut und ich benutze es wann immer möglich. Dies ist jedoch nicht Ihre Frage . Ich werde Ihre Frage Punkt für Punkt beantworten, um sicherzustellen, dass Sie die Antworten und Beispiele erhalten, die Sie benötigen.
"Bedürfnis" ist hier ein relativer Begriff. Unveränderliche Klassen sind ein Entwurfsmuster, das wie jedes Paradigma / Muster / Werkzeug dazu dient, das Erstellen von Software zu vereinfachen. Ebenso wurde viel Code geschrieben, bevor das OO-Paradigma aufkam, aber zähle mich zu den Programmierern, die OO "brauchen" . Unveränderliche Klassen wie OO werden nicht unbedingt benötigt , aber ich werde so tun, als ob ich sie brauche.
Wenn Sie die Objekte in der Problemdomäne nicht mit der richtigen Perspektive betrachten, sehen Sie möglicherweise keine Anforderung für ein unveränderliches Objekt. Es ist leicht zu glauben, dass für eine Problemdomäne keine unveränderlichen Klassen erforderlich sind, wenn Sie nicht wissen, wann Sie sie vorteilhaft verwenden sollen.
Ich verwende oft unveränderliche Klassen, in denen ich ein bestimmtes Objekt in meiner Problemdomäne als Wert oder feste Instanz betrachte . Diese Vorstellung hängt manchmal von der Perspektive oder dem Standpunkt ab, aber im Idealfall ist es einfach, in die richtige Perspektive zu wechseln, um gute Kandidatenobjekte zu identifizieren.
Sie können sich ein besseres Bild davon machen, wo unveränderliche Objekte wirklich nützlich sind (wenn nicht unbedingt erforderlich), indem Sie verschiedene Bücher / Online-Artikel lesen, um ein gutes Gefühl dafür zu entwickeln, wie man über unveränderliche Klassen denkt. Ein guter Artikel für den Einstieg ist die Java-Theorie und -Praxis: Mutieren oder nicht mutieren?
Ich werde versuchen, im Folgenden einige Beispiele zu nennen, wie man Objekte in verschiedenen Perspektiven (veränderlich oder unveränderlich) sehen kann, um zu verdeutlichen, was ich unter Perspektive verstehe.
Da Sie nach echten Beispielen gefragt haben, gebe ich Ihnen einige, aber beginnen wir zunächst mit einigen klassischen Beispielen.
Klassische Wertobjekte
Zeichenfolgen und Ganzzahlen werden oft als Werte betrachtet. Daher ist es nicht überraschend, dass die String-Klasse und die Integer-Wrapper-Klasse (sowie die anderen Wrapper-Klassen) in Java unveränderlich sind. Eine Farbe wird normalerweise als Wert betrachtet, daher die unveränderliche Farbklasse.
Gegenbeispiel
Im Gegensatz dazu wird ein Auto normalerweise nicht als Wertobjekt betrachtet. Das Modellieren eines Autos bedeutet normalerweise das Erstellen einer Klasse mit sich änderndem Zustand (Kilometerzähler, Geschwindigkeit, Kraftstoffstand usw.). Es gibt jedoch einige Domänen, in denen das Auto ein Wertobjekt sein kann. Beispielsweise kann ein Auto (oder speziell ein Automodell) als Wertobjekt in einer App betrachtet werden, um das richtige Motoröl für ein bestimmtes Fahrzeug nachzuschlagen.
Kartenspielen
Hast du jemals ein Spielkartenprogramm geschrieben? Ich tat. Ich hätte eine Spielkarte als veränderbares Objekt mit veränderlicher Farbe und Rang darstellen können. Eine Draw-Poker-Hand kann aus 5 festen Instanzen bestehen, bei denen das Ersetzen der 5. Karte in meiner Hand bedeuten würde, dass die 5. Spielkarteninstanz durch Ändern der Farbe und des Ranges der Ivars in eine neue Karte umgewandelt wird.
Ich neige jedoch dazu, eine Spielkarte als unveränderliches Objekt zu betrachten, das nach seiner Erstellung eine feste, unveränderliche Farbe und einen festen Rang hat. Meine Draw Poker Hand würde 5 Instanzen umfassen und das Ersetzen einer Karte in meiner Hand würde das Abwerfen einer dieser Instanzen und das Hinzufügen einer neuen zufälligen Instanz zu meiner Hand beinhalten.
Kartenprojektion
Ein letztes Beispiel ist, als ich an einem Kartencode gearbeitet habe, bei dem sich die Karte in verschiedenen Projektionen anzeigen könnte . Der ursprüngliche Code ließ die Karte eine feste, aber veränderbare Projektionsinstanz verwenden (wie die veränderbare Spielkarte oben). Das Ändern der Kartenprojektion bedeutete, die ivars der Kartenprojektionsinstanz (Projektionstyp, Mittelpunkt, Zoom usw.) zu mutieren.
Ich fand das Design jedoch einfacher, wenn ich mir eine Projektion als unveränderlichen Wert oder feste Instanz vorstellte. Das Ändern der Kartenprojektion bedeutete, dass die Kartenreferenz auf eine andere Projektionsinstanz verweist, anstatt die feste Projektionsinstanz der Karte zu mutieren. Dies machte es auch einfacher, benannte Projektionen wie z
MERCATOR_WORLD_VIEW
.quelle
Unveränderliche Klassen sind im Allgemeinen viel einfacher zu entwerfen, zu implementieren und korrekt zu verwenden . Ein Beispiel ist String: Die Implementierung von
java.lang.String
ist wesentlich einfacher als die vonstd::string
C ++, hauptsächlich aufgrund seiner Unveränderlichkeit.Ein besonderer Bereich, in dem Unveränderlichkeit einen besonders großen Unterschied macht, ist die Parallelität: Unveränderliche Objekte können sicher von mehreren Threads gemeinsam genutzt werden , während veränderbare Objekte durch sorgfältiges Design und Implementierung threadsicher gemacht werden müssen - normalerweise ist dies keine triviale Aufgabe.
Update: Effective Java 2nd Edition behebt dieses Problem ausführlich - siehe Punkt 15: Minimierung der Veränderlichkeit .
Siehe auch diese verwandten Beiträge:
quelle
Effektives Java von Joshua Bloch beschreibt mehrere Gründe, unveränderliche Klassen zu schreiben:
Im Allgemeinen empfiehlt es sich, ein Objekt unveränderlich zu machen, es sei denn, dies führt zu schwerwiegenden Leistungsproblemen. Unter solchen Umständen können veränderbare Builder-Objekte verwendet werden, um unveränderliche Objekte zu erstellen, z. B. StringBuilder
quelle
Hashmaps sind ein klassisches Beispiel. Der Schlüssel zu einer Karte muss unbedingt unveränderlich sein. Wenn der Schlüssel nicht unveränderlich ist und Sie einen Wert für den Schlüssel so ändern, dass hashCode () zu einem neuen Wert führt, ist die Zuordnung jetzt fehlerhaft (ein Schlüssel befindet sich jetzt an der falschen Stelle in der Hash-Tabelle).
quelle
Java ist praktisch eine Referenz. Manchmal wird eine Instanz mehrmals referenziert. Wenn Sie eine solche Instanz ändern, wird sie in allen Referenzen wiedergegeben. Manchmal möchten Sie dies einfach nicht, um die Robustheit und Thread-Sicherheit zu verbessern. Dann ist eine unveränderliche Klasse nützlich, so dass man gezwungen ist, eine neue Instanz zu erstellen und sie der aktuellen Referenz neu zuzuweisen. Auf diese Weise bleibt die ursprüngliche Instanz der anderen Referenzen unberührt.
Stellen Sie sich vor, wie Java aussehen würde, wenn
String
es veränderlich wäre.quelle
Date
undCalendar
veränderlich waren. Oh, warte, sie sind, OH SHString
ist veränderlich! (Hinweis: einige ältere JRockit-Versionen). Das Aufrufen von string.trim () führte dazu, dass die ursprüngliche ZeichenfolgeWir brauchen per se keine unveränderlichen Klassen, aber sie können sicherlich einige Programmieraufgaben erleichtern, insbesondere wenn mehrere Threads beteiligt sind. Sie müssen keine Sperre durchführen, um auf ein unveränderliches Objekt zuzugreifen, und alle Fakten, die Sie bereits zu einem solchen Objekt ermittelt haben, werden auch in Zukunft zutreffen.
quelle
Nehmen wir einen Extremfall: ganzzahlige Konstanten. Wenn ich eine Aussage wie "x = x + 1" schreibe, möchte ich 100% sicher sein, dass die Zahl "1" nicht irgendwie zu 2 wird, egal was irgendwo anders im Programm passiert.
Okay, Integer-Konstanten sind keine Klasse, aber das Konzept ist dasselbe. Angenommen, ich schreibe:
Sieht einfach aus. Aber wenn Strings nicht unveränderlich wären, müsste ich die Möglichkeit in Betracht ziehen, dass getCustomerName die customerId ändern könnte, sodass ich beim Aufruf von getCustomerBalance den Kontostand für einen anderen Kunden erhalte. Jetzt könnten Sie sagen: "Warum in aller Welt sollte jemand, der eine getCustomerName-Funktion schreibt, die ID ändern? Das würde keinen Sinn ergeben." Aber genau hier könnten Sie in Schwierigkeiten geraten. Die Person, die den obigen Code schreibt, könnte es als offensichtlich ansehen, dass die Funktionen den Parameter nicht ändern würden. Dann kommt jemand, der eine andere Verwendung dieser Funktion ändern muss, um den Fall zu behandeln, in dem ein Kunde mehrere Konten unter demselben Namen hat. Und er sagt: "Oh, hier ist diese praktische Funktion getCustomer name, die den Namen bereits nachschlägt. I '
Unveränderlichkeit bedeutet einfach, dass eine bestimmte Klasse von Objekten Konstanten sind, und wir können sie als Konstanten behandeln.
(Natürlich könnte der Benutzer einer Variablen ein anderes "konstantes Objekt" zuweisen. Jemand kann String s = "Hallo" schreiben und später s = "Auf Wiedersehen" schreiben. Wenn ich die Variable nicht endgültig mache, kann ich nicht sicher sein dass es nicht in meinem eigenen Codeblock geändert wird. Genau wie Ganzzahlkonstanten versichern Sie mir, dass "1" immer die gleiche Zahl ist, aber nicht, dass "x = 1" niemals durch Schreiben von "x = 2" geändert wird. Aber ich kann sicher sein, dass, wenn ich ein Handle für ein unveränderliches Objekt habe, keine Funktion, an die ich es übergebe, es an mir ändern kann, oder dass, wenn ich zwei Kopien davon mache, eine Änderung an der Variablen, die eine Kopie enthält, das nicht ändert andere. Etc.
quelle
Es gibt verschiedene Gründe für die Unveränderlichkeit:
String
Klasse.Wenn Sie also Daten über einen Netzwerkdienst senden möchten und die Garantie haben möchten , dass Ihr Ergebnis genau dem entspricht, das Sie gesendet haben, setzen Sie es als unveränderlich.
quelle
final
in Java gekennzeichneten Klassen sind unveränderlich, und nicht alle unveränderlichen Klassen sind unveränderlichfinal
.Ich werde dies aus einer anderen Perspektive angreifen. Ich finde, unveränderliche Objekte erleichtern mir das Leben beim Lesen von Code.
Wenn ich ein veränderliches Objekt habe, bin ich mir nie sicher, welchen Wert es hat, wenn es jemals außerhalb meines unmittelbaren Bereichs verwendet wird. Angenommen, ich erstelle
MyMutableObject
die lokalen Variablen einer Methode, fülle sie mit Werten aus und übergebe sie dann an fünf andere Methoden. JEDE dieser Methoden kann den Status meines Objekts ändern, daher muss eines von zwei Dingen auftreten:Das erste macht es schwierig, über meinen Code nachzudenken. Die zweite führt dazu, dass mein Code an Leistung verliert - ich ahme im Grunde genommen ein unveränderliches Objekt mit Copy-on-Write-Semantik nach, mache es aber immer wieder, unabhängig davon, ob die aufgerufenen Methoden den Status meines Objekts tatsächlich ändern oder nicht.
Wenn ich stattdessen verwende
MyImmutableObject
, kann ich sicher sein, dass ich die Werte für die Lebensdauer meiner Methode festlege. Es gibt keine "gruselige Aktion in der Ferne", die es unter mir ändern könnte, und ich muss keine defensiven Kopien meines Objekts erstellen, bevor ich die fünf anderen Methoden aufrufe. Wenn die anderen Methoden für ihre Zwecke zu ändern Dinge wollen sie haben , um die Kopie zu machen - aber sie dies nur tun , wenn sie wirklich eine Kopie machen müssen (im Gegensatz zu meinen es vor jedem einzelnen externen Methodenaufruf tun). Ich erspare mir die mentalen Ressourcen, um Methoden zu verfolgen, die möglicherweise nicht einmal in meiner aktuellen Quelldatei enthalten sind, und ich erspare dem System den Aufwand, für alle Fälle unnötige defensive Kopien zu erstellen.(Wenn ich mich außerhalb der Java-Welt und beispielsweise in der C ++ - Welt befinde, kann ich noch kniffliger werden. Ich kann die Objekte so aussehen lassen, als wären sie veränderlich, aber hinter den Kulissen lassen sie sie transparent auf jede klonen Art von Zustandsänderung - das ist Copy-on-Write - mit niemandem, der klüger ist.)
quelle
Meine 2 Cent für zukünftige Besucher:
2 Szenarien, in denen unveränderliche Objekte eine gute Wahl sind:
Im Multithreading
Parallelitätsprobleme in einer Multithread-Umgebung können sehr gut durch Synchronisierung gelöst werden, aber die Synchronisierung ist eine kostspielige Angelegenheit (würde hier nicht auf das "Warum" eingehen). Wenn Sie also unveränderliche Objekte verwenden, gibt es keine Synchronisierung, um das Parallelitätsproblem aufgrund des Status von zu lösen Unveränderliche Objekte können nicht geändert werden. Wenn der Status nicht geändert werden kann, können alle Threads nahtlos auf das Objekt zugreifen. Unveränderliche Objekte sind daher eine gute Wahl für gemeinsam genutzte Objekte in einer Umgebung mit mehreren Threads.
Als Schlüssel für Hash-basierte Sammlungen
Eines der wichtigsten Dinge, die bei der Arbeit mit Hash-basierten Auflistungen zu beachten sind, ist, dass der Schlüssel so sein sollte, dass er
hashCode()
für die Lebensdauer des Objekts immer den gleichen Wert zurückgibt. Wenn dieser Wert geändert wird, wird ein alter Eintrag in die Hash-basierte Auflistung vorgenommen Die Verwendung dieses Objekts kann nicht abgerufen werden, daher würde dies zu einem Speicherverlust führen. Da der Status unveränderlicher Objekte nicht geändert werden kann, treffen sie eine gute Wahl als Schlüssel für die Hash-basierte Sammlung. Wenn Sie also ein unveränderliches Objekt als Schlüssel für die Hash-basierte Erfassung verwenden, können Sie sicher sein, dass dadurch kein Speicherverlust auftritt (natürlich kann es immer noch zu einem Speicherverlust kommen, wenn das als Schlüssel verwendete Objekt nicht von irgendwoher referenziert wird sonst, aber darum geht es hier nicht).quelle
Unveränderliche Objekte sind Instanzen, deren Status sich nach dem Start nicht ändern. Die Verwendung solcher Objekte ist anforderungsspezifisch.
Die unveränderliche Klasse eignet sich für Caching-Zwecke und ist threadsicher.
quelle
Durch die Unveränderlichkeit können Sie sicher sein, dass sich das Verhalten / der Zustand des zugrunde liegenden unveränderlichen Objekts nicht ändert, wodurch Sie den zusätzlichen Vorteil erhalten, zusätzliche Operationen auszuführen:
Sie können problemlos mehrere Kerne / Verarbeitungen ( gleichzeitige / parallele Verarbeitung ) verwenden (da die Reihenfolge der Vorgänge keine Rolle mehr spielt).
Kann Caching für teure Vorgänge durchführen (da Sie sich des gleichen
Ergebnisses sicher sind ).
Kann problemlos debuggen (da die Laufhistorie kein Problem
mehr darstellt)
quelle
Die Verwendung des letzten Schlüsselworts macht nicht unbedingt etwas unveränderlich:
public class Scratchpad { public static void main(String[] args) throws Exception { SomeData sd = new SomeData("foo"); System.out.println(sd.data); //prints "foo" voodoo(sd, "data", "bar"); System.out.println(sd.data); //prints "bar" } private static void voodoo(Object obj, String fieldName, Object value) throws Exception { Field f = SomeData.class.getDeclaredField("data"); f.setAccessible(true); Field modifiers = Field.class.getDeclaredField("modifiers"); modifiers.setAccessible(true); modifiers.setInt(f, f.getModifiers() & ~Modifier.FINAL); f.set(obj, "bar"); } } class SomeData { final String data; SomeData(String data) { this.data = data; } }
Nur ein Beispiel, um zu demonstrieren, dass das Schlüsselwort "final" dazu dient, Programmiererfehler zu vermeiden, und nicht viel mehr. Während die Neuzuweisung eines Werts ohne endgültiges Schlüsselwort leicht versehentlich erfolgen kann, müsste das Abrufen dieser Länge zum Ändern eines Werts absichtlich erfolgen. Es dient zur Dokumentation und zur Vermeidung von Programmierfehlern.
quelle
Unveränderliche Datenstrukturen können auch beim Codieren rekursiver Algorithmen hilfreich sein. Angenommen, Sie versuchen, ein 3SAT- Problem zu lösen . Eine Möglichkeit besteht darin, Folgendes zu tun:
Wenn Sie eine veränderbare Struktur haben, um das Problem darzustellen, müssen Sie beim Vereinfachen der Instanz im TRUE-Zweig entweder:
Wenn Sie es jedoch auf clevere Weise codieren, können Sie eine unveränderliche Struktur haben, in der jede Operation eine aktualisierte (aber immer noch unveränderliche) Version des Problems zurückgibt (ähnlich wie
String.replace
- sie ersetzt nicht die Zeichenfolge, sondern gibt Ihnen nur eine neue ). Der naive Weg, dies zu implementieren, besteht darin, die "unveränderliche" Struktur einfach zu kopieren und bei jeder Änderung eine neue zu erstellen, um sie auf die zweite Lösung zu reduzieren, wenn Sie eine veränderbare haben, mit all dem Aufwand, aber Sie können es in mehr tun effizienter Weg.quelle
Einer der Gründe für die "Notwendigkeit" unveränderlicher Klassen ist die Kombination, alles als Referenz zu übergeben und schreibgeschützte Ansichten eines Objekts (dh C ++
const
) nicht zu unterstützen.Betrachten Sie den einfachen Fall einer Klasse, die das Beobachtermuster unterstützt:
class Person { public string getName() { ... } public void registerForNameChange(NameChangedObserver o) { ... } }
Wenn dies
string
nicht unveränderlich wäre, könnte diePerson
Klasse nichtregisterForNameChange()
korrekt implementiert werden, da jemand Folgendes schreiben und den Namen der Person effektiv ändern könnte, ohne eine Benachrichtigung auszulösen.void foo(Person p) { p.getName().prepend("Mr. "); }
In C ++ hat die Rückgabe von
getName()
aconst std::string&
den Effekt, dass die Referenz zurückgegeben wird und der Zugriff auf Mutatoren verhindert wird. Dies bedeutet, dass unveränderliche Klassen in diesem Kontext nicht erforderlich sind.quelle
Sie geben uns auch eine Garantie. Die Garantie der Unveränderlichkeit bedeutet, dass wir sie erweitern und neue Muster für Effizienz erstellen können, die sonst nicht möglich wären.
http://en.wikipedia.org/wiki/Singleton_pattern
quelle
Ein Merkmal unveränderlicher Klassen, das noch nicht aufgerufen wurde: Das Speichern eines Verweises auf ein tief unveränderliches Klassenobjekt ist ein effizientes Mittel zum Speichern des gesamten darin enthaltenen Zustands. Angenommen, ich habe ein veränderliches Objekt, das ein tief unveränderliches Objekt verwendet, um Statusinformationen im Wert von 50.000 zu speichern. Angenommen, ich möchte 25 Mal eine "Kopie" meines ursprünglichen (veränderlichen) Objekts erstellen (z. B. für einen "Rückgängig" -Puffer). Der Status kann sich zwischen den Kopiervorgängen ändern, ist dies jedoch normalerweise nicht der Fall. Das Erstellen einer "Kopie" des veränderlichen Objekts würde lediglich das Kopieren eines Verweises auf seinen unveränderlichen Zustand erfordern, sodass 20 Kopien einfach 20 Verweise ergeben würden. Wenn der Status dagegen in veränderlichen Objekten im Wert von 50.000 gehalten würde, müsste jede der 25 Kopiervorgänge eine eigene Kopie von Daten im Wert von 50.000 erstellen. Das Halten aller 25 Kopien würde das Halten eines Megas im Wert von größtenteils duplizierten Daten erfordern. Obwohl die erste Kopieroperation eine Kopie der Daten erzeugen würde, die sich niemals ändern wird, und die anderen 24 Operationen theoretisch einfach darauf zurückgreifen könnten, gäbe es in den meisten Implementierungen keine Möglichkeit für das zweite Objekt, eine Kopie der Daten anzufordern Informationen, um zu wissen, dass bereits eine unveränderliche Kopie vorhanden ist (*).
(*) Ein Muster, das manchmal nützlich sein kann, besteht darin, dass veränderbare Objekte zwei Felder haben, um ihren Status beizubehalten - eines in veränderlicher Form und eines in unveränderlicher Form. Objekte können als veränderlich oder unveränderlich kopiert werden und würden mit dem einen oder anderen Referenzsatz beginnen. Sobald das Objekt seinen Status ändern möchte, kopiert es den unveränderlichen Verweis auf den veränderlichen (falls dies noch nicht geschehen ist) und macht den unveränderlichen ungültig. Wenn das Objekt als unveränderlich kopiert wird und seine unveränderliche Referenz nicht festgelegt ist, wird eine unveränderliche Kopie erstellt und die unveränderliche Referenz darauf hingewiesen. Dieser Ansatz erfordert ein paar mehr Kopiervorgänge als eine "vollwertige Kopie beim Schreiben" (z. B. die Aufforderung, ein Objekt zu kopieren, das seit der letzten Kopie mutiert wurde, würde einen Kopiervorgang erfordern).
quelle
Sobald ein Objekt instanziiert wurde, kann sein Status während der Lebensdauer nicht mehr geändert werden. Das macht es auch threadsicher.
Offensichtlich String, Integer und BigDecimal usw. Sobald diese Werte erstellt wurden, können sie nicht mehr auf Lebenszeit geändert werden.
quelle
von Effective Java; Eine unveränderliche Klasse ist einfach eine Klasse, deren Instanzen nicht geändert werden können. Alle in jeder Instanz enthaltenen Informationen werden beim Erstellen bereitgestellt und sind für die Lebensdauer des Objekts festgelegt. Die Java-Plattformbibliotheken enthalten viele unveränderliche Klassen, einschließlich String, die primitiven Klassen in Boxen sowie BigInteger und BigDecimal. Dafür gibt es viele gute Gründe: Unveränderliche Klassen sind einfacher zu entwerfen, zu implementieren und zu verwenden als veränderbare Klassen. Sie sind weniger fehleranfällig und sicherer.
quelle