wtorek, 15 stycznia 2013

Modyfikowanie niemodyfikowalnych kolekcji w Javie

Jakiś czas temu w pracy natrafiłem na coś, czego na pierwszy rzut oka nie zrozumiałem czemu nie działa. Mianowicie była tworzona niemodyfikowalna lista, zawierająca pewne elementy. Jednak po chwili lista stawała się pusta. Samo usuwanie z listy działa poprawnie (tzn. rzuca wyjątkiem), Kod poniżej:
public class ModifyUnmodifiableListTest {

    final List<String> exampleList = new ArrayList<String>();

    @Before
    public void setUp() {
        exampleList.add("text 1");
        exampleList.add("text 2");
        exampleList.add("text 3");
    }

    @Test(expected = UnsupportedOperationException.class)
    public void shouldThrowExceptionWhenModifyUnmodifiableList() {
        List<String> unmodifiableList = Collections
                .unmodifiableList(exampleList);
        unmodifiableList.clear();
    }

Operacja clear() wyrzuca UnsupportedOperationException zgodnie z oczekiwaniem. Poniżej kod który mnie zadziwił:

    @Test
    public void shouldModifyUnmodifiableList() {
        // given
        List<String> unmodifiableList = Collections
                .unmodifiableList(exampleList);

        // when
        exampleList.clear();

        // then
        assertEquals(0, unmodifiableList.size());
    }

Test przechodzi, czyli na nowoutworzonej liście nie ma elementów!

Co się stało? Otóż metoda jak można przeczytać w dokumentacji:
Returns an unmodifiable view of the specified list
Zwraca nam widok (projekcję) na oryginalną listę. Czyli źródłową kolekcje można dalej modyfikować!

Jak można więc stworzyć niezależną kopię kolekcji? A no znajdzie się kilka sposobów. Jednym z prostszych jest skonwertowanie kolekcji do tablicy:
    @Test
    public void shouldCloneUnmodifiableListToArray() {
        // given
        String[] tab = exampleList.toArray(
                new String[exampleList.size()]);

        // when
        exampleList.clear();

        // then
        assertEquals(3, tab.length);
        assertEquals("text 1", tab[0]);
    }

Jak komuś koniecznie potrzebna lista lub inna kolekcja, to można z powrotem z tablicy zrobić kolekcje.

Inną możliwością jest metoda na piechotę, czyli ręczne dodanie elementów kolekcji do nowej kolekcji.
    @Test
    public void shouldCloneUnmodifiableListInForLoop() {
        // given
        List<String> list = new ArrayList<String>();
        for (String s : exampleList) {
            list.add(s);
        }

        // when
        exampleList.clear();

        // then
        assertEquals(3, list.size());
        assertEquals("text 1", list.get(0));
    }

Można jeszcze próbować walczyć z Collections.copy(...) ale tutaj lista docelowa musi mieć co najmniej tyle samo elementów, co lista źródłowa.

Rozwiązanie jakie mi jeszcze wpadło do głowy to wykorzystanie Apache Commons a dokładniej SerializationUtils.clone(...).

Jest to już bardziej uniwersalna metoda robienia głębokiej kopii, która działa na wszystkim co jest serializowane.

A jakie jest rozwiązanie najprostsze? Po prostu skorzystać z konstruktora docelowej kolekcji:
    @Test
    public void shouldCreateNewListBasedOnList() {
        // given
        List<String> list = new ArrayList<String>(exampleList);

        // when
        exampleList.clear();

        // then
        assertEquals(3, list.size());
        assertEquals("text 1", list.get(0));
    }

To tyle. Cały kod to pobawienia się dostępny jest na githubie: ModifyUnmodifiableList.