¿Cómo evito la modificación de un campo privado en una clase?

165

Imagina que tengo esta clase:

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

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

Ahora, tengo otra clase que usa la clase anterior:

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

Este es el problema: ¡he accedido a un campo privado de una clase desde afuera! ¿Cómo puedo prevenir esto? Quiero decir, ¿cómo puedo hacer que esta matriz sea inmutable? ¿Significa esto que con cada método getter puedes avanzar para acceder al campo privado? (No quiero ninguna biblioteca como Guava. Solo necesito saber la forma correcta de hacerlo).

Hossein
fuente
10
En realidad, lo que finalhace evitar que la modificación del campo . Sin embargo, evitar la modificación de lo Objectmencionado por un campo es más complicado.
OldCurmudgeon
22
Hay un problema con su modelo mental si cree que poder modificar una matriz a la que tiene una referencia almacenada en un campo privado es lo mismo que poder modificar un campo privado.
Joren
45
Si es privado, ¿por qué exponerlo en primer lugar?
Erik Reppen
77
Me tomó un tiempo darme cuenta de esto, pero siempre mejora mi código cuando me aseguro de que todas las estructuras de datos estén completamente encapsuladas dentro de su objeto, lo que significa que no hay forma de "Obtener" su "arr", en lugar de hacerlo siempre que sea necesario la clase o proporcionar un iterador.
Bill K
8
¿Alguien más también se sorprende por qué esta pregunta recibió tanta atención?
Romano

Respuestas:

163

Debes devolver un copia de su matriz.

public String[] getArr() {
  return arr == null ? null : Arrays.copyOf(arr, arr.length);
}
OldCurmudgeon
fuente
121
Me gustaría recordarles a las personas que, aunque se ha votado como la respuesta correcta a la pregunta, la mejor solución al problema es, en realidad, como dice sp00m : devolver un Unmodifiable List.
OldCurmudgeon
48
No si realmente necesita devolver una matriz.
Svish
8
En realidad, la mejor solución al problema discutido es a menudo como dice @MikhailVladimirov: - para proporcionar o devolver una vista de la matriz o colección.
Christoffer Hammarström
20
pero asegúrese de que sea una copia profunda, no una copia superficial, de los datos. Para Strings no hace ninguna diferencia, para otras clases como Date, donde el contenido de la instancia se puede modificar, puede marcar la diferencia.
Jwenting
16
@Svish Debería haber sido más drástico: si devuelve una matriz de una función API, lo está haciendo mal. En funciones privadas dentro de una biblioteca, puede ser correcto. En una API (donde la protección contra la modificabilidad juega un papel), nunca lo es.
Konrad Rudolph
377

Si puede usar una Lista en lugar de una matriz, Colecciones proporciona una lista no modificable :

public List<String> getList() {
    return Collections.unmodifiableList(list);
}
sp00m
fuente
Se agregó una respuesta que se usa Collections.unmodifiableList(list)como asignación al campo, para asegurarse de que la clase no cambie la lista.
Maarten Bodewes
Me pregunto: si descompila este método, obtendrá muchas palabras clave nuevas. ¿Esto no perjudica el rendimiento cuando getList () obtiene mucho acceso? Por ejemplo en el código de renderizado.
Thomas15v
2
Tenga en cuenta que esto funciona si los elementos de las matrices son inmutables en sí mismos como Stringes, pero si son mutables, es posible que deba hacerse algo para evitar que la persona que llama los cambie.
Lii
45

El modificador privateprotege solo el campo mismo del acceso desde otras clases, pero no las referencias a objetos de este campo. Si necesita proteger el objeto referenciado, simplemente no lo entregue. Cambio

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

a:

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

o para

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

public String getArrElementAt (int index)
{
    return arr [index];
}
Mikhail Vladimirov
fuente
55
Este artículo no argumenta en contra del uso de clonar en matrices, solo en objetos que se pueden subclasificar.
Lyn Headley
28

El Collections.unmodifiableListya ha sido mencionado, ¡ Arrays.asList()curiosamente no! Mi solución también sería usar la lista desde el exterior y envolver la matriz de la siguiente manera:

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

El problema con la copia de la matriz es: si lo hace cada vez que accede al código y la matriz es grande, seguramente creará mucho trabajo para el recolector de basura. Por lo tanto, la copia es un enfoque simple pero realmente malo: yo diría "barato", ¡pero costoso para la memoria! Especialmente cuando tienes más de 2 elementos.

Si nos fijamos en el código fuente de Arrays.asListy en Collections.unmodifiableListrealidad no hay mucho creado. El primero simplemente envuelve la matriz sin copiarla, el segundo simplemente envuelve la lista, haciendo que los cambios no estén disponibles.

michael_s
fuente
Aquí se supone que la matriz que incluye las cadenas se copia cada vez. No lo es, solo se copian las referencias a las cadenas inmutables. Mientras su matriz no sea enorme, la sobrecarga es insignificante. Si la matriz es enorme, ¡busca listas y immutableListtodo el camino!
Maarten Bodewes
No, no hice esa suposición, ¿dónde? Se trata de las referencias que se copian, no importa si es una Cadena o cualquier otro objeto. Solo digo que para matrices grandes no recomendaría copiar la matriz y que las operaciones Arrays.asListy Collections.unmodifiableListson probablemente más baratas para matrices más grandes. Tal vez alguien pueda calcular cuántos elementos son necesarios, pero ya he visto código en bucles for con matrices que contienen miles de elementos que se copian solo para evitar modificaciones, ¡eso es una locura!
michael_s
6

También puede usar ImmutableListcuál debería ser mejor que el estándar unmodifiableList. La clase es parte de las bibliotecas de guayaba que fue creada por Google.

Aquí está la descripción:

A diferencia de Collections.unmodifiableList (java.util.List), que es una vista de una colección separada que aún puede cambiar, una instancia de ImmutableList contiene sus propios datos privados y nunca cambiará

Aquí hay un ejemplo simple de cómo usarlo:

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

  public ImmutableList<String> getArr() 
  {
    return ImmutableList.copyOf(arr);
  }
}
iTech
fuente
Use esta solución si no puede confiar en la Testclase para dejar la lista devuelta sin alterar . Debe asegurarse de que ImmutableListse devuelva y de que los elementos de la lista también sean inmutables. De lo contrario, mi solución también debería ser segura.
Maarten Bodewes
3

en este punto de vista, debe usar la copia de la matriz del sistema:

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
fuente
-1 ya que no hace nada más que llamar clone, y no es tan conciso.
Maarten Bodewes
2

Puede devolver una copia de los datos. La persona que llama que elige cambiar los datos solo cambiará la copia

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;
    }
}
artfullyContrived
fuente
2

El meollo del problema es que está devolviendo un puntero a un objeto mutable. Ups O representa el objeto inmutable (la solución de lista no modificable) o devuelve una copia del objeto.

Como cuestión general, la finalidad de los objetos no protege a los objetos de ser cambiados si son mutables. Estos dos problemas son "besar primos".

ncmathsadist
fuente
Bueno, Java tiene referencias, mientras que C tiene punteros. Basta de charla. Además, el modificador "final" puede tener múltiples impactos dependiendo de dónde se aplique. Aplicar en un miembro de la clase le obligará a asignar solo valor al miembro exactamente una vez con el operador "=" (asignación). Esto no tiene nada que ver con la inmutabilidad. Si reemplaza la referencia de un miembro a uno nuevo (alguna otra instancia de la misma clase), la instancia anterior permanecerá sin cambios (pero puede cambiarse por GC si no hay otras referencias que las contengan).
gyorgyabraham
1

Devolver una lista no modificable es una buena idea. Pero una lista que no se puede modificar durante la llamada al método getter puede ser cambiada por la clase o las clases derivadas de la clase.

En su lugar, debe dejar en claro a cualquiera que extienda la clase que la lista no debe modificarse.

Entonces, en su ejemplo, podría conducir al siguiente código:

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

En el ejemplo anterior, hice STRINGSpúblico el campo, en principio, podría eliminar la llamada al método, ya que los valores ya se conocen.

También puede asignar las cadenas a un private final List<String>campo que no se puede modificar durante la construcción de la instancia de clase. El uso de argumentos constantes o de instanciación (del constructor) depende del diseño de la clase.

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
fuente
0

Sí, debe devolver una copia de la matriz:

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