Suche…


Bemerkungen

Unveränderliche Objekte haben einen festen Status (keine Setter), daher muss der Status zum Zeitpunkt der Objekterstellung bekannt sein.

Obwohl dies nicht technisch erforderlich ist, sollten alle Felder final . Dadurch wird die unveränderliche Klasse Thread-sicher (vgl. Java Concurrency in Practice, 3.4.1).

Die Beispiele zeigen mehrere Muster, die dabei helfen können.

Erstellen einer unveränderlichen Version eines Typs durch defensives Kopieren.

Einige grundlegende Typen und Klassen in Java sind grundsätzlich veränderbar. Zum Beispiel sind alle Array-Typen veränderbar, ebenso Klassen wie java.util.Data . Dies kann in Situationen schwierig sein, in denen ein unveränderlicher Typ vorgeschrieben ist.

Eine Möglichkeit, damit umzugehen, besteht darin, einen unveränderlichen Wrapper für den veränderbaren Typ zu erstellen. Hier ist ein einfacher Wrapper für ein Array von Ganzzahlen

public class ImmutableIntArray {
    private final int[] array;
    
    public ImmutableIntArray(int[] array) {
        this.array = array.clone();
    }

    public int[] getValue() {
        return this.clone();
    }
}

Diese Klasse verwendet defensives Kopieren , um den veränderbaren Zustand (das int[] ) von jedem Code zu isolieren, der ihn möglicherweise verändert:

  • Der Konstruktor erstellt mit clone() eine eindeutige Kopie des Parameter-Arrays. Wenn der Aufrufer des Konstruktors das Parameterarray anschließend geändert hat, hat dies keinen Einfluss auf den Status des ImmutableIntArray .

  • Die getValue() -Methode verwendet auch clone() , um das zurückgegebene Array zu erstellen. Wenn der Aufrufer das Ergebnis-Array ändern würde, hat dies keinen Einfluss auf den Status des ImmutableIntArray .

Wir könnten auch ImmutableIntArray Methoden hinzufügen, um schreibgeschützte Operationen für das umschlossene Array auszuführen. z. B. Länge abrufen, Wert an einem bestimmten Index abrufen usw.

Beachten Sie, dass ein auf diese Weise implementierter unveränderlicher Wrapper-Typ nicht mit dem Originaltyp kompatibel ist. Sie können das erstere nicht einfach durch das Letztere ersetzen.

Das Rezept für eine unveränderliche Klasse

Ein unveränderliches Objekt ist ein Objekt, dessen Status nicht geändert werden kann. Eine unveränderliche Klasse ist eine Klasse, deren Instanzen nach Entwurf und Implementierung unveränderlich sind. Die Java-Klasse, die am häufigsten als Beispiel für Unveränderlichkeit dargestellt wird, ist java.lang.String .

Das folgende ist ein stereotypes Beispiel:

public final class Person {
    private final String name;
    private final String ssn;     // (SSN == social security number)

    public Person(String name, String ssn) {
        this.name = name;
        this.ssn = ssn;
    }

    public String getName() {
        return name;
    }
   
    public String getSSN() {
        return ssn;
    }
}

Eine Variante davon ist, den Konstruktor als private zu deklarieren und stattdessen eine public static Factory-Methode bereitzustellen.


Das Standardrezept für eine unveränderliche Klasse lautet wie folgt:

  • Alle Eigenschaften müssen in den Konstruktoren oder Factory-Methoden festgelegt werden.
  • Es sollte keine Setter geben.
  • Wenn aus Gründen der Schnittstellenkompatibilität Setter erforderlich sind, sollten sie entweder nichts tun oder eine Ausnahme auslösen.
  • Alle Eigenschaften sollten als private und final deklariert werden.
  • Für alle Eigenschaften, die Verweise auf veränderbare Typen sind:
    • Die Eigenschaft sollte mit einer tiefen Kopie des über den Konstruktor übergebenen Werts initialisiert werden
    • Der Getter der Eigenschaft sollte eine tiefe Kopie des Eigenschaftswerts zurückgeben.
  • Die Klasse sollte als final deklariert werden, um zu verhindern, dass jemand eine veränderliche Unterklasse einer unveränderlichen Klasse erstellt.

Ein paar andere Dinge zu beachten:

  • Die Unveränderlichkeit verhindert nicht, dass das Objekt nullfähig ist. null kann null einer String Variablen zugewiesen werden.
  • Wenn die Eigenschaften einer unveränderlichen Klasse als final deklariert werden, sind Instanzen von Natur aus Thread-sicher. Dies macht unveränderliche Klassen zu einem guten Baustein für die Implementierung von Multithread-Anwendungen.

Typische Designfehler, die verhindern, dass eine Klasse unveränderlich ist

Einige Setter verwenden, ohne alle erforderlichen Eigenschaften in den Konstruktoren festzulegen

public final class Person { // example of a bad immutability
    private final String name;
    private final String surname;
    public Person(String name) {
        this.name = name;
      }
    public String getName() { return name;}
    public String getSurname() { return surname;}
    public void setSurname(String surname) { this.surname = surname); }
}

Es ist leicht zu zeigen, dass die Person nicht unveränderlich ist:

Person person = new Person("Joe");
person.setSurname("Average"); // NOT OK, change surname field after creation

Um dies zu beheben, löschen Sie einfach setSurname() und setzen den Konstruktor wie folgt um:

public Person(String name, String surname) {
    this.name = name;
    this.surname = surname;
  }

Instanzvariablen werden nicht als privat und als final markiert

Schauen Sie sich die folgende Klasse an:

public final class Person {
    public String name;
    public Person(String name) {
        this.name = name;
     }
    public String getName() {
        return name;
    }
    
}

Der folgende Ausschnitt zeigt, dass die oben genannte Klasse nicht unveränderlich ist:

Person person = new Person("Average Joe");
person.name = "Magic Mike"; // not OK, new name for person after creation

Um dies zu beheben, kennzeichnen Sie einfach name property als private und final .


Offenlegen eines veränderlichen Objekts der Klasse in einem Getter

Schauen Sie sich die folgende Klasse an:

import java.util.List;
import java.util.ArrayList;
public final class Names {
    private final List<String> names;
    public Names(List<String> names) {
        this.names = new ArrayList<String>(names);
    }
    public List<String> getNames() {
        return names;
    }
    public int size() {
        return names.size();
    }
}

Names Klasse scheint auf den ersten Blick unveränderlich zu sein. Dies ist jedoch nicht der folgende Code:

List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
Names names = new Names(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList = names.getNames();
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"

Dies geschah , weil eine Änderung der Referenzliste zurück von getNames() die aktuelle Liste von ändern können Names .

Um dies zu beheben, vermeiden einfach Referenzen Rückkehr , die Klasse veränderbare Objekte verweisen entweder durch defensive Kopien zu machen, wie folgt:

public List<String> getNames() {
   return new ArrayList<String>(this.names); // copies elements
}

oder indem Sie Getter so erstellen, dass nur andere unveränderliche Objekte und Primitive wie folgt zurückgegeben werden:

public String getName(int index) {
    return names.get(index);
}
public int size() {
    return names.size();
}

Einfügen eines Konstruktors mit Objekten, die außerhalb der unveränderlichen Klasse geändert werden können

Dies ist eine Variation des vorherigen Fehlers. Schauen Sie sich die folgende Klasse an:

import java.util.List;
public final class NewNames {
    private final List<String> names;
    public Names(List<String> names) {
        this.names = names;
    }
    public String getName(int index) {
        return names.get(index);
    }
    public int size() {
        return names.size();
    }
}

Als Names Klasse zuvor scheint auch die NewNames Klasse auf den ersten Blick unveränderlich zu sein. NewNames ist jedoch nicht der NewNames , sondern der folgende Ausschnitt zeigt das Gegenteil:

List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
NewNames names = new NewNames(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"

Um dies zu beheben, wie beim vorherigen Fehler, erstellen Sie einfach defensive Kopien des Objekts, ohne es direkt der unveränderlichen Klasse zuzuweisen. Der Konstruktor kann also wie folgt geändert werden:

    public Names(List<String> names) {
        this.names = new ArrayList<String>(names);
    }

Die Methoden der Klasse überschreiben lassen

Schauen Sie sich die folgende Klasse an:

public class Person {
    private final String name;
    public Person(String name) {
        this.name = name;
      }
    public String getName() { return name;}
}

Person scheint auf den ersten Blick unveränderlich zu sein, es wird jedoch eine neue Unterklasse von Person definiert:

public class MutablePerson extends Person {
    private String newName;
    public MutablePerson(String name) {
        super(name);            
    }
    @Override
    public String getName() {
        return newName;
    }
    public void setName(String name) {
        newName = name;
    }
}

Jetzt kann die Mutabilität von Person (un) durch Polymorphismus ausgenutzt werden, indem die neue Unterklasse verwendet wird:

Person person = new MutablePerson("Average Joe");
System.out.println(person.getName()); prints Average Joe
person.setName("Magic Mike"); // NOT OK, person has now a new name!
System.out.println(person.getName()); // prints Magic Mike    

Um dies zu beheben, markieren Sie die Klasse entweder als final sodass sie nicht erweitert werden kann, oder deklarieren Sie alle Konstruktoren als private .



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow