Коллекции ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-23

Интерфейс 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, Stack

Реализации 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(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] — нет. Чтобы сравнить без учёта порядка, отсортируйте копии списков и сравните их.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.