Warum sollte man ein unveränderliches Klassenfinale in Java deklarieren?

83

Ich habe gelesen, dass wir Folgendes tun sollten , um eine Klasse in Java unveränderlich zu machen :

  1. Stellen Sie keine Setter zur Verfügung
  2. Markieren Sie alle Felder als privat
  3. Mach die Klasse endgültig

Warum ist Schritt 3 erforderlich? Warum sollte ich die Klasse markieren final?

Anand
quelle
java.math.BigIntegerKlasse ist Beispiel, seine Werte sind unveränderlich, aber es ist nicht endgültig.
Nandkumar Tekale
@ Nandkumar Wenn Sie eine haben BigInteger, wissen Sie nicht, ob es unveränderlich ist oder nicht. Es ist ein durcheinandergebrachtes Design. Ich java.io.Filebin ein interessanteres Beispiel.
Tom Hawtin - Tackline
1
Unterklassen dürfen Methoden nicht überschreiben. Der einfachste Weg, dies zu tun, besteht darin, die Klasse als endgültig zu deklarieren. Ein ausgefeilterer Ansatz besteht darin, den Konstruktor privat zu machen und Instanzen in Factory-Methoden zu erstellen
Raúl
1
@mkobit Fileist interessant, da es wahrscheinlich als unveränderlich eingestuft wird und daher zu TOCTOU-Angriffen (Time Of Check / Time Of Use) führt. Vertrauenswürdiger Code überprüft, ob der FilePfad akzeptabel ist, und verwendet dann den Pfad. Eine Unterklasse Filekann den Wert zwischen Prüfung und Verwendung ändern.
Tom Hawtin - Tackline
1
Make the class finaloder
Stellen

Antworten:

138

Wenn Sie die Klasse nicht markieren final, kann es sein, dass ich Ihre scheinbar unveränderliche Klasse plötzlich tatsächlich veränderlich mache. Betrachten Sie beispielsweise diesen Code:

public class Immutable {
     private final int value;

     public Immutable(int value) {
         this.value = value;
     }

     public int getValue() {
         return value;
     }
}

Angenommen, ich mache Folgendes:

public class Mutable extends Immutable {
     private int realValue;

     public Mutable(int value) {
         super(value);

         realValue = value;
     }

     public int getValue() {
         return realValue;
     }
     public void setValue(int newValue) {
         realValue = newValue;
     }

    public static void main(String[] arg){
        Mutable obj = new Mutable(4);
        Immutable immObj = (Immutable)obj;              
        System.out.println(immObj.getValue());
        obj.setValue(8);
        System.out.println(immObj.getValue());
    }
}

Beachten Sie, dass ich in meiner MutableUnterklasse das Verhalten getValuebeim Lesen eines neuen, veränderlichen Felds, das in meiner Unterklasse deklariert ist , überschrieben habe . Infolgedessen ist Ihre Klasse, die zunächst unveränderlich aussieht, wirklich nicht unveränderlich. Ich kann dieses MutableObjekt überall dort übergeben, wo ein ImmutableObjekt erwartet wird, was sehr schlechte Dinge für den Code tun kann, vorausgesetzt, das Objekt ist wirklich unveränderlich. Das Markieren der Basisklasse finalverhindert dies.

Hoffe das hilft!

templatetypedef
quelle
13
Wenn ich Mutable mache, ist m = new Mutable (4); m.setValue (5); Hier spiele ich mit veränderlichen Klassenobjekten herum, nicht mit unveränderlichen Klassenobjekten. Also bin ich immer noch verwirrt, warum unveränderliche Klassen nicht unveränderlich sind
Anand
18
@ anand- Stellen Sie sich vor, Sie haben eine Funktion, die Immutableein Argument akzeptiert . Ich kann Mutableseitdem ein Objekt an diese Funktion übergeben Mutable extends Immutable. Während Sie in dieser Funktion denken, dass Ihr Objekt unveränderlich ist, könnte ich einen sekundären Thread haben, der den Wert ändert, während die Funktion ausgeführt wird. Ich könnte Ihnen auch ein MutableObjekt geben, das die Funktion speichert, und später seinen Wert extern ändern. Mit anderen Worten, wenn Ihre Funktion davon ausgeht, dass der Wert unveränderlich ist, kann er leicht beschädigt werden, da ich Ihnen ein veränderliches Objekt geben und es später ändern könnte. Ist das sinnvoll?
Templatetypedef
6
@ anand- Die Methode, an die Sie das MutableObjekt übergeben, ändert das Objekt nicht. Die Sorge ist, dass diese Methode annehmen könnte, dass das Objekt unveränderlich ist, wenn es wirklich nicht ist. Beispielsweise könnte die Methode davon ausgehen, dass das Objekt, da sie es für unveränderlich hält, als Schlüssel in a verwendet werden kann HashMap. Ich könnte diese Funktion dann unterbrechen, indem ich a übergebe Mutable, darauf warte, dass es das Objekt als Schlüssel speichert, und dann das MutableObjekt ändere. Jetzt schlagen Suchvorgänge HashMapfehl, da ich den dem Objekt zugeordneten Schlüssel geändert habe. Ist das sinnvoll?
Templatetypedef
3
@ templatetypedef- Ja, ich habe es jetzt bekommen. Ich muss sagen, das war eine großartige Erklärung. Ich brauchte ein wenig Zeit, um es zu verstehen
Anand
1
@ SAM- Das stimmt. Dies spielt jedoch keine Rolle, da die überschriebenen Funktionen diese Variable nie wieder referenzieren.
Templatetypedef
47

Im Gegensatz zu dem, was viele Leute glauben, ist finales nicht erforderlich , eine unveränderliche Klasse zu bilden .

Das Standardargument für die Erstellung unveränderlicher Klassen finallautet: Wenn Sie dies nicht tun, können Unterklassen die Veränderlichkeit erhöhen und damit den Vertrag der Oberklasse verletzen. Klienten der Klasse werden Unveränderlichkeit annehmen, werden aber überrascht sein, wenn etwas unter ihnen mutiert.

Wenn Sie dieses Argument auf das logische Extrem bringen, sollten alle Methoden ausgeführt werden final, da andernfalls eine Unterklasse eine Methode auf eine Weise überschreiben könnte, die nicht dem Vertrag ihrer Oberklasse entspricht. Es ist interessant, dass die meisten Java-Programmierer dies als lächerlich ansehen, aber irgendwie mit der Idee einverstanden sind, dass unveränderliche Klassen sein sollten final. Ich vermute, dass es etwas damit zu tun hat, dass Java-Programmierer im Allgemeinen nicht ganz mit dem Begriff der Unveränderlichkeit vertraut sind und vielleicht eine Art unscharfes Denken in Bezug auf die vielfältigen Bedeutungen des finalSchlüsselworts in Java.

Die Einhaltung des Vertrags Ihrer Oberklasse kann oder sollte vom Compiler nicht immer durchgesetzt werden. Der Compiler kann bestimmte Aspekte Ihres Vertrags erzwingen (z. B. einen Mindestsatz von Methoden und deren Typensignaturen), aber es gibt viele Teile typischer Verträge, die vom Compiler nicht erzwungen werden können.

Unveränderlichkeit ist Bestandteil des Vertrages einer Klasse. Es unterscheidet sich ein wenig von einigen Dingen, an die die Leute eher gewöhnt sind, weil es besagt, was die Klasse (und alle Unterklassen) nicht können, während ich denke, dass die meisten Java- (und im Allgemeinen OOP-) Programmierer dazu neigen, Verträge als verhältnismäßig zu betrachten Was eine Klasse kann , nicht was sie nicht kann .

Unveränderlichkeit betrifft auch mehr als nur eine einzelne Methode - sie betrifft die gesamte Instanz -, aber dies unterscheidet sich nicht wesentlich von der Art equalsund Weise und hashCodein der Java-Arbeit. Für diese beiden Methoden ist ein spezifischer Vertrag festgelegt Object. Dieser Vertrag legt sehr sorgfältig Dinge fest, die diese Methoden nicht können. Dieser Vertrag wird in Unterklassen präzisiert. Es ist sehr leicht zu überschreiben equalsoder hashCodeauf eine Weise, die gegen den Vertrag verstößt. Wenn Sie nur eine dieser beiden Methoden ohne die andere überschreiben, besteht die Möglichkeit, dass Sie gegen den Vertrag verstoßen. So soll equalsund hashCodeerklärt wurde , finalin Objectdies zu vermeiden? Ich denke, die meisten würden argumentieren, dass sie nicht sollten. Ebenso ist es nicht notwendig, unveränderliche Klassen zu erstellenfinal.

Das heißt, die meisten Ihrer Klassen, unveränderlich oder nicht, sollten es wahrscheinlich sein final. Siehe Effektive Java Second Edition, Punkt 17: "Entwurf und Dokument zur Vererbung oder zum Verbot".

Eine korrekte Version von Schritt 3 wäre also: "Machen Sie die Klasse endgültig oder dokumentieren Sie beim Entwerfen für Unterklassen eindeutig, dass alle Unterklassen weiterhin unveränderlich sein müssen."

Laurence Gonsalves
quelle
3
Es ist erwähnenswert, dass Java erwartet, aber nicht erzwingt, dass bei zwei Objekten Xund Yder Wert von X.equals(Y)unveränderlich ist (solange Xund Yweiterhin auf dieselben Objekte verwiesen wird). Es hat ähnliche Erwartungen in Bezug auf Hash-Codes. Es ist klar, dass niemand erwarten sollte, dass der Compiler die Unveränderlichkeit von Äquivalenzbeziehungen und Hash-Codes erzwingt (da dies einfach nicht möglich ist). Ich sehe keinen Grund, warum die Leute erwarten sollten, dass es für andere Aspekte eines Typs durchgesetzt wird.
Supercat
1
Es gibt auch viele Fälle, in denen es nützlich sein kann, einen abstrakten Typ zu haben, dessen Vertrag die Unveränderlichkeit festlegt. Beispielsweise kann man einen abstrakten ImmutableMatrix-Typ haben, der bei einem gegebenen Koordinatenpaar a zurückgibt double. Man könnte ein ableiten GeneralImmutableMatrix, das ein Array als Hintergrundspeicher verwendet, aber man könnte auch z. B. ImmutableDiagonalMatrixeinfach ein Array von Elementen entlang der Diagonale speichern (das Lesen von Element X, Y würde Arr [X] ergeben, wenn X == y und sonst Null). .
Supercat
2
Diese Erklärung gefällt mir am besten. finalWenn Sie immer eine unveränderliche Klasse erstellen, wird deren Nützlichkeit eingeschränkt, insbesondere wenn Sie eine API entwerfen, die erweitert werden soll. Im Interesse der Thread-Sicherheit ist es sinnvoll, Ihre Klasse so unveränderlich wie möglich zu gestalten, sie jedoch erweiterbar zu halten. Sie können die Felder protected finalanstelle von erstellen private final. Erstellen Sie dann explizit einen Vertrag (in der Dokumentation) über Unterklassen, die die Unveränderlichkeits- und Gewindesicherheitsgarantien einhalten.
neugieriger Techizen
4
+1 - Ich bin bis zum Bloch-Zitat bei dir. Wir müssen nicht so paranoid sein. 99% der Codierung sind kooperativ. Keine große Sache, wenn jemand wirklich etwas überschreiben muss, lassen Sie ihn. 99% von uns schreiben keine Kern-APIs wie String oder Collection. Viele Ratschläge von Bloch basieren leider auf solchen Anwendungsfällen. Sie sind für die meisten Programmierer, insbesondere für neue, nicht wirklich hilfreich.
ZhongYu
Ich würde es vorziehen, unveränderliche Klasse zu machen final. Nur weil equalsund hashcodenicht endgültig sein kann, heißt das nicht, dass ich dasselbe für unveränderliche Klassen tolerieren kann, es sei denn, ich habe einen gültigen Grund dafür, und in diesem Fall kann ich Dokumentation oder Tests als Verteidigungslinie verwenden.
O.Badr
21

Markieren Sie nicht das gesamte Klassenfinale.

Es gibt triftige Gründe dafür, dass eine unveränderliche Klasse erweitert werden kann, wie in einigen anderen Antworten angegeben. Daher ist es nicht immer eine gute Idee, die Klasse als endgültig zu markieren.

Es ist besser, Ihre Immobilien als privat und endgültig zu markieren, und wenn Sie den "Vertrag" schützen möchten, markieren Sie Ihre Getter als endgültig.

Auf diese Weise können Sie zulassen, dass die Klasse erweitert wird (ja, möglicherweise sogar durch eine veränderbare Klasse), jedoch sind die unveränderlichen Aspekte Ihrer Klasse geschützt. Eigenschaften sind privat und können nicht aufgerufen werden. Getter für diese Eigenschaften sind endgültig und können nicht überschrieben werden.

Jeder andere Code, der eine Instanz Ihrer unveränderlichen Klasse verwendet, kann sich auf die unveränderlichen Aspekte Ihrer Klasse verlassen, selbst wenn die übergebene Unterklasse in anderen Aspekten veränderbar ist. Da es sich um eine Instanz Ihrer Klasse handelt, würde sie natürlich nicht einmal über diese anderen Aspekte Bescheid wissen.

Justin Ohms
quelle
1
Ich möchte lieber alle unveränderlichen Aspekte in einer separaten Klasse zusammenfassen und mittels Komposition verwenden. Es wäre einfacher zu überlegen, als veränderlichen und unveränderlichen Zustand in einer einzigen Codeeinheit zu mischen.
Toniedzwiedz
1
Stimme voll und ganz zu. Die Zusammensetzung wäre ein sehr guter Weg, um dies zu erreichen, vorausgesetzt, Ihre veränderliche Klasse enthält Ihre unveränderliche Klasse. Wenn Ihre Struktur umgekehrt ist, wäre dies eine gute Teillösung.
Justin Ohms
5

Wenn es nicht endgültig ist, kann jeder die Klasse erweitern und tun, was er will, z. B. Setter bereitstellen, Ihre privaten Variablen beschatten und sie im Grunde veränderlich machen.

digitaljoel
quelle
Obwohl Sie Recht haben, ist nicht klar, warum die Tatsache, dass Sie veränderliches Verhalten hinzufügen können, die Dinge notwendigerweise beschädigen würde, wenn die Basisklassenfelder alle privat und endgültig sind. Die reale Gefahr besteht darin, dass die Unterklasse ein zuvor unveränderliches Objekt durch Überschreiben des Verhaltens in ein veränderliches Objekt verwandelt.
Templatetypedef
4

Dies schränkt andere Klassen ein, die Ihre Klasse erweitern.

Die letzte Klasse kann nicht um andere Klassen erweitert werden.

Wenn eine Klasse die Klasse erweitert, die Sie als unveränderlich festlegen möchten, kann sich der Status der Klasse aufgrund von Vererbungsprinzipien ändern.

Klären Sie einfach "es kann sich ändern". Unterklassen können das Verhalten von Oberklassen überschreiben, z. B. das Überschreiben von Methoden (wie die Antwort templatetypedef / Ted Hop).

kosa
quelle
2
Das stimmt, aber warum ist es hier notwendig?
Templatetypedef
@templatetypedef: Du bist zu schnell. Ich bearbeite meine Antwort mit unterstützenden Punkten.
kosa
4
Im Moment ist Ihre Antwort so vage, dass ich glaube, sie beantwortet die Frage überhaupt nicht. Was meinst du mit "es kann den Status der Klasse aufgrund von Vererbungsprinzipien ändern?" Wenn Sie nicht bereits wissen, warum Sie es markieren sollten final, kann ich nicht sehen, wie diese Antwort hilft.
Templatetypedef
3

Wenn Sie es nicht endgültig machen, kann ich es erweitern und nicht veränderlich machen.

public class Immutable {
  privat final int val;
  public Immutable(int val) {
    this.val = val;
  }

  public int getVal() {
    return val;
  }
}

public class FakeImmutable extends Immutable {
  privat int val2;
  public FakeImmutable(int val) {
    super(val);
  }

  public int getVal() {
    return val2;
  }

  public void setVal(int val2) {
    this.val2 = val2;
  }
}

Jetzt kann ich FakeImmutable an jede Klasse übergeben, die Immutable erwartet, und es wird sich nicht wie der erwartete Vertrag verhalten.

Roger Lindsjö
quelle
2
Ich denke, das ist ziemlich identisch mit meiner Antwort.
Templatetypedef
Ja, nach der Hälfte der Veröffentlichung überprüft, keine neuen Antworten. Und dann, als es gepostet wurde, war deins 1 Minute vor mir. Zumindest haben wir nicht für alles die gleichen Namen verwendet.
Roger Lindsjö
Eine Korrektur: In der FakeImmutable-Klasse sollte der Konstruktorname FakeImmutable NOT Immutable sein
Sunny Gupta
2

Zum Erstellen einer unveränderlichen Klasse ist es nicht zwingend erforderlich, die Klasse als endgültig zu markieren.

Lassen Sie mich eines dieser Beispiele aus den Java-Klassen selbst nehmen. Die "BigInteger" -Klasse ist unveränderlich, aber nicht endgültig.

Tatsächlich ist Unveränderlichkeit ein Konzept, nach dem das erstellte Objekt dann nicht geändert werden kann.

Nehmen wir an, aus JVM-Sicht müssen alle Threads aus JVM-Sicht dieselbe Kopie des Objekts gemeinsam nutzen und es ist vollständig erstellt, bevor ein Thread darauf zugreift und sich der Status des Objekts nach seiner Erstellung nicht ändert.

Unveränderlichkeit bedeutet, dass Sie den Status des Objekts nach seiner Erstellung nicht mehr ändern können. Dies wird durch drei Daumenregeln erreicht, mit denen der Compiler erkennt, dass die Klasse unveränderlich ist und wie folgt lautet: -

  • Alle nicht privaten Felder sollten endgültig sein
  • Stellen Sie sicher, dass die Klasse keine Methode enthält, mit der die Felder des Objekts direkt oder indirekt geändert werden können
  • In der Klasse definierte Objektreferenzen können nicht außerhalb der Klasse geändert werden

Weitere Informationen finden Sie unter der folgenden URL

http://javaunturnedtopics.blogspot.in/2016/07/can-we-create-immutable-class-without.html

Tarun Bedi
quelle
1

Angenommen, Sie haben die folgende Klasse:

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class PaymentImmutable {
    private final Long id;
    private final List<String> details;
    private final Date paymentDate;
    private final String notes;

    public PaymentImmutable (Long id, List<String> details, Date paymentDate, String notes) {
        this.id = id;
        this.notes = notes;
        this.paymentDate = paymentDate == null ? null : new Date(paymentDate.getTime());
        if (details != null) {
            this.details = new ArrayList<String>();

            for(String d : details) {
                this.details.add(d);
            }
        } else {
            this.details = null;
        }
    }

    public Long getId() {
        return this.id;
    }

    public List<String> getDetails() {
        if(this.details != null) {
            List<String> detailsForOutside = new ArrayList<String>();
            for(String d: this.details) {
                detailsForOutside.add(d);
            }
            return detailsForOutside;
        } else {
            return null;
        }
    }

}

Dann verlängern Sie es und brechen seine Unveränderlichkeit.

public class PaymentChild extends PaymentImmutable {
    private List<String> temp;
    public PaymentChild(Long id, List<String> details, Date paymentDate, String notes) {
        super(id, details, paymentDate, notes);
        this.temp = details;
    }

    @Override
    public List<String> getDetails() {
        return temp;
    }
}

Hier testen wir es:

public class Demo {

    public static void main(String[] args) {
        List<String> details = new ArrayList<>();
        details.add("a");
        details.add("b");
        PaymentImmutable immutableParent = new PaymentImmutable(1L, details, new Date(), "notes");
        PaymentImmutable notImmutableChild = new PaymentChild(1L, details, new Date(), "notes");

        details.add("some value");
        System.out.println(immutableParent.getDetails());
        System.out.println(notImmutableChild.getDetails());
    }
}

Das Ausgabeergebnis ist:

[a, b]
[a, b, some value]

Wie Sie sehen können, können untergeordnete Klassen veränderbar sein, während die ursprüngliche Klasse ihre Unveränderlichkeit beibehält. Folglich können Sie in Ihrem Entwurf nicht sicher sein, dass das von Ihnen verwendete Objekt unveränderlich ist, es sei denn, Sie machen Ihre Klasse endgültig.

Valijon
quelle
0

Angenommen, die folgende Klasse wäre nicht final:

public class Foo {
    private int mThing;
    public Foo(int thing) {
        mThing = thing;
    }
    public int doSomething() { /* doesn't change mThing */ }
}

Es ist anscheinend unveränderlich, weil selbst Unterklassen nicht geändert werden können mThing. Eine Unterklasse kann jedoch veränderlich sein:

public class Bar extends Foo {
    private int mValue;
    public Bar(int thing, int value) {
        super(thing);
        mValue = value;
    }
    public int getValue() { return mValue; }
    public void setValue(int value) { mValue = value; }
}

Jetzt kann Foonicht mehr garantiert werden, dass ein Objekt, das einer Variablen vom Typ zugewiesen werden kann, mmeable ist. Dies kann Probleme mit Dingen wie Hashing, Gleichheit, Parallelität usw. verursachen.

Ted Hopp
quelle
0

Design an sich hat keinen Wert. Design wird immer verwendet, um ein Ziel zu erreichen. Was ist das Ziel hier? Wollen wir die Anzahl der Überraschungen im Code reduzieren? Wollen wir Fehler verhindern? Befolgen wir blind Regeln?

Auch Design ist immer mit Kosten verbunden. Jedes Design, das den Namen verdient, bedeutet, dass Sie einen Zielkonflikt haben .

In diesem Sinne müssen Sie Antworten auf diese Fragen finden:

  1. Wie viele offensichtliche Fehler werden dadurch verhindert?
  2. Wie viele subtile Fehler wird dies verhindern?
  3. Wie oft wird dadurch anderer Code komplexer (= fehleranfälliger)?
  4. Erleichtert oder erschwert dies das Testen?
  5. Wie gut sind die Entwickler in Ihrem Projekt? Wie viel Anleitung mit einem Vorschlaghammer brauchen sie?

Angenommen, Sie haben viele Nachwuchsentwickler in Ihrem Team. Sie werden verzweifelt jede dumme Sache versuchen, nur weil sie noch keine guten Lösungen für ihre Probleme kennen. Wenn die Klasse endgültig ist, können Fehler (gut) vermieden, aber auch "clevere" Lösungen gefunden werden, z. B. das Kopieren all dieser Klassen in veränderbare Klassen überall im Code.

Auf der anderen Seite wird es sehr schwierig sein, eine Klasse zu erstellen, finalnachdem sie überall verwendet wird, aber es ist einfach, eine finalKlasse nicht zu erstellen.final später zu wenn Sie herausfinden, dass Sie sie erweitern müssen.

Wenn Sie Schnittstellen ordnungsgemäß verwenden, können Sie das Problem "Ich muss dieses veränderbar machen" vermeiden, indem Sie immer die Schnittstelle verwenden und später bei Bedarf eine veränderbare Implementierung hinzufügen.

Fazit: Für diese Antwort gibt es keine "beste" Lösung. Es hängt davon ab, welchen Preis Sie bereit sind und welchen Sie zahlen müssen.

Aaron Digulla
quelle
0

Die Standardbedeutung von equals () entspricht der referenziellen Gleichheit. Bei unveränderlichen Datentypen ist dies fast immer falsch. Sie müssen also die equals () -Methode überschreiben und durch Ihre eigene Implementierung ersetzen. Verknüpfung

Anitha BS
quelle