Как предотвратить изменение частного поля в классе?

165

Представь, что у меня есть этот класс:

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

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

Теперь у меня есть другой класс, который использует вышеупомянутый класс:

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

Вот в чем проблема: я получил доступ к закрытому полю класса извне! Как я могу предотвратить это? Я имею в виду, как я могу сделать этот массив неизменным? Означает ли это, что с каждым методом получения вы можете получить доступ к приватному полю? (Я не хочу никаких библиотек, таких как гуава. Мне просто нужно знать правильный способ сделать это).

Хоссейн
источник
10
На самом деле, что делает его finalдействительно предотвратить модификацию поля . Однако предотвращение модификации Objectупомянутого поля является более сложным.
OldCurmudgeon
22
Существует проблема с вашей ментальной моделью, если вы думаете, что возможность изменить массив, на который у вас есть ссылка, сохраненная в приватном поле, такая же, как и возможность изменить приватное поле.
Joren
45
Если это личное, зачем выставлять его на первое место?
Эрик Реппен
7
Мне потребовалось некоторое время, чтобы понять это, но это всегда улучшает мой код, когда я гарантирую, что все структуры данных полностью инкапсулированы в их объекте - это означает, что нет никакого способа «получить» ваш «arr», вместо этого делайте, куда вы должны внутри класс или предоставить итератор.
Билл К
8
Кто-нибудь еще удивляется, почему этому вопросу уделяется так много внимания?
Роман

Ответы:

163

Вы должны вернуть копию вашего массива.

public String[] getArr() {
  return arr == null ? null : Arrays.copyOf(arr, arr.length);
}
OldCurmudgeon
источник
121
Я хотел бы напомнить людям, что, хотя за этот вопрос был выбран правильный ответ, лучшее решение проблемы, как говорит sp00m, - вернуть ответ Unmodifiable List.
OldCurmudgeon
48
Нет, если вам действительно нужно вернуть массив.
Свиш
8
На самом деле, лучшее решение обсуждаемой проблемы часто заключается в том, что @MikhailVladimirov говорит: - предоставить или вернуть представление массива или коллекции.
Кристофер Хаммарстрём,
20
но убедитесь, что это глубокая, а не мелкая копия данных. Для строк это не имеет значения, для других классов, таких как Date, где содержимое экземпляра может быть изменено, это может иметь значение.
jwenting
16
@Svish Я должен был быть более радикальным: если вы возвращаете массив из функции API, вы делаете это неправильно. В частных функциях внутри библиотеки это может быть правильно. В API (где защита от модифицируемости играет роль) этого никогда не происходит.
Конрад Рудольф
377

Если вы можете использовать List вместо массива, Collections предоставит неизменяемый список :

public List<String> getList() {
    return Collections.unmodifiableList(list);
}
sp00m
источник
Добавлен ответ, который используется в Collections.unmodifiableList(list)качестве назначения для поля, чтобы убедиться, что список не изменен самим классом.
Maarten Bodewes
Просто интересно, если вы декомпилируете этот метод, вы получите много новых ключевых слов. Не влияет ли это на производительность, когда getList () получает много доступа. Например, в коде рендеринга.
Thomas15v
2
Обратите внимание, что это работает, если элементы массивов являются неизменяемыми сами по себе, как Stringесть, но если они являются изменяемыми, возможно, необходимо что-то сделать, чтобы предотвратить изменение их вызывающей стороной.
Ли
45

Модификатор privateзащищает только само поле от доступа к другим классам, но не ссылки на объекты этим полем. Если вам нужно защитить указанный объект, просто не выдавайте его. + Изменить

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

чтобы:

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

или

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

public String getArrElementAt (int index)
{
    return arr [index];
}
Михаил Владимиров
источник
5
Эта статья не оспаривает использование клона на массивах, только на объектах, которые могут быть разделены на подклассы.
Лин Хедли
28

Это Collections.unmodifiableListуже упоминалось - как Arrays.asList()ни странно! Мое решение также будет использовать список извне и обернуть массив следующим образом:

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

Проблема с копированием массива: если вы делаете это каждый раз, когда получаете доступ к коду, а массив большой, вы наверняка создадите много работы для сборщика мусора. Таким образом, копия - простой, но очень плохой подход - я бы сказал, «дешево», но дорого! Особенно, когда у вас больше, чем просто 2 элемента.

Если вы посмотрите на исходный код Arrays.asListи Collections.unmodifiableListтам на самом деле не так много создано. Первый просто оборачивает массив, не копируя его, второй просто оборачивает список, делая изменения в нем недоступными.

michael_s
источник
Здесь вы делаете предположение, что массив, включающий строки , копируется каждый раз. Это не так, копируются только ссылки на неизменяемые строки. Пока ваш массив не огромен, накладные расходы незначительны. Если массив огромен, переходите на списки и immutableListвсе!
Maarten Bodewes
Нет, я не делал этого предположения - где? Речь идет о скопированных ссылках - не имеет значения, является ли это строкой или любым другим объектом. Просто сказать , что для больших массивов я не рекомендовал бы скопировать массив и что Arrays.asListи Collections.unmodifiableListоперации, вероятно , дешевле для больших массивов. Может быть, кто-то может подсчитать, сколько элементов необходимо, но я уже видел код в циклах for с массивами, содержащими тысячи элементов, копируемых просто для предотвращения модификации - это безумие!
michael_s
6

Вы также можете использовать, ImmutableListкоторый должен быть лучше, чем стандарт unmodifiableList. Этот класс является частью библиотек Guava , созданных Google.

Вот описание:

В отличие от Collections.unmodifiableList (java.util.List), который является представлением отдельной коллекции, которая все еще может изменяться, экземпляр ImmutableList содержит свои собственные личные данные и никогда не изменится

Вот простой пример того, как его использовать:

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

  public ImmutableList<String> getArr() 
  {
    return ImmutableList.copyOf(arr);
  }
}
ITech
источник
Используйте это решение, если вы не можете доверять Testклассу, чтобы оставить возвращенный список без изменений . Вы должны убедиться, что ImmutableListэто возвращается, и что элементы списка также являются неизменяемыми. В противном случае мое решение также должно быть безопасным.
Maarten Bodewes
3

с этой точки зрения вы должны использовать копию системного массива:

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 Рамеш
источник
-1, поскольку он ничего не делает над вызовом cloneи не так краток.
Мартен Бодьюз
2

Вы можете вернуть копию данных. Звонящий, который решит изменить данные, будет только менять копию

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
источник
2

Суть проблемы в том, что вы возвращаете указатель на изменяемый объект. К сожалению. Либо вы делаете объект неизменным (решение с неизменяемым списком), либо вы возвращаете копию объекта.

В целом, окончательность объектов не защищает объекты от изменения, если они изменчивы. Этими двумя проблемами являются «поцелуи двоюродных братьев».

ncmathsadist
источник
Ну, у Java есть ссылки, а у C есть указатели. Достаточно сказано. Кроме того, модификатор «final» может иметь несколько воздействий в зависимости от того, где он применяется. Применение к члену класса заставит вас назначать значение этому члену только один раз с помощью оператора «=» (назначение). Это не имеет ничего общего с неизменностью. Если вы замените ссылку на элемент новым (какой-то другой экземпляр из того же класса), предыдущий экземпляр останется неизменным (но может быть GCed, если никакие другие ссылки не содержат их).
gyorgyabraham
1

Возвращение неизменяемого списка - хорошая идея. Но список, который становится неизменяемым во время вызова метода получателя, все еще может быть изменен классом или классами, производными от класса.

Вместо этого вы должны дать понять любому, кто расширяет класс, что список не следует изменять.

Так что в вашем примере это может привести к следующему коду:

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

В приведенном выше примере я сделал STRINGSполе общедоступным, в принципе вы можете покончить с вызовом метода, так как значения уже известны.

Вы также можете назначить строки private final List<String>полю, которое нельзя изменить во время создания экземпляра класса. Использование константы или аргументов экземпляра (конструктора) зависит от дизайна класса.

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;
    }
}
Мартен Бодевес
источник
0

Да, вы должны вернуть копию массива:

 public String[] getArr()
 {
    return Arrays.copyOf(arr);
 }
kofemann
источник