Wie verhindere ich die Änderung eines privaten Feldes in einer Klasse?

165

Stellen Sie sich vor, ich habe diese Klasse:

public class Test
{
  private String[] arr = new String[]{"1","2"};    

  public String[] getArr() 
  {
    return arr;
  }
}

Jetzt habe ich eine andere Klasse, die die obige Klasse verwendet:

Test test = new Test();
test.getArr()[0] ="some value!"; //!!!

Das ist also das Problem: Ich habe von außen auf ein privates Feld einer Klasse zugegriffen! Wie kann ich das verhindern? Ich meine, wie kann ich dieses Array unveränderlich machen? Bedeutet dies, dass Sie sich mit jeder Getter-Methode nach oben arbeiten können, um auf das private Feld zuzugreifen? (Ich möchte keine Bibliotheken wie Guava. Ich muss nur den richtigen Weg kennen, um dies zu tun.)

Hossein
quelle
10
Eigentlich, so dass es finalfunktioniert die Änderung des Feldes verhindern . Das Verhindern der Modifikation des Objectdurch ein Feld genannten ist jedoch komplizierter.
OldCurmudgeon
22
Es gibt ein Problem mit Ihrem mentalen Modell, wenn Sie der Meinung sind, dass das Ändern eines Arrays, auf das eine Referenz in einem privaten Feld gespeichert ist, dasselbe ist wie das Ändern eines privaten Felds.
Joren
45
Wenn es privat ist, warum sollte man es überhaupt aussetzen?
Erik Reppen
7
Es hat eine Weile gedauert, bis ich das herausgefunden habe, aber es verbessert immer meinen Code, wenn ich sicherstelle, dass alle Datenstrukturen vollständig in ihrem Objekt gekapselt sind - was bedeutet, dass es keine Möglichkeit gibt, Ihr "arr" zu "bekommen", sondern zu tun, was immer Sie müssen die Klasse oder einen Iterator bereitstellen.
Bill K
8
Ist noch jemand überrascht, warum diese Frage so viel Aufmerksamkeit erhielt?
Roman

Antworten:

163

Sie müssen eine Kopie Ihres Arrays zurückgeben.

public String[] getArr() {
  return arr == null ? null : Arrays.copyOf(arr, arr.length);
}
OldCurmudgeon
quelle
121
Ich möchte die Leute daran erinnern, dass, obwohl dies die richtige Antwort auf die Frage ist, die beste Lösung für das Problem tatsächlich darin besteht, wie sp00m sagt - eine zurückzugeben Unmodifiable List.
OldCurmudgeon
48
Nicht, wenn Sie tatsächlich ein Array zurückgeben müssen.
Svish
8
Tatsächlich ist die beste Lösung für das besprochene Problem oft, wie @MikhailVladimirov sagt: - eine Ansicht des Arrays oder der Sammlung bereitzustellen oder zurückzugeben .
Christoffer Hammarström
20
Stellen Sie jedoch sicher, dass es sich um eine tiefe Kopie der Daten handelt, nicht um eine flache Kopie. Für Strings macht es keinen Unterschied, für andere Klassen wie Date, in denen der Inhalt der Instanz geändert werden kann, kann es einen Unterschied machen.
Jwenting
16
@Svish Ich hätte drastischer sein sollen: Wenn Sie ein Array von einer API-Funktion zurückgeben, machen Sie es falsch. In der privaten Funktionen in einer Bibliothek es könnte richtig sein. In einer API (wo der Schutz vor Modifizierbarkeit eine Rolle spielt) ist dies niemals der Fall.
Konrad Rudolph
377

Wenn Sie eine Liste anstelle eines Arrays verwenden können, bietet Collections eine nicht änderbare Liste :

public List<String> getList() {
    return Collections.unmodifiableList(list);
}
sp00m
quelle
Es wurde eine Antwort hinzugefügt , die Collections.unmodifiableList(list)als Zuordnung zum Feld verwendet wird, um sicherzustellen, dass die Liste nicht von der Klasse selbst geändert wird.
Maarten Bodewes
Ich frage mich nur, wenn Sie diese Methode dekompilieren, erhalten Sie viele neue Schlüsselwörter. Beeinträchtigt dies nicht die Leistung, wenn getList () häufig Zugriff erhält? Zum Beispiel im Rendercode.
Thomas15v
2
Beachten Sie, dass dies funktioniert, wenn die Elemente der Arrays in sich unveränderlich Stringsind, aber wenn sie veränderbar sind, muss möglicherweise etwas getan werden, um zu verhindern, dass der Aufrufer sie ändert.
Lii
45

Der Modifikator privateschützt nur das Feld selbst vor dem Zugriff durch andere Klassen, nicht jedoch die Objektreferenzen in diesem Feld. Wenn Sie ein referenziertes Objekt schützen müssen, geben Sie es einfach nicht weiter. Veränderung

public String [] getArr ()
{
    return arr;
}

zu:

public String [] getArr ()
{
    return arr.clone ();
}

oder zu

public int getArrLength ()
{
    return arr.length;
}

public String getArrElementAt (int index)
{
    return arr [index];
}
Mikhail Vladimirov
quelle
5
Dieser Artikel spricht sich nicht gegen die Verwendung von Klonen für Arrays aus, sondern nur für Objekte, die in Unterklassen unterteilt werden können.
Lyn Headley
28

Das Collections.unmodifiableListwurde schon erwähnt - das Arrays.asList()seltsamerweise nicht! Meine Lösung wäre auch, die Liste von außen zu verwenden und das Array wie folgt zu verpacken:

String[] arr = new String[]{"1", "2"}; 
public List<String> getList() {
    return Collections.unmodifiableList(Arrays.asList(arr));
}

Das Problem beim Kopieren des Arrays ist: Wenn Sie dies jedes Mal tun, wenn Sie auf den Code zugreifen und das Array groß ist, werden Sie mit Sicherheit viel Arbeit für den Garbage Collector erstellen. Die Kopie ist also ein einfacher, aber wirklich schlechter Ansatz - ich würde sagen "billig", aber speicherintensiv! Besonders wenn Sie mehr als nur 2 Elemente haben.

Wenn Sie sich den Quellcode von ansehen Arrays.asListund Collections.unmodifiableListdort tatsächlich nicht viel erstellt wird. Der erste umschließt das Array nur, ohne es zu kopieren, der zweite umschließt nur die Liste und macht Änderungen daran nicht verfügbar.

michael_s
quelle
Sie gehen hier davon aus, dass das Array einschließlich der Zeichenfolgen jedes Mal kopiert wird. Es werden nicht nur die Verweise auf die unveränderlichen Zeichenfolgen kopiert. Solange Ihr Array nicht riesig ist, ist der Overhead vernachlässigbar. Wenn das Array riesig ist, gehen Sie für Listen und den immutableListganzen Weg!
Maarten Bodewes
Nein, ich habe diese Annahme nicht gemacht - wo? Es geht um die Referenzen, die kopiert werden - egal ob es sich um einen String oder ein anderes Objekt handelt. Ich sage nur, dass ich für große Arrays nicht empfehlen würde, das Array zu kopieren, und dass die Operationen Arrays.asListund Collections.unmodifiableListfür größere Arrays wahrscheinlich billiger sind. Vielleicht kann jemand berechnen, wie viele Elemente erforderlich sind, aber ich habe bereits Code in for-Schleifen mit Arrays gesehen, die Tausende von Elementen enthalten, die kopiert werden, um Änderungen zu verhindern - das ist verrückt!
michael_s
6

Sie können auch verwenden, ImmutableListwas besser als der Standard sein sollte unmodifiableList. Die Klasse ist Teil von Guava- Bibliotheken, die von Google erstellt wurden.

Hier ist die Beschreibung:

Im Gegensatz zu Collections.unmodutableList (java.util.List), einer Ansicht einer separaten Sammlung, die sich noch ändern kann, enthält eine Instanz von ImmutableList ihre eigenen privaten Daten und wird sich niemals ändern

Hier ist ein einfaches Beispiel für die Verwendung:

public class Test
{
  private String[] arr = new String[]{"1","2"};    

  public ImmutableList<String> getArr() 
  {
    return ImmutableList.copyOf(arr);
  }
}
iTech
quelle
Verwenden Sie diese Lösung, wenn Sie der TestKlasse nicht vertrauen können , dass die zurückgegebene Liste unverändert bleibt . Sie müssen sicherstellen, dass dies ImmutableListzurückgegeben wird und dass die Elemente der Liste ebenfalls unveränderlich sind. Ansonsten sollte meine Lösung auch sicher sein.
Maarten Bodewes
3

Unter diesem Gesichtspunkt sollten Sie die Systemarray-Kopie verwenden:

public String[] getArr() {
   if (arr != null) {
      String[] arrcpy = new String[arr.length];
      System.arraycopy(arr, 0, arrcpy, 0, arr.length);
      return arrcpy;
   } else
      return null;
   }
}
GM Ramesh
quelle
-1, da es beim Anrufen nichts bewirkt cloneund nicht so präzise ist.
Maarten Bodewes
2

Sie können eine Kopie der Daten zurückgeben. Der Anrufer, der die Daten ändern möchte, ändert nur die Kopie

public class Test {
    private static String[] arr = new String[] { "1", "2" };

    public String[] getArr() {

        String[] b = new String[arr.length];

        System.arraycopy(arr, 0, b, 0, arr.length);

        return b;
    }
}
kunstvoll kontrolliert
quelle
2

Der Kern des Problems besteht darin, dass Sie einen Zeiger auf ein veränderliches Objekt zurückgeben. Hoppla. Entweder machen Sie das Objekt unveränderlich (die nicht modifizierbare Listenlösung) oder Sie geben eine Kopie des Objekts zurück.

Im Allgemeinen schützt die Endgültigkeit von Objekten Objekte nicht vor Änderungen, wenn sie veränderlich sind. Diese beiden Probleme sind "Cousins ​​küssen".

ncmathsadist
quelle
Nun, Java hat Referenzen, während C Zeiger hat. Genug gesagt. Darüber hinaus kann der "endgültige" Modifikator je nach Anwendungsort mehrere Auswirkungen haben. Wenn Sie sich für ein Klassenmitglied bewerben, müssen Sie dem Mitglied nur genau einmal mit dem Operator "=" (Zuweisung) einen Wert zuweisen. Dies hat nichts mit Unveränderlichkeit zu tun. Wenn Sie die Referenz eines Mitglieds durch eine neue ersetzen (eine andere Instanz derselben Klasse), bleibt die vorherige Instanz unverändert (kann jedoch überprüft werden, wenn keine anderen Referenzen sie enthalten).
Gyorgyabraham
1

Es ist eine gute Idee, eine nicht veränderbare Liste zurückzugeben. Eine Liste, die während des Aufrufs der Getter-Methode nicht geändert werden kann, kann jedoch weiterhin von der Klasse oder von Klassen, die von der Klasse abgeleitet sind, geändert werden.

Stattdessen sollten Sie jedem, der die Klasse erweitert, klar machen, dass die Liste nicht geändert werden sollte.

In Ihrem Beispiel könnte dies zu folgendem Code führen:

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Test {
    public static final List<String> STRINGS =
        Collections.unmodifiableList(
            Arrays.asList("1", "2"));

    public final List<String> getStrings() {
        return STRINGS;
    }
}

Im obigen Beispiel habe ich das STRINGSFeld öffentlich gemacht. Im Prinzip können Sie den Methodenaufruf abschaffen, da die Werte bereits bekannt sind.

Sie können die Zeichenfolgen auch einem private final List<String>Feld zuweisen, das während der Erstellung der Klasseninstanz nicht geändert werden kann. Die Verwendung einer Konstante oder von Instanziierungsargumenten (des Konstruktors) hängt vom Design der Klasse ab.

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Test {
    private final List<String> strings;

    public Test(final String ... strings) {
        this.strings = Collections.unmodifiableList(Arrays
                .asList(strings));
    }

    public final List<String> getStrings() {
        return strings;
    }
}
Maarten Bodewes
quelle
0

Ja, Sie sollten eine Kopie des Arrays zurückgeben:

 public String[] getArr()
 {
    return Arrays.copyOf(arr);
 }
kofemann
quelle