Интерфейс List и класс ArrayList в Java
Попробуйте выполнить List<String> list = new ArrayList<>(10); list.set(0, "Java"); — кажется, что в списке уже есть 10 ячеек, но программа падает с IndexOutOfBoundsException: Index 0 out of bounds for length 0. Число в конструкторе — это ёмкость внутреннего массива, а не количество элементов: пока не вызван add(), size() равен 0.
Интерфейс List в Java — это упорядоченная коллекция (последовательность), которая хранит элементы в порядке добавления, допускает дубликаты и null и даёт доступ к элементу по индексу от 0 до size() - 1. ArrayList — самая используемая реализация List: динамический массив, который сам увеличивается при добавлении элементов.
Что такое интерфейс List в Java
Интерфейс List<E> из пакета java.util описывает список — коллекцию, в которой у каждого элемента есть позиция (индекс). Он расширяет интерфейс Collection, наследует его методы add(), remove(), contains(), size() и добавляет операции с индексом.
Свойства любого List:
- порядок сохраняется — элементы перебираются в том порядке, в котором их добавили (или вставили по индексу);
- дубликаты разрешены — одно и то же значение может встречаться несколько раз;
- доступ по индексу — нумерация с нуля, как у массива;
- null допускается в
ArrayListиLinkedList, но не в неизменяемых спискахList.of().
Почти все собственные методы List принимают или возвращают индекс: add(int, E), get(int), set(int, E), remove(int), indexOf(), lastIndexOf(), subList(), listIterator(). Полный список с граничными случаями — в разделе Основные методы.
Иерархия интерфейса List:

Реализации List: ArrayList, LinkedList, Vector, List.of()
Основные реализации интерфейса List в Java — ArrayList (динамический массив), LinkedList (двусвязный список), устаревший синхронизированный Vector, потокобезопасный CopyOnWriteArrayList и неизменяемые списки из List.of(). В большинстве задач выбирают ArrayList: у него самый быстрый доступ по индексу и меньше расход памяти.
| Реализация | Внутри | get(i) | Вставка/удаление в середине | Особенности |
|---|---|---|---|---|
ArrayList | Массив Object[] | O(1) | O(n) — сдвиг элементов | Выбор по умолчанию; не синхронизирован |
LinkedList | Двусвязный список узлов | O(n) | O(1) на найденной позиции, но поиск позиции — O(n) | Реализует ещё и Deque; сравнение с ArrayList — в уроке LinkedList |
Vector (и Stack) | Массив Object[] | O(1) | O(n) | Все методы synchronized, массив растёт вдвое; класс из Java 1.0 — Legacy Classes |
CopyOnWriteArrayList | Массив, копируется при каждой записи | O(1) | O(n) + копия массива | Потокобезопасный, для редких изменений — потокобезопасные коллекции |
List.of(), List.copyOf() | Неизменяемый массив | O(1) | Нельзя — UnsupportedOperationException | Java 9/10; null запрещён — неизменяемые коллекции |
Ещё один частый источник списков — Arrays.asList(): он возвращает список фиксированного размера, связанный с исходным массивом. Как это работает, показано в уроке Backed Collections.
Создание ArrayList и List
ArrayList создают одним из трёх конструкторов: new ArrayList<>() — пустой список, new ArrayList<>(capacity) — пустой список с заранее выделенным массивом, new ArrayList<>(collection) — копия другой коллекции. Для готового неизменяемого списка используют фабрику List.of(...). Переменную принято объявлять типом интерфейса List.
| Способ | Что получится | Когда использовать |
|---|---|---|
new ArrayList<>() | Пустой изменяемый список; массив на 10 ячеек выделяется при первом add() | Обычный случай |
new ArrayList<>(int capacity) | Пустой список с массивом на capacity ячеек; отрицательное значение — IllegalArgumentException | Заранее известно примерное число элементов |
new ArrayList<>(Collection<? extends E> c) | Изменяемая копия элементов c в порядке её итератора | Нужна независимая копия или изменяемый список с начальными значениями |
List.of(e1, e2, ...) | Неизменяемый список (Java 9) | Константы, тестовые данные, параметры методов |
import java.util.ArrayList;
import java.util.List;
public class ArrayListCreateExample {
public static void main(String[] args) {
List<String> empty = new ArrayList<>();
List<String> withCapacity = new ArrayList<>(100);
List<String> copy = new ArrayList<>(List.of("Java", "Kotlin"));
List<String> fixed = List.of("Java", "Kotlin");
copy.add("Scala");
System.out.println(empty.size()); // 0
System.out.println(withCapacity.size()); // 0 - ёмкость 100, но элементов нет
System.out.println(copy); // [Java, Kotlin, Scala]
System.out.println(fixed); // [Java, Kotlin]
// fixed.add("Scala"); - UnsupportedOperationException
}
} Вывод:
0
0
[Java, Kotlin, Scala]
[Java, Kotlin] Как устроен ArrayList: capacity и size
Внутри ArrayList хранит два поля: массив Object[] elementData и счётчик size. Длина массива называется ёмкостью (capacity), а size — число реально добавленных элементов. Когда массив заполнен, ArrayList создаёт новый, в 1,5 раза больше, и копирует в него элементы — незаметно для программиста.
Массив имеет тип Object[], а не String[] или E[]: из-за стирания типов параметр <E> во время выполнения неизвестен. Поэтому в ArrayList хранятся только объекты; примитивы (int, double) кладут через классы-обёртки — List<Integer>.
new ArrayList<>()с Java 8 не выделяет память сразу: внутренний массив пустой, а при первомadd()создаётся массив на 10 элементов.- Дальше ёмкость растёт по формуле
old + old / 2: 10 → 15 → 22 → 33 → 49. - Если число элементов известно заранее, передайте его в конструктор или вызовите
ensureCapacity(n)— это избавит от лишних копирований массива. Освободить неиспользуемые ячейки можно методомtrimToSize(). Оба метода есть только у классаArrayList, не у интерфейсаList.
Ёмкость не делает индексы доступными — вот что происходит в примере из начала урока:
import java.util.ArrayList;
import java.util.List;
public class CapacityVsSizeExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(10);
System.out.println(list.size());
list.set(0, "Java");
}
} Вывод:
0
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0 Правильно — сначала добавить элемент через add("Java"), а set() использовать только для замены существующих.
Какое исключение ловить
Массив при выходе за границу бросает ArrayIndexOutOfBoundsException, а методы List — IndexOutOfBoundsException (его родительский класс). Добавлять элементы сверх ёмкости можно сколько угодно — исключение возникает только при обращении к индексу < 0 или ≥ size().
Из устройства следует скорость операций:
| Операция | Сложность | Почему |
|---|---|---|
get(i), set(i, e) | O(1) | Прямое обращение к ячейке массива |
add(e) в конец | O(1) амортизированно | Обычно есть свободная ячейка; изредка — копирование массива за O(n) |
remove(size() - 1) | O(1) | Сдвигать нечего |
add(i, e), remove(i) в начале или середине | O(n) | Хвост массива сдвигается через System.arraycopy() |
indexOf(), contains(), remove(Object) | O(n) | Линейный поиск с вызовом equals() |
Основные методы List и ArrayList
Основные методы ArrayList: add() добавляет элемент, get() читает по индексу, set() заменяет, remove() удаляет по индексу или по значению, indexOf() и contains() ищут, size() возвращает количество элементов. Часть методов объявлена в Collection, часть — в самом List; в таблице указано, откуда метод и что происходит в граничном случае.
| Метод | Откуда | Что делает | Граничный случай |
|---|---|---|---|
boolean add(E e) | Collection | Добавляет элемент в конец, возвращает true | У List.of() — UnsupportedOperationException |
void add(int index, E e) | List | Вставляет элемент, сдвигая последующие вправо | index == size() — вставка в конец; index > size() — IndexOutOfBoundsException |
boolean addAll(Collection c)boolean addAll(int index, Collection c) | Collection / List | Добавляет все элементы c в конец или начиная с позиции index | Возвращает true, если список изменился; для пустой c — false |
E get(int index) | List | Возвращает элемент по индексу | index < 0 или ≥ size() — IndexOutOfBoundsException |
E set(int index, E e) | List | Заменяет элемент и возвращает старое значение | Не добавляет новых позиций: на пустом списке set(0, e) бросает исключение |
E remove(int index) | List | Удаляет элемент по индексу и возвращает его; хвост сдвигается влево | Для List<Integer> вызов remove(1) удаляет по индексу, а не число 1 |
boolean remove(Object o) | Collection | Удаляет первое вхождение o | Если элемента нет — false, исключения нет |
int indexOf(Object o)int lastIndexOf(Object o) | List | Индекс первого / последнего вхождения | Элемента нет — -1; сравнение через equals() |
boolean contains(Object o) | Collection | Проверяет, есть ли элемент | Линейный поиск O(n); List.of().contains(null) — NullPointerException |
int size(), boolean isEmpty() | Collection | Количество элементов / пуст ли список | Возвращает число элементов, а не ёмкость массива |
boolean removeAll(Collection c)boolean retainAll(Collection c) | Collection | Удаляет все элементы, которые есть в c / оставляет только их | Удаляются все вхождения, включая дубликаты |
void clear() | Collection | Удаляет все элементы | Ёмкость массива не уменьшается (для этого — trimToSize()) |
List<E> subList(int from, int to) | List | Представление (view) части списка с from до to - 1 | Изменения видны в обоих списках; после add()/remove() в исходном списке обращение к subList — ConcurrentModificationException; from > to — IllegalArgumentException |
void sort(Comparator c) | List, Java 8 | Сортирует список на месте | sort(null) — естественный порядок (Comparable) |
ListIterator<E> listIterator() | List | Двунаправленный итератор | Разбор — в уроке Iterator |
E getFirst(), E getLast(), List<E> reversed() | SequencedCollection, Java 21 | Первый / последний элемент, обратное представление | Пустой список — NoSuchElementException; на Java 17 не компилируется (подробнее) |
Добавление элементов: add(), set(), addAll()
Элемент в ArrayList добавляют методом add(e) — в конец списка — или add(index, e) — в указанную позицию со сдвигом остальных вправо. Метод set(index, e) ничего не добавляет, а заменяет существующий элемент. Несколько элементов сразу добавляет addAll().
import java.util.ArrayList;
import java.util.List;
public class ArrayListAddExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
System.out.println("Начальный размер arrayList: "
+ arrayList.size());
arrayList.add("C");
arrayList.add("A");
arrayList.add("E");
arrayList.add("B");
arrayList.add("D");
arrayList.add("F");
arrayList.add("F");
arrayList.add(1, "A2");
arrayList.set(0, "C2");
System.out.println("Размер arrayList после добавления: "
+ arrayList.size());
System.out.println("Содержимое arrayList: " + arrayList);
System.out.println(arrayList.get(0));
}
} Вывод:
Начальный размер arrayList: 0
Размер arrayList после добавления: 8
Содержимое arrayList: [C2, A2, A, E, B, D, F, F]
C2 "A2" встал на позицию 1 и сдвинул "A" и остальные элементы вправо, а set(0, "C2") заменил "C" — размер от этого не изменился. Дубликат "F" сохранился: список, в отличие от Set, повторы допускает.
Метод addAll(index, collection) вставляет целую коллекцию с заданной позиции, а clear() очищает список:
import java.util.ArrayList;
import java.util.List;
public class ArrayListAddAllExample {
public static void main(String[] args) {
List<String> arrayList1 = new ArrayList<>();
List<String> arrayList2 = List.of("1", "2");
arrayList1.add("A");
arrayList1.add("B");
arrayList1.add("C");
arrayList1.add("D");
arrayList1.add("E");
arrayList1.add("F");
System.out.println("arrayList1 до добавления " + arrayList1);
arrayList1.addAll(3, arrayList2);
System.out.println("arrayList1 после добавления " + arrayList1);
arrayList1.clear();
System.out.println("arrayList1 после очистки " + arrayList1);
}
} Вывод:
arrayList1 до добавления [A, B, C, D, E, F]
arrayList1 после добавления [A, B, C, 1, 2, D, E, F]
arrayList1 после очистки [] Удаление элементов: remove(), removeAll(), retainAll(), clear()
Удалить элемент из ArrayList можно по индексу — remove(int index), по значению — remove(Object o) (удаляется только первое вхождение), группой — removeAll(collection), а retainAll(collection) оставляет только элементы из переданной коллекции. clear() удаляет всё. После удаления из середины элементы справа сдвигаются на одну позицию влево.
import java.util.ArrayList;
import java.util.List;
public class ArrayListRemoveExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("C");
arrayList.add("A");
arrayList.add("E");
arrayList.add("B");
arrayList.add("D");
arrayList.add("F");
arrayList.add("F");
arrayList.add(1, "A2");
arrayList.set(0, "C2");
System.out.println("Содержимое arrayList: " + arrayList);
System.out.println("Размер arrayList после добавления: "
+ arrayList.size());
arrayList.remove("F");
arrayList.remove(2);
System.out.println("Размер arrayList после удаления: "
+ arrayList.size());
System.out.println("Содержимое arrayList: " + arrayList);
}
} Вывод:
Содержимое arrayList: [C2, A2, A, E, B, D, F, F]
Размер arrayList после добавления: 8
Размер arrayList после удаления: 6
Содержимое arrayList: [C2, A2, E, B, D, F] remove("F") удалил только первую "F", вторая осталась в конце; remove(2) удалил элемент с индексом 2 — "A".
Неочевидный момент: remove() для List<Integer>
У списка чисел два метода remove() конкурируют: компилятор выбирает remove(int index), если аргумент — литерал int. Для List<Integer> nums = new ArrayList<>(List.of(10, 20, 30)) вызов nums.remove(1) удалит 20 (индекс 1) и оставит [10, 30], а nums.remove(Integer.valueOf(10)) удалит само значение и оставит [30]. Вызов nums.remove(10) на этом списке бросит IndexOutOfBoundsException.
Метод removeAll()
removeAll(c) удаляет из списка все вхождения элементов, которые есть в коллекции c. Значения из c, которых нет в списке (здесь "AA"), просто игнорируются.
import java.util.ArrayList;
import java.util.List;
public class ArrayListRemoveAllExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("C");
arrayList.add("A");
arrayList.add("E");
arrayList.add("B");
arrayList.add("D");
arrayList.add("F");
arrayList.add("F");
arrayList.add(1, "A2");
arrayList.set(0, "C2");
List<String> removeElements = List.of("C2", "A2", "AA", "F");
System.out.println("Содержимое arrayList до removeAll: "
+ arrayList);
arrayList.removeAll(removeElements);
System.out.println("Содержимое arrayList после removeAll: "
+ arrayList);
}
} Вывод:
Содержимое arrayList до removeAll: [C2, A2, A, E, B, D, F, F]
Содержимое arrayList после removeAll: [A, E, B, D] Метод retainAll()
retainAll(c) — обратная операция: оставляет в списке только элементы, которые есть в c, со всеми их повторами и в исходном порядке.
import java.util.ArrayList;
import java.util.List;
public class ArrayListRetainAllExample {
public static void main(String[] args) {
List<String> arrayList1 = new ArrayList<>();
List<String> arrayList2 = List.of("F", "FF", "E");
arrayList1.add("A");
arrayList1.add("A");
arrayList1.add("B");
arrayList1.add("C");
arrayList1.add("D");
arrayList1.add("E");
arrayList1.add("F");
arrayList1.add("F");
arrayList1.retainAll(arrayList2);
System.out.println(arrayList1);
}
} Вывод:
[E, F, F] Поиск: indexOf(), lastIndexOf(), contains()
Метод indexOf(o) возвращает индекс первого вхождения элемента в List, lastIndexOf(o) — последнего, а если элемента нет, оба возвращают -1. contains(o) отвечает только «есть или нет». Все три метода перебирают список от начала (O(n)) и сравнивают элементы через equals().
import java.util.ArrayList;
import java.util.List;
public class ArrayListSearchExample {
public static void main(String[] args) {
List<String> langs = new ArrayList<>(List.of("Java", "Kotlin", "Java", "Scala"));
System.out.println(langs.indexOf("Java")); // 0
System.out.println(langs.lastIndexOf("Java")); // 2
System.out.println(langs.indexOf("Go")); // -1
System.out.println(langs.contains("Kotlin")); // true
System.out.println(langs.contains("java")); // false - регистр важен
}
} Вывод:
0
2
-1
true
false Для собственных классов поиск работает, только если в них переопределён метод equals(): без него indexOf() сравнивает ссылки и не найдёт объект с теми же полями. Если искать приходится часто, а порядок не важен, быстрее HashSet — у него contains() работает за O(1).
Перебор элементов List
Перебрать List в Java можно тремя способами: циклом for по индексу (нужен, когда важен номер элемента), циклом for-each (самый короткий и читаемый) и методом forEach() с лямбдой или ссылкой на метод (Java 8). Для ArrayList все три работают за O(n).
import java.util.ArrayList;
import java.util.List;
public class ArrayListIterateExample {
public static void main(String[] args) {
List<String> langs = new ArrayList<>(List.of("Java", "Kotlin", "Scala"));
for (int i = 0; i < langs.size(); i++) {
System.out.println(i + ": " + langs.get(i));
}
for (String lang : langs) {
System.out.print(lang + " ");
}
System.out.println();
langs.forEach(System.out::println);
}
} Вывод:
0: Java
1: Kotlin
2: Scala
Java Kotlin Scala
Java
Kotlin
Scala Вызывать list.remove() или list.add() внутри for-each нельзя: следующий шаг цикла бросит ConcurrentModificationException. Почему так происходит и как работают Iterator и ListIterator, разобрано в уроке Интерфейс Iterator. Для LinkedList цикл for с get(i) не подходит — каждый get(i) там идёт по цепочке узлов, и обход становится O(n²).
Преобразование ArrayList в массив: toArray()
Превратить ArrayList в массив можно методом toArray() из интерфейса Collection. Без аргументов он возвращает Object[], а чтобы получить типизированный массив, например String[], передают массив нужного типа — toArray(new String[0]) — или, начиная с Java 11, ссылку на конструктор: toArray(String[]::new).
| Вызов | Результат | Нюанс |
|---|---|---|
Object[] toArray() | Новый массив Object[] | Привести его к String[] нельзя — ClassCastException |
<T> T[] toArray(T[] a) | Массив типа T | Если a короче списка, создаётся новый массив; если длиннее — элементы пишутся в a, а ячейка сразу после последнего элемента становится null |
<T> T[] toArray(IntFunction<T[]> generator) | Массив типа T (Java 11) | Самая короткая запись: list.toArray(String[]::new) |
Что выведет этот код? Обратите внимание на третий вариант с массивом длиннее списка:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ArrayListToArrayExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>(List.of("C", "A", "E", "B", "D", "F"));
// 1. Object[]
Object[] objectArray = arrayList.toArray();
System.out.println(Arrays.toString(objectArray));
// 2. Массив нулевой длины - метод создаст новый массив нужного размера
String[] stringArray = arrayList.toArray(new String[0]);
System.out.println(Arrays.toString(stringArray));
// 3. Массив длиннее списка
String[] big = new String[8];
Arrays.fill(big, "X");
arrayList.toArray(big);
System.out.println(Arrays.toString(big));
// 4. Java 11+
String[] generated = arrayList.toArray(String[]::new);
System.out.println(generated.length);
// String[] wrong = (String[]) arrayList.toArray(); - ClassCastException:
// class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;
}
} Вывод:
[C, A, E, B, D, F]
[C, A, E, B, D, F]
[C, A, E, B, D, F, null, X]
6 В третьем случае метод записал 6 элементов в переданный массив и поставил null в ячейку с индексом 6 — так по массиву можно понять, где кончился список; последняя "X" осталась нетронутой. Вариант new String[0] короче и на современных JVM не медленнее, чем new String[list.size()], поэтому его рекомендуют по умолчанию.
Часто задаваемые вопросы
Чем ArrayList отличается от обычного массива?
Длина массива задаётся при создании и не меняется, а ArrayList растёт сам. Массив хранит и примитивы (int[]), а ArrayList — только объекты (List<Integer> с автоупаковкой). Длину массива дают через поле length, размер списка — через метод size(). Переход между ними — toArray() и new ArrayList<>(Arrays.asList(array)), подробнее в разделе toArray().
Как отсортировать ArrayList: list.sort() или Collections.sort()?
Результат одинаковый: с Java 8 Collections.sort(list) просто вызывает list.sort(null). В новом коде пишут list.sort(null) для естественного порядка или list.sort(Comparator.reverseOrder()) для обратного. Для списка из List.of() оба способа бросят UnsupportedOperationException. Другие алгоритмы — в уроке Методы класса Collections.
Как удалить элементы из ArrayList по условию во время перебора?
Проще всего — методом removeIf() (Java 8): list.removeIf(s -> s.isEmpty()) удалит все пустые строки за один проход. Второй вариант — явный Iterator и его метод remove(). Вызов list.remove() внутри for-each приводит к ConcurrentModificationException (см. раздел Перебор элементов List).
Как сравнить два списка в Java?
Метод equals() у List возвращает true, если списки одного размера и элементы попарно равны в том же порядке. Класс реализации не важен: ArrayList и LinkedList с элементами [a, b] равны, а [a, b] и [b, a] — нет. Чтобы сравнить без учёта порядка, отсортируйте копии списков и сравните их.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
Все задачи модуля →Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии