Comment empêcher la modification d'un champ privé dans une classe?

165

Imaginez que j'ai cette classe:

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

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

Maintenant, j'ai une autre classe qui utilise la classe ci-dessus:

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

Voilà donc le problème: j'ai accédé à un champ privé d'une classe de l'extérieur! Comment éviter cela? Je veux dire, comment puis-je rendre ce tableau immuable? Cela signifie-t-il qu'avec chaque méthode getter, vous pouvez progresser pour accéder au champ privé? (Je ne veux pas de bibliothèques telles que Guava. J'ai juste besoin de connaître la bonne façon de faire cela).

Hossein
la source
10
En fait, ce qui en fait n'empêcher la modification du champ . Cependant, empêcher la modification du référencé par un champ est plus compliqué. finalObject
OldCurmudgeon
22
Il y a un problème avec votre modèle mental si vous pensez que pouvoir modifier un tableau auquel vous avez une référence stockée dans un champ privé équivaut à pouvoir modifier un champ privé.
Joren
45
Si c'est privé, pourquoi l'exposer en premier lieu?
Erik Reppen le
7
Cela m'a pris un certain temps pour comprendre cela, mais cela améliore toujours mon code lorsque je m'assure que toutes les structures de données sont complètement encapsulées dans leur objet - ce qui signifie qu'il n'y a aucun moyen de "Obtenir" votre "arr", faites plutôt ce que vous devez à l'intérieur la classe ou fournir un itérateur.
Bill K
8
Quelqu'un d'autre est-il également surpris de savoir pourquoi cette question a retenu autant d'attention?
Roman

Réponses:

163

Vous devez renvoyer une copie de votre tableau.

public String[] getArr() {
  return arr == null ? null : Arrays.copyOf(arr, arr.length);
}
VieuxCurmudgeon
la source
121
Je voudrais rappeler aux gens que bien que cela ait été voté comme la bonne réponse à la question, la meilleure solution au problème est en fait comme le dit sp00m - renvoyer un Unmodifiable List.
OldCurmudgeon
48
Pas si vous avez réellement besoin de renvoyer un tableau.
Svish
8
En fait, la meilleure solution au problème discuté est souvent comme le dit @MikhailVladimirov: - pour fournir ou renvoyer une vue du tableau ou de la collection.
Christoffer Hammarström
20
mais assurez-vous qu'il s'agit d'une copie complète, et non d'une copie superficielle, des données. Pour les chaînes, cela ne fait aucune différence, pour d'autres classes comme Date où le contenu de l'instance peut être modifié, cela peut faire une différence.
jwenting le
16
@Svish, j'aurais dû être plus drastique: si vous renvoyez un tableau à partir d'une fonction API, vous le faites mal. Dans les fonctions privées à l'intérieur d'une bibliothèque, cela peut être correct. Dans une API (où la protection contre la modifiabilité joue un rôle), ce n'est jamais le cas .
Konrad Rudolph
377

Si vous pouvez utiliser une liste au lieu d'un tableau, Collections fournit une liste non modifiable :

public List<String> getList() {
    return Collections.unmodifiableList(list);
}
sp00m
la source
Ajout d'une réponse qui utilise Collections.unmodifiableList(list)comme affectation au champ, pour s'assurer que la liste n'est pas modifiée par la classe elle-même.
Maarten Bodewes
Je me demande simplement, si vous décompilez cette méthode, vous obtenez beaucoup de nouveaux mots clés. Cela ne nuit-il pas aux performances lorsque getList () y accède beaucoup. Par exemple dans le code de rendu.
Thomas15v
2
Notez que cela fonctionne si les éléments des tableaux sont immuables en eux-mêmes comme l' Stringest, mais s'ils sont modifiables, il faudra peut-être faire quelque chose pour empêcher l'appelant de les modifier.
Lii
45

Le modificateur privateprotège uniquement le champ lui-même contre l'accès à partir d'autres classes, mais pas les références d'objets par ce champ. Si vous avez besoin de protéger un objet référencé, ne le donnez pas. Changement

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

à:

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

ou pour

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

public String getArrElementAt (int index)
{
    return arr [index];
}
Mikhail Vladimirov
la source
5
Cet article ne s'oppose pas à l'utilisation du clone sur les tableaux, uniquement sur les objets qui peuvent être sous-classés.
Lyn Headley
28

Le Collections.unmodifiableLista déjà été mentionné - Arrays.asList()étrangement non! Ma solution serait également d'utiliser la liste de l'extérieur et d'encapsuler le tableau comme suit:

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

Le problème avec la copie du tableau est le suivant: si vous le faites à chaque fois que vous accédez au code et que le tableau est gros, vous créerez certainement beaucoup de travail pour le garbage collector. La copie est donc une approche simple mais vraiment mauvaise - je dirais "bon marché", mais chère en mémoire! Surtout lorsque vous avez plus de 2 éléments.

Si vous regardez le code source de Arrays.asListet Collections.unmodifiableListil n'y a en fait pas grand chose de créé. Le premier encapsule simplement le tableau sans le copier, le second encapsule simplement la liste, rendant les modifications indisponibles.

michael_s
la source
Vous faites l'hypothèse ici que le tableau comprenant les chaînes est copié à chaque fois. Ce n'est pas le cas, seules les références aux chaînes immuables sont copiées. Tant que votre baie n'est pas énorme, la surcharge est négligeable. Si le tableau est énorme, optez pour des listes et immutableListjusqu'au bout!
Maarten Bodewes
Non, je n'ai pas fait cette supposition - où? Il s'agit des références copiées - peu importe qu'il s'agisse d'une chaîne ou de tout autre objet. En disant simplement que pour les grands tableaux, je ne recommanderais pas de copier le tableau et que les opérations Arrays.asListet Collections.unmodifiableListsont probablement moins chères pour les tableaux plus grands. Peut-être que quelqu'un peut calculer le nombre d'éléments nécessaires, mais j'ai déjà vu du code dans des boucles for avec des tableaux contenant des milliers d'éléments copiés juste pour empêcher la modification - c'est insensé!
michael_s
6

Vous pouvez également utiliser ImmutableListce qui devrait être meilleur que la norme unmodifiableList. La classe fait partie des bibliothèques Guava créées par Google.

Voici la description:

Contrairement à Collections.unmodifiableList (java.util.List), qui est une vue d'une collection distincte qui peut encore changer, une instance d'ImmutableList contient ses propres données privées et ne changera jamais

Voici un exemple simple de son utilisation:

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

  public ImmutableList<String> getArr() 
  {
    return ImmutableList.copyOf(arr);
  }
}
iTech
la source
Utilisez cette solution si vous ne pouvez pas faire confiance à la Testclasse pour laisser la liste renvoyée inchangée . Vous devez vous assurer que cela ImmutableListest renvoyé et que les éléments de la liste sont également immuables. Sinon, ma solution devrait également être sûre.
Maarten Bodewes
3

à ce point de vue, vous devez utiliser la copie de tableau système:

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
la source
-1 car il ne fait rien sur les appels cloneet n'est pas aussi concis.
Maarten Bodewes
2

Vous pouvez renvoyer une copie des données. L'appelant qui choisit de modifier les données ne changera que la copie

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;
    }
}
astucieusement
la source
2

Le nœud du problème est que vous renvoyez un pointeur vers un objet mutable. Oups. Soit vous rendez l'objet immuable (la solution de liste non modifiable), soit vous renvoyez une copie de l'objet.

En règle générale, la finalité des objets ne protège pas les objets d'être modifiés s'ils sont mutables. Ces deux problèmes sont «embrasser des cousins».

ncmathsadist
la source
Eh bien, Java a des références, tandis que C a des pointeurs. Assez dit. De plus, le modificateur «final» peut avoir des impacts multiples selon l'endroit où il est appliqué. L'application sur un membre de classe vous obligera à attribuer une valeur au membre exactement une seule fois avec l'opérateur "=" (affectation). Cela n'a rien à voir avec l'immuabilité. Si vous remplacez la référence d'un membre par une nouvelle (une autre instance de la même classe), l'instance précédente restera inchangée (mais peut être GC si aucune autre référence ne les contient).
gyorgyabraham
1

Renvoyer une liste non modifiable est une bonne idée. Mais une liste rendue non modifiable lors de l'appel à la méthode getter peut toujours être modifiée par la classe ou par les classes dérivées de la classe.

À la place, vous devez indiquer clairement à quiconque étend la classe que la liste ne doit pas être modifiée.

Donc, dans votre exemple, cela pourrait conduire au code suivant:

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;
    }
}

Dans l'exemple ci-dessus, j'ai rendu le STRINGSchamp public, en principe vous pouvez supprimer l'appel de méthode, car les valeurs sont déjà connues.

Vous pouvez également affecter les chaînes à un private final List<String>champ rendu non modifiable lors de la construction de l'instance de classe. L'utilisation d'une constante ou d'arguments d'instanciation (du constructeur) dépend de la conception de la classe.

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
la source
0

Oui, vous devez renvoyer une copie du tableau:

 public String[] getArr()
 {
    return Arrays.copyOf(arr);
 }
Kofemann
la source