Dies ist eine nicht triviale Frage. Grundsätzlich müssen Sie über jeden internen Zustand einer Klasse nachdenken, den Sie einer anderen Klasse über Getter oder durch den Aufruf von Settern einer anderen Klasse geben. Zum Beispiel, wenn Sie dies tun:
Date now = new Date();
someObject.setDate(now);
// another use of "now" that expects its value to not have changed
dann haben Sie möglicherweise zwei Probleme:
someObject
könnte möglicherweise den Wert von " now
", was bedeutet, dass die obige Methode, wenn sie später diese Variable verwendet, einen anderen Wert haben könnte als erwartet, und
- wenn nach der Übergabe von "
now
" zu someObject
ändern Sie seinen Wert, und wenn someObject
keine Verteidigungskopie erstellt haben, haben Sie den internen Zustand von someObject
.
Sie sollten sich entweder gegen beide Fälle absichern, oder Sie sollten dokumentieren, was Sie erwarten, je nachdem, wer der Kunde des Codes ist, was erlaubt oder nicht erlaubt ist. Ein anderer Fall ist, wenn eine Klasse eine Map
und Sie stellen einen Getter für die Map
selbst. Wenn die Map
ist Teil des internen Zustands des Objekts und das Objekt erwartet, dass vollständig den Inhalt des Map
dann sollten Sie niemals soll die Map
aus. Wenn Sie einen Getter für eine Karte bereitstellen müssen, dann geben Sie Collections.unmodifiableMap(myMap)
代わりに myMap
. Hier wollen Sie wahrscheinlich nicht einen Klon oder eine defensive Kopie wegen der möglichen Kosten zu machen. Indem Sie Ihre Map so verpackt zurückgeben, dass sie nicht geändert werden kann, schützen Sie Ihren internen Zustand vor Änderungen durch eine andere Klasse.
Aus vielen Gründen, clone()
ist oft nicht die richtige Lösung. Einige bessere Lösungen sind:
- Für Getter:
- Statt der Rückgabe einer
Map
, nur Rückgabe Iterator
s entweder an den keySet
oder an den Map.Entry
oder was auch immer dem Client-Code erlaubt, das zu tun, was er tun muss. Mit anderen Worten: Geben Sie etwas zurück, das im Wesentlichen eine schreibgeschützte Ansicht Ihres internen Zustands ist, oder
- Geben Sie Ihr veränderbares Zustandsobjekt zurück, das in einen unveränderlichen Wrapper verpackt ist, ähnlich wie bei
Collections.unmodifiableMap()
- Anstatt eine
Map
bieten eine get
Methode, die einen Schlüssel annimmt und den entsprechenden Wert aus der Map zurückgibt. Wenn alle Clients mit der Methode Map
ist, Werte daraus zu gewinnen, dann geben Sie den Kunden nicht die Map
selbst; bieten Sie stattdessen einen Getter an, der die Map
's get()
Methode.
- Für Konstrukteure:
- Verwenden Sie Kopierkonstruktoren in Ihren Objektkonstruktoren, um eine Kopie von allem zu erstellen, was übergeben wird und veränderbar ist.
- Entwerfen Sie, wenn möglich, unveränderliche Mengen als Konstruktorargumente, anstatt veränderliche Mengen zu verwenden. Manchmal ist es sinnvoll, die von
new Date().getTime()
zum Beispiel, statt einer Date
Objekt.
- Machen Sie so viel aus Ihrem Staat
final
wie möglich, aber denken Sie daran, dass ein final
Objekt kann immer noch veränderbar sein und ein final
Array kann noch geändert werden.
In allen Fällen, in denen es eine Frage darüber gibt, wem der veränderbare Zustand "gehört", dokumentieren Sie es in den Gettern oder Settern oder Konstruktoren. Dokumentieren Sie es irgendwo.
Hier ist ein triviales Beispiel für schlechten Code:
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date now = new Date();
Thread t1 = new Thread(new MyRunnable(now, 500));
t1.start();
try { Thread.sleep(250); } catch (InterruptedException e) { }
now.setTime(new Date().getTime()); // BAD! Mutating our Date!
Thread t2 = new Thread(new MyRunnable(now, 500));
t2.start();
}
static public class MyRunnable implements Runnable {
private final Date date;
private final int count;
public MyRunnable(final Date date, final int count) {
this.date = date;
this.count = count;
}
public void run() {
try { Thread.sleep(count); } catch (InterruptedException e) { }
long time = new Date().getTime() - date.getTime();
System.out.println("Runtime = " + time);
}
}
}
Sie sollte sehen, dass jeder Runnable 500 msec schläft, aber stattdessen erhalten Sie die falsche Zeitinformation. Wenn Sie den Konstruktor ändern, um eine defensive Kopie zu erstellen:
public MyRunnable(final Date date, final int count) {
this.date = new Date(date.getTime());
this.count = count;
}
dann erhalten Sie die richtige Zeitangabe. Dies ist ein triviales Beispiel. Sie wollen nicht ein kompliziertes Beispiel debuggen müssen.
ANMERKUNG: A gemeinsame Ergebnis einer unzureichenden Verwaltung des Staates ist eine ConcurrentModificationException
bei der Iteration über eine Sammlung.
Sollten Sie defensiv programmieren? Wenn Sie können Garantie Wenn Sie davon ausgehen, dass Ihr Projekt immer von demselben kleinen Team erfahrener Programmierer geschrieben und gewartet wird, dass diese kontinuierlich daran arbeiten und sich die Details des Projekts merken können, dass dieselben Personen während der gesamten Lebensdauer des Projekts daran arbeiten und dass das Projekt nie "groß" wird, dann können Sie vielleicht darauf verzichten. Aber die Kosten der defensiven Programmierung sind nicht hoch, außer in den seltensten Fällen - und der Nutzen ist groß. Außerdem ist defensives Programmieren eine gute Angewohnheit. Sie wollen nicht die Entwicklung schlechter Gewohnheiten fördern, indem Sie veränderbare Daten an Stellen weitergeben, die sie nicht haben sollten. Diese wird dich eines Tages beißen. All dies hängt natürlich von der erforderlichen Betriebszeit Ihres Projekts ab.