Неизменен обект с променлива член на ArrayList - защо тази променлива може да бъде променена?

Имам един клас с различни членски променливи. Има конструктор и има методи за получаване, но няма методи за настройка. Всъщност този обект трябва да бъде неизменен.

public class Example {
   private ArrayList<String> list; 
}

Сега забелязах следното: когато получа списъка с променливи с getter-method, мога да добавя нови стойности и така нататък - мога да променя ArrayList. Когато извикам следващия път get() за тази променлива, промененият ArrayList се връща. Как е възможно това? Не съм го задавал отново, просто работих върху него! С String това поведение не е възможно. И така, каква е разликата тук?


person nano7    schedule 26.05.2011    source източник


Отговори (9)


Това, че позоваването на списъка е неизменно, не означава, че списъкът, за който се отнася, е неизменен.

Дори ако list беше направено final това щеше да бъде разрешено

// changing the object which list refers to
example.getList().add("stuff");

но това не би било позволено:

// changing list
example.list = new ArrayList<String>();   // assuming list is public

За да направите списъка неизменен (предотвратете и първия ред), предлагам да използвате Collections.unmodifiableList:

public class Example {
    final private ArrayList<String> list;

    Example(ArrayList<String> listArg) {
        list = Collections.unmodifiableList(listArg);
    }
}

(Имайте предвид, че това създава непроменим изглед на списъка. Ако някой държи оригиналната препратка, тогава списъкът все още може да бъде модифициран чрез това.)


С низ това поведение не е възможно. И така, каква е разликата тук?

Това е така, защото String вече е неизменен (непроменим) точно както би бил списъкът, ако го превърнете в непроменим списък.

Сравнение:

              String data structure  | List data structure
           .-------------------------+------------------------------------.
Immutable  | String                  | Collection.unmodifiableList(...)   |
-----------+-------------------------+------------------------------------|
Mutable    | StringBuffer            | ArrayList                          |
           '-------------------------+------------------------------------'
person aioobe    schedule 26.05.2011
comment
AFAIK Collections.unmodifiableList() връща неизменен WRAPPER за даден списък. Ако съм прав, горното няма да гарантира неизменност. Един клас може да създаде екземпляр на списък, да инстанцира Пример и все още може да модифицира списъка, който Примерът държи, като модифицира оригиналния списък, предаден на конструктора. Въпреки че отговорът може да е достатъчен за справяне с разликите, той може да не отговаря на строгите изисквания за неизменност. - person Maurice; 19.06.2015
comment
Това е правилното. Отговорът е актуализиран. Можете да направите Collections.unmodifiableList(new ArrayList<>(listArg)), за да сте сигурни, че никой няма препратка към основния променлив списък и по този начин да избегнете променливостта. - person aioobe; 19.06.2015

Връщате препратка към list. И list не е неизменно.


Ако не искате вашият списък да бъде променен, върнете копие от него:

public List<String> get() {
    return new ArrayList<String>(this.list);
}

Или можете да върнете непроменим списък:

public List<String> get() {
    return Collections.unmodifiableList(this.list);
}
person dacwe    schedule 26.05.2011

Ключът е да разберете, че не променяте низа - вие променяте кой низ препраща към списъка.

Казано по друг начин: ако извадя долар от портфейла ви и го заменя с стотинка, не съм променил нито долара, нито стотинката - просто съм променил съдържанието на портфейла ви.

Ако искате изглед само за четене в списъка, погледнете Collections.unmodifiableList. Това няма да попречи на списъка, който опакова, от промяна на курса, но ще попречи на всеки, който има само препратка към непроменимия списък, да променя съдържанието.

За наистина неизменен списък вижте Guava "http://google.github.io/guava/releases/snapshot/api/docs/com/google/common/collect/ImmutableList.html" rel="nofollow noreferrer">ImmutableList клас.

person Jon Skeet    schedule 26.05.2011

Както казват другите отговори, обектът, който връщате от получателите, все още е променлив.

Можете да превърнете списъка в неизменен, като го украсите с помощта на класа Collections:

 list = Collections.unmodifiableList(list);

Ако върнете това на клиентите, те няма да могат да добавят или премахват елементи към него. Въпреки това, те все още могат да извадят елементи от списъка - така че трябва да сте сигурни, че те също са неизменни, ако това е, което търсите!

person planetjones    schedule 26.05.2011

Collections.unmodifiableList() прави списъка неизползваем. Което отново създава нов окончателен списък с масиви и отменя метода за добавяне, премахване, добавяне и изчистване, за да хвърли изключение на unsupportedoperation. Това е хак на клас Collections. Но по време на компилиране това не ви спира да добавяте и премахвате неща. Предпочитам да клонирам този списък. Което може да ми помогне да запазя съществуващия си обект неизменен и не ми струва създаването на нов списък. Прочетете разликата между клониране и нов оператор (http://www.javatpoint.com/object-cloning ). Също така ще помогне при срив на моя код по време на изпълнение.

person Pratik Adhau    schedule 12.01.2015

Следователно не трябва да предоставяте метод за получаване на списъка, ако искате да го защитите от промяна.

Обектите му все още остават непокътнати, тъй като не сте имали сетери. Но това, което правите, е да премахнете/добавите нови/различни обекти към него и това е добре.

person Boro    schedule 26.05.2011

Препратката към списъка е неизменна, но не и списъкът. Ако искате самият списък да бъде неизменен, обмислете използването на ImmutableList

person simon    schedule 26.05.2011

За да получите наистина неизменен списък, ще трябва да направите дълбоки копия на съдържанието на списъка. UnmodifiableList само ще направи списъка с препратки донякъде неизменен. Сега създаването на дълбоко копие на списъка или масива ще бъде трудно за паметта с нарастващия размер. Можете да използвате сериализация/десериализация и да съхраните дълбокото копие на масив/списък във временен файл. Инструментът за настройка няма да бъде наличен, тъй като членската променлива трябва да бъде неизменна. Getter ще сериализира членската променлива във файл и след това ще я десиализира, за да получи дълбоко копие. Сериализацията има вроден характер да навлиза в дълбините на дървото на обектите. Това обаче би осигурило пълна неизменност при известна цена на производителността.

package com.home.immutable.serial;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public final class ImmutableBySerial {

    private final int num;
    private final String str;
    private final TestObjSerial[] arr;

    ImmutableBySerial(int num, String str, TestObjSerial[] arr){
        this.num = num;
        this.str = str;
        this.arr = getDeepCloned(arr);
    }

    public int getNum(){
        return num;
    }

    public String getStr(){
        return str;
    }

    public TestObjSerial[] getArr(){
        return getDeepCloned(arr);
    }

    private TestObjSerial[] getDeepCloned(TestObjSerial[] arr){
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        TestObjSerial[] clonedObj = null;
        try {
             fos = new FileOutputStream(new File("temp"));
             oos = new ObjectOutputStream(fos);
             oos.writeObject(arr);
             fis = new FileInputStream(new File("temp"));
             ois = new ObjectInputStream(fis);
             clonedObj = (TestObjSerial[])ois.readObject();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                oos.close();
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return clonedObj;
    }
}
person Rajeev Ranjan    schedule 27.05.2017

Друго решение е да върнете копие на списъка с масиви, а не действителния списък с масиви като този код

import java.util.ArrayList;
import java.util.List;

public final class ImmutableExample {

    private final List<String> strings = new ArrayList<String>();

    public ImmutableExample() {
        strings.add("strin 1");
        strings.add("string 2");
    }

    public List<String> getStrings() {
        List<String> newStrings = new ArrayList<String>();
        strings.forEach(s -> newStrings.add(s));
        return newStrings;
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ImmutableExample im = new ImmutableExample();
        System.out.println(im.getStrings());
        im.getStrings().add("string 3");
        System.out.println(im.getStrings());

    }
}
person Hamada Ibrahim    schedule 14.11.2017
comment
Това не добавя нова информация. Връщането на копие е обяснено в този отговор: stackoverflow.com/a/6137267 и дори използва много по-чиста версия на копиране на списъка. - person Tom; 14.11.2017