Интерфейс List в Java
Упорядоченные коллекции с доступом по индексу в Java: интерфейс List и его основные операции.
List<E> — это Collection<E> плюс два дополнительных обязательства: элементы имеют определённый порядок, и этот порядок адресуется целочисленным индексом. Как только есть порядок и индекс, становится осмысленным целый класс методов — get(i), set(i, x), indexOf(x), subList(from, to), сортировка, итерация в обратном порядке. Эта глава рассматривает контракт; реализации (ArrayList, LinkedList, Vector) следуют сразу после со своими характеристиками производительности.
Что означает «упорядоченный» в данном контексте
«Упорядоченный» применительно к List означает, что порядок вставки сохраняется — индекс 0 — это первый добавленный элемент, индекс size() - 1 — последний, а добавление нового элемента в конец ничего не сдвигает. Это не «отсортированный» — список сохраняет тот порядок, который вы создаёте. Если нужна сортированная итерация, вызовите Collections.sort(list) (изменяет список) или используйте TreeSet / TreeMap с самого начала. Не путайте эти понятия.
Дубликаты разрешены. [1, 1, 2, 1] — вполне допустимый List<Integer>.
Методы, которые List добавляет к Collection
Всё, что объявляет Collection, по-прежнему присутствует — add, remove, contains, size и т. д. List добавляет позиционные и порядко-зависимые операции:
Позиционный доступ
E get(int index)— элемент по индексуindex.E set(int index, E element)— заменить, возвращая старое значение.void add(int index, E element)— вставить (сдвигает следующие элементы вправо).E remove(int index)— удалить по позиции (возвращает удалённый элемент). Обратите внимание на перегрузку сObject—list.remove(1)вызывает версию сint;list.remove(Integer.valueOf(1))вызывает версию сObject.
Поиск
int indexOf(Object o)— первое вхождение или-1.int lastIndexOf(Object o)— последнее вхождение или-1.
Подпредставления и итерация
List<E> subList(int fromIndex, int toIndex)— живое представление диапазона. Изменение его изменяет исходный список (и наоборот). Полуоткрытый интервал:[from, to).ListIterator<E> listIterator()/listIterator(int index)— итератор, который также может идти в обратном направлении, получать текущий индекс и выполнятьset/addна позиции курсора. Глава ListIterator описывает его подробнее.
Массовые изменения с учётом порядка
default void replaceAll(UnaryOperator<E> op)— применитьopк каждому элементу на месте.default void sort(Comparator<? super E> c)— отсортировать список, используяc(или естественный порядок, еслиnull).boolean addAll(int index, Collection<? extends E> c)— вставить целую коллекцию начиная с позицииindex.
Фабрики (Java 9+)
List.of(...)— неизменяемый список из заданных элементов. Компактный, без лишних аллокаций для небольших размеров.List.copyOf(Collection)— неизменяемый снимок другой коллекции.
Равенство List зависит от порядка
Два списка равны тогда и только тогда, когда они имеют одинаковый размер, одинаковый порядок и равные элементы на каждом индексе. List.of(1, 2) не равен List.of(2, 1), даже если как Set-ы они были бы равны. Это жёсткое правило контракта List — если вы сравниваете два списка и получаете false, когда «не должны», проверьте порядок в первую очередь.
subList — это представление, а не копия
С этим сталкивается почти каждый, кто учится:
List<Integer> xs = new ArrayList<>(List.of(0, 1, 2, 3, 4, 5));
List<Integer> middle = xs.subList(2, 5); // [2, 3, 4]
middle.set(0, 99);
System.out.println(xs); // [0, 1, 99, 3, 4, 5] — xs changed!
middle.clear();
System.out.println(xs); // [0, 1, 5] — gone from xssubList возвращает живое окно. Чтения и записи проходят через исходный список. Это очень полезно для алгоритмов на месте — очистить диапазон, отсортировать диапазон, вставить диапазон — но это также означает, что нельзя хранить ссылку на subList и при этом изменять родительский список через любой другой путь. В Javadoc сказано, что структурные изменения исходного списка за пределами подсписка «неопределяют» поведение подсписка. На практике — ConcurrentModificationException при следующем же обращении.
Если нужен независимый срез, скопируйте его: new ArrayList<>(xs.subList(2, 5)).
Две перегрузки remove
Распространённая ошибка:
List<Integer> nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(1); // removes index 1 → [10, 30]
nums.remove(Integer.valueOf(10)); // removes the value 10 → [30]Перегрузка с int побеждает, потому что int более специфичен, чем Integer. Если вы хотите «удалить значение 10», оберните его явно. Это одно из немногих мест в языке, где правила автобоксинга и разрешение перегрузки активно конфликтуют.
Сортировка на месте
list.sort(comparator) изменяет список. Передайте null, чтобы использовать естественный порядок элементов (их Comparable); иначе передайте Comparator. Это современная форма — Collections.sort(list) по-прежнему работает и идентична, но list.sort(...) является методом по умолчанию в List:
List<String> names = new ArrayList<>(List.of("Linus", "Ada", "Grace"));
names.sort(null); // natural: ["Ada", "Grace", "Linus"]
names.sort(Comparator.comparingInt(String::length)); // shortest firstГлава Comparable / Comparator далее в этой части является справочником по тому, что означает null и как строить компараторы для собственных типов.
Неизменяемые фабрики: когда add выбрасывает исключение
List.of(...), List.copyOf(...) и списки, возвращаемые Collectors.toUnmodifiableList(), являются неизменяемыми. Они отклоняют каждый изменяющий вызов с UnsupportedOperationException. Они также не допускают элементы null. Они идеальны для данных только для чтения, используемых повсеместно:
List<String> CONSTANTS = List.of("red", "green", "blue");
CONSTANTS.add("yellow"); // throws UnsupportedOperationExceptionЕсли вы планируете мутировать список позже, начинайте с new ArrayList<>(List.of(...)).
Практический пример: все методы, специфичные для List
Программа ниже демонстрирует методы, которые List добавляет к Collection. Обратите внимание на то, как мутация через subList распространяется на исходный список, на ловушку перегрузки и на разницу между sort и replaceAll.
Несколько выводов из результатов, которые стоит запомнить:
remove(1)удалил20(значение по индексу 1);remove(Integer.valueOf(10))удалил10по значению. Одно имя метода — две разные задачи в зависимости от статического типа аргумента.- После
mid.clear()родительский список равен[0, 1, 5]. Представление и было тем диапазоном — его очистка удалила эти элементы из исходного массива. replaceAllсохраняет длину списка и перезаписывает каждый элемент на месте;sortпереставляет уже имеющиеся элементы. Они хорошо сочетаются друг с другом.
Что дальше
Теперь вы знаете контракт List — что гарантируется, что изменяет что, где скрываются подводные камни. Пришло время познакомиться с реализацией, которую вы будете использовать в 90% случаев: ArrayList на основе массива с изменяемым размером. Тот же контракт, конкретные характеристики производительности и несколько собственных дополнений.