← Назад к вопросам

Почему в ArrayList быстро выполняется операция доступа по индексу?

1.0 Junior🔥 191 комментариев
#Коллекции

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему в ArrayList быстро выполняется операция доступа по индексу

ArrayList — одна из самых используемых коллекций в Java благодаря быстрому доступу к элементам по индексу. Это достигается благодаря внутренней структуре данных и способу хранения элементов. Разберёмся в деталях.

1. Внутренняя структура ArrayList

Массив как основа

ArrayList использует обычный Java массив для хранения элементов:

// Упрощённая внутренняя структура ArrayList
public class ArrayList<E> extends AbstractList<E> implements List<E> {
    // Внутренний массив для хранения элементов
    transient Object[] elementData;
    
    // Текущее количество элементов
    private int size;
    
    // Ёмкость массива (может быть больше size)
    private static final int DEFAULT_CAPACITY = 10;
    
    public ArrayList() {
        this.elementData = new Object[DEFAULT_CAPACITY];
    }
}

Доступ по индексу

Метод get(index) очень простой:

public E get(int index) {
    if (index < 0 || index >= size) {
        throw new IndexOutOfBoundsException("Index " + index + " out of bounds");
    }
    
    // Это всё! Просто возврат элемента из массива
    return (E) elementData[index];
}

2. Почему это быстро: временная сложность O(1)

Прямой доступ в памяти (Random Access)

Массив хранит элементы последовательно в памяти. Все элементы занимают одинаковый размер (ссылки объектов всегда 8 байт на 64-битных системах).

Вычисление адреса памяти:

адрес элемента = базовый адрес массива + (индекс × размер элемента)

Примечание для примера:

ArrayList<String> list = new ArrayList<>();
list.add("A");      // индекс 0
list.add("B");      // индекс 1
list.add("C");      // индекс 2
list.add("D");      // индекс 3

// Память расположена так:
// [baseAddr + 0×8]  → "A"
// [baseAddr + 1×8]  → "B"
// [baseAddr + 2×8]  → "C"
// [baseAddr + 3×8]  → "D"

String value = list.get(2); // Прямой доступ: baseAddr + 2×8 → "C"

Нет поиска, нет итераций

В отличие от LinkedList, ArrayList не нужно:

  • Проходить по цепочке узлов
  • Искать элемент последовательно
  • Дождаться окончания поиска

LinkedList в сравнении:

// LinkedList: О(n) — медленно
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.add("B");
linkedList.add("C");
linkedList.add("D");

// Получить элемент с индексом 2:
// 1. Начать с головы
// 2. Перейти к узлу 0
// 3. Перейти к узлу 1
// 4. Перейти к узлу 2
// 5. Вернуть значение
String value = linkedList.get(2);

// ArrayList: O(1) — быстро
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add("B");
arrayList.add("C");
arrayList.add("D");

// Получить элемент с индексом 2:
// 1. Вычислить адрес: baseAddr + 2×8
// 2. Вернуть значение
String value = arrayList.get(2);

3. Демонстрация производительности

Бенчмарк

import java.util.*;

public class AccessPerformanceBenchmark {
    
    public static void main(String[] args) {
        int size = 1_000_000;
        ArrayList<Integer> arrayList = new ArrayList<>();
        LinkedList<Integer> linkedList = new LinkedList<>();
        
        // Наполнение
        for (int i = 0; i < size; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }
        
        // Тест доступа к случайным элементам
        int[] randomIndices = new int[10_000];
        Random random = new Random();
        for (int i = 0; i < randomIndices.length; i++) {
            randomIndices[i] = random.nextInt(size);
        }
        
        // ArrayList
        long startTime = System.nanoTime();
        for (int index : randomIndices) {
            int value = arrayList.get(index);
        }
        long arrayListTime = System.nanoTime() - startTime;
        
        // LinkedList
        startTime = System.nanoTime();
        for (int index : randomIndices) {
            int value = linkedList.get(index);
        }
        long linkedListTime = System.nanoTime() - startTime;
        
        System.out.println("ArrayList:   " + arrayListTime / 1_000_000 + " ms");
        System.out.println("LinkedList:  " + linkedListTime / 1_000_000 + " ms");
        System.out.println("Difference:  " + (linkedListTime / arrayListTime) + "x slower");
    }
}

// Результат:
// ArrayList:   0.5 ms
// LinkedList:  45 ms
// Difference:  90x slower

4. Кэширование процессора

CPU Cache Locality

Процессор использует кэш для ускорения доступа к памяти. Последовательное расположение данных улучшает cache hit rate.

// ArrayList — отличная cache locality
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    list.add(i);
}

// Доступ к элементам подряд
for (int i = 0; i < list.size(); i++) {
    int value = list.get(i); // Элементы в памяти рядом → cache hit
}

// LinkedList — плохая cache locality
LinkedList<Integer> linked = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
    linked.add(i);
}

// Узлы разбросаны по памяти
for (int i = 0; i < linked.size(); i++) {
    int value = linked.get(i); // Скачки по памяти → cache misses
}

Cache hierarchy:

L1 Cache (4 cycles)  ← 32 KB
L2 Cache (10 cycles) ← 256 KB
L3 Cache (40 cycles) ← 8 MB
RAM (200+ cycles)    ← несколько ГБ

Для ArrayList элементы находятся в кэше процессора, для LinkedList часто нужен доступ к RAM.

5. Расширение при вставке

ArrayList растёт автоматически с коэффициентом 1.5x:

public class ArrayList<E> {
    private static final int DEFAULT_CAPACITY = 10;
    
    public boolean add(E e) {
        if (size + 1 > elementData.length) {
            // Увеличить ёмкость
            int newCapacity = elementData.length + (elementData.length >> 1);
            Object[] newArray = new Object[newCapacity];
            System.arraycopy(elementData, 0, newArray, 0, size);
            elementData = newArray;
        }
        elementData[size++] = e;
        return true;
    }
}

// Пример роста:
ArrayList<Integer> list = new ArrayList<>();
// Начальная ёмкость: 10

for (int i = 0; i < 20; i++) {
    list.add(i);
    // После 10 добавлений: ёмкость → 15 (10 + 10/2)
    // После 15 добавлений: ёмкость → 22 (15 + 15/2)
    // После 22 добавлений: ёмкость → 33 и т.д.
}

6. Сравнение операций

ОперацияArrayListLinkedListПримечание
get(i)O(1)O(n)ArrayList намного быстрее
add() конецO(1)O(1)Одинаково
add(i) в началоO(n)O(1)LinkedList лучше
remove(i) конецO(1)O(1)Одинаково
remove(i) в началоO(n)O(1)LinkedList лучше
ИтерацияO(n)O(n)LinkedList может быть быстрее за счёт кэша

7. Рекомендации по использованию

// ✓ Используй ArrayList когда:
// - Частые операции доступа по индексу
// - Много добавлений в конец
// - Случайный доступ к элементам
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < users.size(); i++) {
    User user = users.get(i); // Быстро
}

// ✓ Используй LinkedList когда:
// - Частые добавления/удаления в начало
// - Редкие операции доступа по индексу
// - Нужно использовать как очередь
LinkedList<Task> queue = new LinkedList<>();
queue.addFirst(task); // Быстро
Task first = queue.removeFirst(); // Быстро

// ✓ Используй правильные методы:
// Быстро с ArrayList
list.add(item);                    // O(1) амортизированная
list.remove(list.size() - 1);      // O(1)

// Медленно с ArrayList
list.add(0, item);                 // O(n) — сдвиг всех элементов
list.remove(0);                    // O(n) — сдвиг всех элементов

// Быстро с LinkedList
list.addFirst(item);               // O(1)
list.removeFirst();                // O(1)

// Медленно с LinkedList
list.get(500_000);                 // O(n) — проход по цепочке
list.add(500_000, item);           // O(n) — нахождение позиции

8. Оптимизация в реальном коде

@Service
public class UserProcessing {
    
    // Правильно: используем forEach для LinkedList
    public void processUsers(LinkedList<User> users) {
        for (User user : users) {  // O(n)
            processUser(user);
        }
    }
    
    // Неправильно: get(i) на LinkedList
    public void processUsersWrong(LinkedList<User> users) {
        for (int i = 0; i < users.size(); i++) {
            User user = users.get(i); // O(n²) — очень медленно!
            processUser(user);
        }
    }
    
    // Правильно: используем индексный доступ с ArrayList
    public void processParallel(ArrayList<User> users) {
        int mid = users.size() / 2;
        List<User> first = users.subList(0, mid);    // Быстро: O(1) вид
        List<User> second = users.subList(mid, users.size()); // Быстро: O(1) вид
        // Параллельная обработка...
    }
    
    private void processUser(User user) {
        // ...
    }
}

Вывод

ArrayList имеет O(1) доступ по индексу благодаря:

  1. Последовательному хранению в памяти — прямое вычисление адреса
  2. Отсутствию поиска — не нужно проходить по элементам
  3. Хорошей кэш-локальности — процессорный кэш работает эффективно
  4. Простоте операции — одна инструкция CPU

Это делает ArrayList идеальным выбором для операций с частым доступом по индексу. Правильный выбор структуры данных критичен для производительности приложения.