Backed Collections и Array-backed списки в Java
Получите список из массива через List<String> list = Arrays.asList(array) и вызовите list.set(0, "яблоко") — изменится и сам массив: array[0] теперь тоже "яблоко". А следующая строка list.add("груша") упадёт с java.lang.UnsupportedOperationException. Оба эффекта объясняются одним: такой список не хранит своих данных, он лишь «окно» в массив.
Backed collection (коллекция-представление, view) — это объект-коллекция, у которого нет собственного хранилища: все операции чтения и записи он перенаправляет в исходную коллекцию или массив. Поэтому изменения в источнике сразу видны через представление, а изменения через представление попадают в источник. Array-backed список — частный случай, когда источник — массив; его возвращает Arrays.asList(). Представления создаются за O(1), ничего не копируя.
Какие методы возвращают представления
В Java представление (view) возвращают Arrays.asList(), List.subList(), методы keySet(), values() и entrySet() у Map, диапазонные методы отсортированных коллекций (headSet(), subMap() и другие), descendingMap(), reversed() (Java 21) и обёртки класса Collections. Каждое представление разрешает свой набор операций.
| Метод | Представление чего | Что можно делать | Граничный случай / ошибка |
|---|---|---|---|
Arrays.asList(T... a) | Массива | get(), set(), сортировка | add(), remove(int) — UnsupportedOperationException: размер массива не меняется |
list.subList(from, to) | Части списка, индексы [from, to) | Всё, что разрешает исходный список, включая add() и clear() | add()/remove() в исходном списке — при следующем обращении к подсписку ConcurrentModificationException |
map.keySet(), values(), entrySet() | Ключей, значений, пар карты | Перебор, удаление (пара исчезает из карты), entry.setValue() | add() — UnsupportedOperationException |
headSet(), subSet(), tailSet() | Диапазона элементов TreeSet | Добавление и удаление внутри диапазона | Элемент вне диапазона — IllegalArgumentException: key out of range |
headMap(), subMap(), tailMap() | Диапазона ключей TreeMap | put() и remove() внутри диапазона | Ключ вне диапазона — IllegalArgumentException: key out of range |
descendingMap(), descendingSet(), list.reversed() (Java 21) | Той же коллекции в обратном порядке | То же, что исходная коллекция | Копии нет: изменения через представление попадают в оригинал |
Collections.unmodifiableList(list) и другие обёртки | Исходной коллекции | Только чтение | Изменения, сделанные в обход обёртки, через неё видны |
Представления Map подробно разобраны в разделе Map.Entry, keySet(), values() и entrySet(), диапазоны множеств — в уроке SortedSet и TreeSet, а обёртки unmodifiableList() и synchronizedList() — в разделе Обёртки класса Collections. Ниже — два классических примера backed-коллекций и подсписки.
Пример backed-коллекции: subMap() у TreeMap
Метод subMap(fromKey, toKey) интерфейса SortedMap возвращает представление ключей от fromKey включительно до toKey не включительно. Записи, добавленные в исходную карту внутри этого диапазона, появляются в представлении, а записи, добавленные через представление, попадают в исходную карту.
import java.util.SortedMap;
import java.util.TreeMap;
public class BackedCollections {
public static void main(String[] args) {
SortedMap<String, String> map = new TreeMap<>();
map.put("а", "арбуз");
map.put("в", "вишня");
map.put("д", "дыня");
SortedMap<String, String> subMap = map.subMap("б", "ж");
System.out.println(map + " " + subMap);
map.put("б", "брусника");
subMap.put("г", "груша");
map.put("я", "яблоко");
// subMap.put("с", "слива");
System.out.println(map + " " + subMap);
}
} Вывод:
{а=арбуз, в=вишня, д=дыня} {в=вишня, д=дыня}
{а=арбуз, б=брусника, в=вишня, г=груша, д=дыня, я=яблоко} {б=брусника, в=вишня, г=груша, д=дыня} Ключ «б» добавлен в исходную карту, но виден в subMap, потому что попадает в диапазон от «б» до «ж». «г» добавлен через subMap и оказался в map. «я» есть только в исходной карте: он за границей диапазона. Если раскомментировать строку subMap.put("с", "слива"), программа завершится исключением:
Exception in thread "main" java.lang.IllegalArgumentException: key out of range Что выведет subMap.put("ё", "ёлка")? Тоже IllegalArgumentException, хотя по алфавиту «ё» стоит перед «ж». Строки сравниваются по кодам символов: у «ж» код U+0436, а у «ё» — U+0451, то есть «ё» для TreeMap идёт после «я» (U+044F). Методы headMap(), tailMap() и версии с флагами inclusive работают по тем же правилам — см. разделы headMap, subMap и tailMap — живые представления и Диапазоны в NavigableMap.
Array-backed список: Arrays.asList()
Метод Arrays.asList(array) возвращает список фиксированного размера, который хранит ссылку на переданный массив, а не его копию. Замена элемента через set() меняет массив, запись в массив видна в списке, а методы, меняющие размер (add(), remove(), clear()), бросают UnsupportedOperationException.
import java.util.Arrays;
import java.util.List;
public class BackedArrayCollection {
public static void main(String[] args) {
String[] array = {"арбуз", "вишня", "дыня"};
List<String> list = Arrays.asList(array);
System.out.println("Список: " + list);
System.out.println("Массив: " + Arrays.toString(array));
list.set(0, "яблоко");
array[1] = "брусника";
System.out.println("Список: " + list);
System.out.println("Массив: " + Arrays.toString(array));
}
} Вывод:
Список: [арбуз, вишня, дыня]
Массив: [арбуз, вишня, дыня]
Список: [яблоко, брусника, дыня]
Массив: [яблоко, брусника, дыня] «яблоко» записано через список, «брусника» — напрямую в массив, но оба изменения видны и там, и там: это одни и те же данные. Какие операции поддерживает такой список:
| Операция | Результат | Почему |
|---|---|---|
get(i), size(), contains(), indexOf() | Работают | Читают массив |
set(i, e), list.sort(...), Collections.shuffle(list) | Работают и меняют массив | Размер не меняется, элементы переставляются на месте |
add(e), add(i, e), remove(int), addAll() | UnsupportedOperationException | Длину массива изменить нельзя |
remove(Object), removeIf(), clear() | UnsupportedOperationException, если есть что удалять | Для отсутствующего элемента remove("груша") просто вернёт false, а clear() пустого списка ничего не сделает |
Элемент null | Разрешён | В отличие от List.of(), Arrays.asList("a", null) допустим |
Что выведет этот код?
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AsListSortExample {
public static void main(String[] args) {
String[] array = {"дыня", "арбуз", "вишня"};
List<String> list = Arrays.asList(array);
list.sort(null);
System.out.println(Arrays.toString(array));
System.out.println(list.getClass());
System.out.println(list instanceof ArrayList);
list.add("груша");
}
} Вывод:
[арбуз, вишня, дыня]
class java.util.Arrays$ArrayList
false
Exception in thread "main" java.lang.UnsupportedOperationException Сортировка списка отсортировала и массив. Класс списка называется Arrays$ArrayList — это закрытый вложенный класс Arrays, у которого нет методов, меняющих размер, а не привычный java.util.ArrayList. Из-за совпадения имён в стектрейсе ошибку часто ищут не там.
Массив примитивов не превращается в список чисел
Arrays.asList(new int[]{1, 2, 3}) возвращает List<int[]> из одного элемента — самого массива: обобщённый параметр T не может быть примитивом, поэтому весь int[] становится одним аргументом varargs. Запись List<Integer> list = Arrays.asList(new int[]{1, 2, 3}) не скомпилируется. С Integer[] или перечислением значений Arrays.asList(1, 2, 3) всё работает как ожидается.
Метод subList(): представление части списка
Метод list.subList(from, to) возвращает представление элементов с индекса from включительно до to не включительно. Через подсписок можно читать, заменять, добавлять и удалять элементы — всё сразу отражается в исходном списке. Но если размер исходного списка изменить в обход подсписка, подсписок становится недействительным.
import java.util.ArrayList;
import java.util.List;
public class SubListExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
List<String> sub = list.subList(1, 4);
System.out.println(sub);
sub.set(0, "B");
System.out.println(list);
sub.clear();
System.out.println(list + " " + sub);
list.add("f");
System.out.println(sub);
}
} Вывод:
[b, c, d]
[a, B, c, d, e]
[a, e] []
Exception in thread "main" java.util.ConcurrentModificationException Строка sub.clear() удалила из исходного списка элементы с индексами 1–3 — это стандартный способ удалить диапазон: list.subList(from, to).clear(). После list.add("f") подсписок «знает», что исходный список изменился структурно, и на любое обращение (toString(), size(), get()) отвечает ConcurrentModificationException. Замена через list.set() структурным изменением не считается и подсписок не ломает. Как работает этот fail-fast механизм, объясняет урок Интерфейс Iterator.
Неверные границы проверяются сразу: для списка из пяти элементов subList(3, 1) бросит IllegalArgumentException: fromIndex(3) > toIndex(1), а subList(0, 10) — IndexOutOfBoundsException: toIndex = 10. У списка из Arrays.asList() подсписок тоже фиксированного размера: subList(0, 2).clear() бросит UnsupportedOperationException.
Представление или копия
Список из Arrays.asList() — представление массива, а List.of(array), List.copyOf() и new ArrayList<>(...) создают копию: после изменения исходного массива они продолжают хранить старые значения. Неизменяемость и связь с источником — разные свойства: List.of() неизменяем, но не является представлением.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ViewVsCopyExample {
public static void main(String[] args) {
String[] array = {"арбуз", "вишня"};
List<String> view = Arrays.asList(array); // представление
List<String> copy = new ArrayList<>(Arrays.asList(array)); // изменяемая копия
List<String> frozen = List.of(array); // неизменяемая копия, Java 9
List<String> frozenCopy = List.copyOf(view); // неизменяемая копия, Java 10
array[0] = "яблоко";
System.out.println("Arrays.asList: " + view);
System.out.println("new ArrayList: " + copy);
System.out.println("List.of: " + frozen);
System.out.println("List.copyOf: " + frozenCopy);
}
} Вывод:
Arrays.asList: [яблоко, вишня]
new ArrayList: [арбуз, вишня]
List.of: [арбуз, вишня]
List.copyOf: [арбуз, вишня] Изменение массива увидел только view. Независимую копию любого представления делают так же: new ArrayList<>(list.subList(1, 4)), new TreeMap<>(map.subMap("б", "ж")), new HashSet<>(map.keySet()). Обратная ситуация с Collections.unmodifiableList(list): изменить список через обёртку нельзя, но это всё равно представление, и изменения исходного list через неё видны. Чем ещё различаются способы создать неизменяемый список (поддержка null, версии Java), сравнивается в разделе Неизменяемые коллекции.
На чём чаще всего ошибаются
Почти все ошибки с backed-коллекциями возникают из-за того, что представление принимают за самостоятельную коллекцию: ждут, что в него можно добавлять элементы, что оно не изменится вместе с источником или что источник можно менять как угодно. Типичные промахи и исправления:
| Что сделали | Что произойдёт | Как исправить |
|---|---|---|
Arrays.asList(...).add(e) или remove(i) | UnsupportedOperationException | new ArrayList<>(Arrays.asList(...)) |
Передали int[] в Arrays.asList() | Список List<int[]> из одного элемента | Integer[] или Arrays.stream(ints).boxed().toList() |
Изменили список через set(), не ожидая, что изменится массив | Исходный массив тоже изменён | Работать с копией: new ArrayList<>(Arrays.asList(array)) |
Сохранили subList(), затем вызвали add()/remove() у исходного списка | ConcurrentModificationException при следующем обращении к подсписку | Менять данные через подсписок или сразу скопировать его в new ArrayList<>(...) |
put() в subMap()/headMap() ключа вне диапазона | IllegalArgumentException: key out of range | Добавлять в исходную карту |
Вернули из метода Collections.unmodifiableList(list) как «снимок» | Последующие изменения list видны вызывающему коду | List.copyOf(list) |
Часто задаваемые вопросы
Чем Arrays.asList() отличается от List.of()?
Arrays.asList() — представление массива фиксированного размера: set() разрешён и меняет массив, null допустим. List.of() (Java 9) — неизменяемая копия: set() бросает UnsupportedOperationException, null — NullPointerException, изменения исходного массива не видны. Пример с выводом — в разделе Представление или копия.
Как превратить массив int[] в List<Integer>?
Arrays.asList() здесь не поможет. Используйте стрим: Arrays.stream(ints).boxed().toList() (Java 16, неизменяемый список) или Arrays.stream(ints).boxed().collect(Collectors.toList()) для изменяемого. Результат — копия: связи с исходным массивом у него нет.
Может ли subList() привести к утечке памяти?
Да. Подсписок хранит ссылку на весь исходный список, поэтому, если сохранить в поле bigList.subList(0, 10) от списка на миллион элементов, сборщик мусора не освободит ни один из них. Когда нужен долгоживущий фрагмент, копируйте его: new ArrayList<>(bigList.subList(0, 10)) или List.copyOf(...).
Зачем нужны представления, если можно скопировать данные?
Представление создаётся за O(1) и не занимает памяти под элементы, а изменения через него сразу попадают в источник. Поэтому list.subList(a, b).clear() удаляет диапазон за одну операцию, map.keySet().removeIf(...) удаляет пары из карты, а Arrays.asList() позволяет передать массив в методы, принимающие List, без копирования. Копия нужна, когда данные должны жить независимо от источника.
Video Explanation
Prefer video format? Watch this lesson with examples and explanations.
Comments