Коллекции ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-24

Класс LinkedList в Java

Цикл for (int i = 0; i < list.size(); i++) { list.get(i); } выглядит безобидно, но на LinkedList из 100 000 элементов он делает около 2,5 миллиарда переходов по ссылкам: каждый вызов get(i) заново идёт к нужному узлу от начала или конца списка. Тот же обход циклом for-each делает 100 000 шагов. Причина — в том, как устроен этот класс.

LinkedList (java.util.LinkedList) — это реализация двусвязного списка (linked list) в Java: каждый элемент хранится в отдельном узле со ссылками на предыдущий и следующий узлы. Класс реализует сразу два интерфейса — List и Deque, поэтому работает как список, очередь, двусторонняя очередь и стек. Добавление и удаление на концах выполняется за O(1), доступ по индексу — за O(n). LinkedList допускает любые значения, в том числе null, и не потокобезопасен.

Реализация LinkedList в Java: узлы Node

Внутри LinkedList нет массива: каждый элемент лежит в объекте-узле Node, который хранит сам элемент (item) и ссылки на следующий (next) и предыдущий (prev) узлы. Сам список помнит только первый узел (first), последний (last) и количество элементов (size).

Так выглядит фрагмент исходного кода LinkedList в современном JDK (Java 7 и новее, включая Java 8, 17 и 21):

public class LinkedList<E> extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable {

    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    // ...
}

Node — хороший пример статического вложенного класса: узлу не нужна ссылка на объект списка, поэтому он объявлен static. У первого узла prev == null, у последнего next == null.

Графически LinkedList из четырёх элементов A, B, C и D можно представить так (стрелки — ссылки next и prev):

Структура LinkedList: двусвязный список из узлов A, B, C, D со ссылками next и prev

Entry или Node?

В старых книгах и статьях внутренний класс называется Entry с полем element, а список замкнут в кольцо через пустой узел-заголовок header. Так LinkedList был устроен до Java 6 включительно. С Java 7 класс переписан: узел называется Node, элемент хранится в поле item, а вместо заголовка список держит ссылки first и last. Принцип двусвязного списка при этом не изменился.

Как работает get(index) в LinkedList

Метод get(index) не может сразу перейти к нужной ячейке, как массив: он идёт по ссылкам от узла к узлу. Чтобы сократить путь, JDK начинает с ближнего конца — с first, если индекс в первой половине, и с last, если во второй:

Node<E> node(int index) {
    if (index < (size >> 1)) {          // индекс в первой половине
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {                            // во второй — идём с конца
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

Поэтому get(0) и get(size() - 1) выполняются мгновенно, а дольше всего ищется элемент в середине — до n/2 шагов. В нотации «O большое» это всё равно O(n). Через тот же метод node() работают set(index, e), add(index, e) и remove(index).

ArrayList vs LinkedList: в чём разница

ArrayList хранит элементы в массиве и даёт доступ по индексу за O(1), а LinkedList хранит их в связанных узлах: доступ по индексу у него O(n), зато вставка и удаление на найденной позиции не сдвигают остальные элементы. В большинстве задач быстрее ArrayList, а LinkedList выбирают для частых операций на обоих концах или вставок через итератор.

Удаление элемента B из середины связного списка сводится к перезаписи двух ссылок: A.next теперь указывает на C, а C.prev — на A. Остальные узлы не трогаются:

LinkedList после удаления элемента B: узел A ссылается на C, а C на A

Но чтобы удалить B вызовом remove(1) или remove("B"), его сначала нужно найти, а это проход по цепочке за O(n). O(1) получается только тогда, когда узел уже под рукой: на концах списка или в текущей позиции итератора.

Признак ArrayList LinkedList
Внутреннее устройство Массив Object[], растёт в 1,5 раза (подробнее) Двусвязный список узлов Node
get(i), set(i, e) O(1) O(n), до n/2 переходов по ссылкам
add(e) в конец O(1) амортизированно O(1), но на каждый элемент создаётся объект-узел
add(0, e), remove(0) O(n) — сдвиг всего массива O(1)
add(i, e), remove(i) в середине O(n) — сдвиг хвоста через System.arraycopy() O(n) на поиск узла + O(1) на перелинковку
Вставка/удаление через ListIterator O(n) — сдвиг хвоста O(1)
contains(), indexOf() O(n) O(n), и медленнее на практике
Память на элемент Одна ссылка в массиве (плюс запас ёмкости) Узел примерно 24 байта (заголовок объекта + три ссылки при сжатых указателях в 64-битной HotSpot)
Интерфейсы List, RandomAccess List, Deque
Когда выбирать По умолчанию для любого списка Частые вставки/удаления через итератор или на обоих концах, нужен null в очереди

Даже там, где у LinkedList лучшая оценка, ArrayList часто оказывается быстрее на реальных данных: элементы массива лежат в памяти подряд и хорошо попадают в кэш процессора, а System.arraycopy() сдвигает тысячи ссылок очень быстро. Узлы LinkedList разбросаны по куче, и каждый переход по next — потенциальный промах кэша. Если нужна именно очередь или стек, обычно лучше ArrayDeque — сравнение трёх классов есть в уроке ArrayDeque vs LinkedList vs Stack.

Мнение автора класса

Джошуа Блох, написавший LinkedList для Java 1.2, в 2015 году заметил в Twitter: «Does anyone actually use LinkedList? I wrote it, and I never use it» («Кто-нибудь вообще использует LinkedList? Я его написал и никогда им не пользуюсь»). Знать устройство LinkedList нужно — это классический вопрос собеседований, но в новом коде по умолчанию пишут new ArrayList<>() и new ArrayDeque<>(), а LinkedList берут, когда его преимущество подтверждено замером.

Конструкторы LinkedList

У LinkedList два конструктора: без параметров — пустой список — и с коллекцией, элементы которой копируются в новый список в порядке её итератора. Конструктора с начальной ёмкостью нет: массива внутри нет, и резервировать место заранее не нужно.

Конструктор Что создаёт Граничный случай
LinkedList() Пустой список: first == last == null, size == 0 Узлы создаются только при добавлении элементов
LinkedList(Collection<? extends E> c) Список из элементов c в порядке итератора c c == null — NullPointerException; null-элементы внутри c допускаются
new LinkedList<>(100) — Не компилируется: no suitable constructor found for LinkedList(int)

Переменную, как и для других коллекций, объявляют типом интерфейса: List<String> list = new LinkedList<>(); — если нужны методы списка, или Deque<String> deque = new LinkedList<>(); — если очереди.

Методы LinkedList: пример и сложность

Методы LinkedList приходят из двух интерфейсов: из List — работа по индексу (add(i, e), get(i), set(i, e), remove(i)), из Deque — работа с концами (addFirst(), addLast(), removeFirst(), removeLast(), peek(), poll()). Методы на концах выполняются за O(1), методы с индексом — за O(n).

В примере ниже переменная объявлена классом LinkedList, а не интерфейсом. В обычном коде так не делают, но здесь это сделано специально: через тип LinkedList доступны и методы List, и методы Deque одновременно.

import java.util.LinkedList;

public class LinkedListExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();

        list.add("F");
        list.add("B");
        list.add("D");
        list.add("E");
        list.add("C");
        list.addLast("Z");
        list.addFirst("A");
        list.add(1, "A2");
        System.out.println("Содержимое: " + list);

        list.remove("F");
        list.remove(2);
        list.removeFirst();
        list.removeLast();
        System.out.println("Содержимое после удаления: " + list);

        String val = list.get(2);
        list.set(2, val + "+");
        System.out.println("Содержимое после изменения: " + list);
    }
}

Вывод:

Содержимое: [A, A2, F, B, D, E, C, Z]
Содержимое после удаления: [A2, D, E, C]
Содержимое после изменения: [A2, D, E+, C]

Разберём по шагам:

  • add() добавляет F, B, D, E, C в конец списка в том же порядке; addLast("Z") — тоже в конец, addFirst("A") — в начало, а add(1, "A2") вставляет A2 в позицию 1 (индексация с нуля).
  • remove("F") удаляет объект F, после чего список — [A, A2, B, D, E, C, Z]; remove(2) удаляет элемент с индексом 2, то есть B. Затем removeFirst() убирает A, а removeLast() — Z.
  • get(2) возвращает E, а set(2, "E+") заменяет его, не меняя размер списка.

Сложность основных методов и их поведение на пустом списке:

Методы Сложность в LinkedList Пустой список и другие граничные случаи
add(e), addLast(e), addFirst(e), offer(e), push(e) O(1) null разрешён; add() и offer() всегда возвращают true
getFirst(), getLast(), removeFirst(), removeLast(), pop() O(1) Пустой список — NoSuchElementException
peekFirst(), peekLast(), poll(), pollLast() O(1) Пустой список — null (неотличим от сохранённого null)
get(i), set(i, e) O(n), до n/2 шагов i < 0 или i ≥ size() — IndexOutOfBoundsException: Index: 0, Size: 0
add(i, e), remove(i) O(n) на поиск + O(1) на перелинковку add(size(), e) — вставка в конец; add(i, e) при i > size() — IndexOutOfBoundsException
remove(Object), contains(), indexOf(), lastIndexOf() O(n) Элемента нет — false или -1; сравнение через equals(), null тоже ищется
size(), isEmpty() O(1) Размер хранится в поле size
listIterator(): add(), remove(), set() O(1) remove() до next() — IllegalStateException
reversed() (Java 21) O(1) — возвращает представление, а не копию Изменения представления видны в исходном списке; на Java 17 не компилируется

Методы getFirst(), removeFirst() и родственные были у LinkedList всегда (из Deque); с Java 21 они объявлены и в List через SequencedCollection (подробнее). Методы, общие для всех списков, разобраны в уроке Интерфейс List и ArrayList.

LinkedList как очередь и стек

LinkedList реализует Deque, поэтому один и тот же объект может работать как очередь FIFO (offer() в хвост, poll() из головы) и как стек LIFO (push() и pop() в голове). При этом он остаётся списком: в любой момент доступны индексы и toString() показывает элементы от головы к хвосту.

Что выведет этот код?

import java.util.LinkedList;

public class LinkedListDequeExample {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.add(1);        // в конец
        list.push(2);       // в начало, как в стек
        list.offer(3);      // в конец, как в очередь
        list.addFirst(4);   // в начало
        System.out.println(list);

        System.out.println(list.pop());
        System.out.println(list.poll());
        System.out.println(list.peekLast());
        System.out.println(list);
    }
}

Вывод:

[4, 2, 1, 3]
4
2
3
[1, 3]

Здесь легко ошибиться: push() добавляет в начало, а add() и offer() — в конец. И pop(), и poll() забирают элемент из головы, поэтому сначала вышла 4, затем 2. peekLast() только смотрит на хвост, не удаляя его. Полный перечень методов очередей и разница между парами offer/add и poll/remove — в уроке Интерфейсы Queue и Deque.

Как перебирать LinkedList и вставлять в середину

LinkedList перебирают циклом for-each, методом forEach() или явным итератором — каждый из этих способов проходит список за O(n). Цикл for с get(i) для LinkedList не подходит: каждый вызов ищет узел заново, и обход превращается в O(n²).

Отсюда и цифра из начала урока. Для списка из n элементов get(i) делает примерно min(i, n − i) шагов, а в сумме по всем индексам это около n²/4: для 100 000 элементов — около 2,5 миллиарда переходов по ссылкам. Итератор же просто запоминает текущий узел и на каждом шаге переходит к next.

Итератор нужен и для того, чтобы получить настоящую вставку и удаление за O(1): ListIterator стоит на найденном узле, и его методы add() и remove() меняют только соседние ссылки.

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class LinkedListIteratorExample {
    public static void main(String[] args) {
        List<String> list = new LinkedList<>(List.of("A", "B", "C", "D"));

        ListIterator<String> it = list.listIterator();
        while (it.hasNext()) {
            String s = it.next();
            if (s.equals("B")) {
                it.add("B2");     // вставка после B за O(1)
            } else if (s.equals("C")) {
                it.remove();      // удаление C за O(1)
            }
        }
        System.out.println(list);
    }
}

Вывод:

[A, B, B2, D]

Элемент B2 вставлен сразу после B, а C удалён — и всё за один проход, без повторного поиска позиции. Если вместо it.add() и it.remove() вызвать list.add() или list.remove() внутри цикла, следующий it.next() бросит ConcurrentModificationException. Все методы ListIterator (previous(), set(), nextIndex()) описаны в уроке Интерфейс ListIterator.

Часто задаваемые вопросы

Чем List отличается от LinkedList?

List — интерфейс, который описывает, что умеет любой список, а LinkedList — один из классов, реализующих его (наряду с ArrayList). Поэтому пишут List<String> list = new LinkedList<>();. Учтите: через переменную типа List методы Deque (push(), pollLast()) недоступны — для них переменную объявляют как Deque.

Медленно ли работает list.get(list.size() - 1) у LinkedList?

Нет: для индексов во второй половине поиск идёт от последнего узла, поэтому последний элемент находится сразу. Дольше всего ищется элемент в середине списка — до n/2 шагов. Для концов списка нагляднее getLast() и peekLast(). Как устроен поиск, показано в разделе Как работает get(index).

Потокобезопасен ли LinkedList?

Нет. При одновременной записи из нескольких потоков ссылки узлов могут разойтись и элементы потеряются. Простой вариант — обёртка Collections.synchronizedList(new LinkedList<>()), но для очереди между потоками лучше ConcurrentLinkedDeque или LinkedBlockingDeque. Обзор — в разделе Потокобезопасные коллекции Java.

Почему LinkedList разрешает null, а ArrayDeque нет?

LinkedList хранит элемент в отдельном узле, и null в поле item ничему не мешает. ArrayDeque отмечает пустые ячейки массива значением null, поэтому запрещает его. Но и в LinkedList хранить null в очереди рискованно: poll() возвращает null и для пустой очереди. Разбор — в разделе Queue и null.

Video Explanation

Prefer video format? Watch this lesson with examples and explanations.

Comments

Please log in or register to have a possibility to add comment.