Як запобігти зміні приватного поля в класі?


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!"; //!!!

Отже, це проблема: я отримав доступ до приватного поля класу ззовні! Як я можу запобігти цьому? Я маю на увазі, як я можу зробити цей масив незмінним? Чи означає це, що з кожним методом getter ви можете пропрацювати свій шлях до приватного поля? (Я не хочу, щоб були такі бібліотеки, як Guava. Мені просто потрібно знати правильний спосіб зробити це).


10
Насправді, це finalробить перешкоджає модифікації поля . Однак запобігання модифікації Objectпосилається на поле є складнішим.
OldCurmudgeon

22
Існує проблема з вашою ментальною моделлю, якщо ви думаєте, що можливість змінити масив, на який у вас є посилання, що зберігається в приватному полі, це те саме, що і можливість змінювати приватне поле.
Жорен

45
Якщо це приватне, навіщо виставляти його в першу чергу?
Ерік Реппен

7
Мені знадобилося певний час, щоб це зрозуміти, але це завжди вдосконалює мій код, коли я гарантую, що всі структури даних повністю інкапсульовані в межах їх об'єкта - це означає, що немає способу "отримати" свою "arr", а не робити те, куди потрібно знаходитись всередині клас або надайте ітератор.
Білл К

8
Хтось ще дивується, чому це питання привернуло стільки уваги?
Роман

Відповіді:


163

Ви повинні повернути копію масиву.

public String[] getArr() {
  return arr == null ? null : Arrays.copyOf(arr, arr.length);
}

121
Я хотів би нагадати людям, що хоч за цю правильну відповідь на питання було проголошено, найкращим рішенням проблеми є, як каже sp00m - повернути Unmodifiable List.
OldCurmudgeon

48
Не, якщо вам дійсно потрібно повернути масив.
Свиш

8
Власне, найкращим рішенням обговорюваної проблеми часто є те, як говорить @MikhailVladimirov: - надати або повернути вигляд масиву чи колекції.
Крістофер Хаммарстрем

20
але переконайтеся, що це дані в глибокій, а не дрібній копії. Для рядків це не має значення, для інших класів, таких як Дата, де вміст екземпляра може бути змінено, він може змінити значення.
1313

16
@Svish Я повинен був бути більш різким: якщо ви повернете масив з функції API, ви робите це неправильно. У приватних функціях всередині бібліотеки це може бути правильним. В API (де захист від модифікованості відіграє певну роль) він ніколи не є.
Конрад Рудольф

377

Якщо ви можете використовувати Список замість масиву, колекції надають список, що не змінюється :

public List<String> getList() {
    return Collections.unmodifiableList(list);
}

Додано відповідь, яка використовує 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));
}

Проблема з копіюванням масиву полягає в тому, що якщо ви робите це кожен раз, коли ви отримуєте доступ до коду, і масив великий, ви створите багато роботи для сміттєзбірника точно. Тож копія - простий, але насправді поганий підхід - я б сказав "дешево", але пам'ять дорого! Особливо, коли у вас більше ніж два елементи.

Якщо ви подивитеся на вихідний код, Arrays.asListа Collections.unmodifiableListнасправді створено не так багато. Перший просто обертає масив, не копіюючи його, другий просто загортає список, роблячи зміни в ньому недоступними.


Ви робите припущення, що масив, що включає рядки , копіюється кожного разу. Це не так, копіюються лише посилання на незмінні рядки. Поки ваш масив не величезний, накладні витрати незначні. Якщо масив величезний, переходьте до списків і immutableListвсьому шляху!
Maarten Bodewes

Ні, я не зробив цього припущення - куди? Йдеться про скопійовані посилання - не має значення, чи це String або будь-який інший об'єкт. Просто сказати , що для великих масивів я не рекомендував би скопіювати масив і що Arrays.asListі Collections.unmodifiableListоперації, ймовірно , дешевше для великих масивів. Можливо, хтось може порахувати, скільки елементів потрібно, але я вже бачив код у for-loops з масивами, що містять тисячі елементів, що копіюються просто для запобігання модифікації - це божевільно!
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);
  }
}

Використовуйте це рішення, якщо ви не можете довіряти 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;
   }
}

-1 оскільки він нічого не робить через дзвінки clone, і не такий стислий.
Maarten Bodewes

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

2

Суть проблеми полягає в тому, що ви повертаєте вказівник на об'єкт, що змінюється. На жаль Або ви зробите об'єкт незмінним (рішення, яке не може змінюватися), або повернете його копію.

Як правило, остаточність об'єктів не захищає об'єкти від їх зміни, якщо вони змінюються. Ці дві проблеми - "поцілунки кузенів".


Добре, що у Java є посилання, в той час як на C є вказівники. Достатньо сказано. Крім того, "остаточний" модифікатор може мати численні впливи залежно від місця його застосування. Застосування до члена класу змусить вас призначати значення члену лише один раз разом з оператором "=" (призначення). Це не має нічого спільного з незмінністю. Якщо ви заміните посилання члена на новий (якийсь інший екземпляр того ж класу), попередній екземпляр залишиться незмінним (але може бути GCed, якщо жодні інші посилання не містять їх).
gyorgyabraham

1

Повернення немодифікованого списку - хороша ідея. Але список, який робиться немодифікованим під час виклику методу getter, все одно може бути змінений класом або класами, що походять від класу.

Натомість ви повинні дати зрозуміти будь-кому, хто розширює клас, що список не слід змінювати.

Тож у вашому прикладі це може призвести до наступного коду:

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);
 }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.