$ sudo teach IT
МОДУЛЬ 10 · УРОК 1

ArrayList — Динамический список

Знакомимся с ArrayList — самой используемой коллекцией в Java. Узнаем, почему обычные массивы ограничены и как ArrayList решает эти проблемы

~30 минут Для новичков Java

Почему обычные массивы не всегда удобны

Давайте начнём с простого вопроса: зачем вообще нужны коллекции, если уже есть массивы? Массивы — это фундамент языка, и они работают отлично. Но у них есть одно серьёзное ограничение, которое вы быстро заметите на практике.

Представьте, что вы ведёте список покупок в блокноте. Выписали 5 продуктов. Но потом вспомнили ещё один. В блокноте просто допишете строку. А в массиве? Массив в Java имеет фиксированный размер. Если вы объявили массив на 5 элементов, вы не можете добавить в него шестой — Java выдаст ошибку.

Вот как это выглядит в коде:

String[] fruits = new String[3];
fruits[0] = "Яблоко";
fruits[1] = "Банан";
fruits[2] = "Вишня";
fruits[3] = "Груша"; // ArrayIndexOutOfBoundsException!

Массив размером 3 может вместить только 3 элемента. Попытка обратиться к индексу 3 приводит к исключению. Да, можно создать новый массив большего размера и скопировать туда данные, но это утомительно и неудобно.

Аналогия: массив — это как коробка с фиксированным числом ячеек. Вы купили коробку на 3 банки — и всё, шестую уже не поставите. А ArrayList — это как телескопическая полка, которая сама расширяется, когда вам нужно больше места.

Кроме фиксированного размера, у массивов есть и другие неудобства: нельзя легко удалить элемент из середины, нет встроенных методов для поиска, сортировки, фильтрации. Именно для решения всех этих проблем и была создана коллекция ArrayList.

Что такое ArrayList

ArrayList — это класс из Java Collections Framework, который реализует интерфейс List. Под капотом он использует обычный массив, но с важным отличием: когда массив заполняется, ArrayList автоматически создаёт новый массив большего размера и копирует туда все элементы. Вы этого не замечаете — ArrayList просто «умеет расти».

Представьте книжный шкаф. Если он полон, вы просто берёте шкаф побольше и переставляете книги. ArrayList делает то же самое, только автоматически и очень быстро.

Вот основные преимущества ArrayList перед обычным массивом:

  • Динамический размер — список сам растёт и уменьшается по мере необходимости
  • Удобные методы — добавление, удаление, поиск, сортировка встроены
  • Работа с объектами — хранит ссылки на объекты, а не сами значения (хотя для примитивов есть обёртки)
  • Типизация — можно указать, какой именно тип данных будет храниться
  • Интеграция — работает со Stream API, методами коллекций, итераторами и многим другим

Создание ArrayList

Создать ArrayList можно несколькими способами. Давайте рассмотрим каждый из них подробно.

Способ 1: Пустой список

Самый простой способ — создать пустой список, в который вы будете добавлять элементы по мере необходимости:

ArrayList<String> names = new ArrayList<>();

Обратите внимание на угловые скобки <String> — это параметр типа (generic). Он говорит компилятору, что в этом списке будут храниться только строки. Если вы попытаетесь добавить число или другой объект, компилятор выдаст ошибку.

В Java 7 и позже можно использовать菱形 operator — два угловых скобки без типа внутри. Компилятор сам выведет тип из левой части:

ArrayList<String> names = new ArrayList<>();

Это то же самое, просто короче.

Способ 2: С начальной ёмкостью

Если вы примерно знаете, сколько элементов будет в списке, можно указать начальную ёмкость. Это оптимизация — ArrayList не будет пересоздавать массив при первых additions:

ArrayList<Integer> numbers = new ArrayList<>(100);

Это не значит, что список будет содержать 100 элементов. Он будет пустым, но под капотом уже будет выделен массив на 100 мест.

Способ 3: Из другой коллекции

Можно создать ArrayList, скопировав элементы из другой коллекции:

List<String> original = Arrays.asList("A", "B", "C");
ArrayList<String> copy = new ArrayList<>(original);

Это полезно, когда нужно создать изменяемую копию неизменяемого списка.

Способ 4: Метод List.of()

Начиная с Java 9, можно создавать неизменяемые списки одним вызовом, а затем обернуть в ArrayList:

ArrayList<String> names = new ArrayList<>(List.of("Анна", "Борис", "Вика"));

Важно: List.of() создаёт неизменяемый список. Если вы попытаетесь добавить в него элемент, получите UnsupportedOperationException. Поэтому для динамической работы лучше использовать new ArrayList<>.

Добавление элементов: метод add()

Главный метод для добавления элементов — add(). Он работает просто: ставит элемент в конец списка. Вот как это выглядит:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> fruits = new ArrayList<>();

        fruits.add("Яблоко");
        fruits.add("Банан");
        fruits.add("Вишня");

        System.out.println(fruits);
    }
}

Результат: [Яблоко, Банан, Вишня]. Обратите внимание — мы вызвали add() три раза, и ArrayList каждый раз увеличивался. Никаких проблем с размером!

Метод add() можно вызвать и с указанием индекса — тогда элемент вставится не в конец, а в указанную позицию, а остальные элементы сдвинутся вправо:

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");

fruits.add(1, "Груша");

System.out.println(fruits);
// [Яблоко, Груша, Банан, Вишня]

Мы вставили «Грушу» на позицию 1 (второй элемент, потому что индексация начинается с 0). «Банан» и «Вишня» автоматически сместились на одну позицию вправо.

Метод add() возвращает boolean (true), если список изменился. Это может пригодиться в некоторых ситуациях, но чаще всего возвращаемое значение просто игнорируют.

Можно добавить сразу несколько элементов, используя addAll():

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");

ArrayList<String> moreFruits = new ArrayList<>();
moreFruits.add("Банан");
moreFruits.add("Вишня");

fruits.addAll(moreFruits);

System.out.println(fruits);
// [Яблоко, Банан, Вишня]

Получение элементов: метод get()

Чтобы получить элемент по индексу, используется метод get(). Он работает так же, как обращение по индексу в массиве, но с вызовом метода:

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");

String first = fruits.get(0);
String second = fruits.get(1);
String third = fruits.get(2);

System.out.println(first);   // Яблоко
System.out.println(second);  // Банан
System.out.println(third);   // Вишня

Как и в массивах, индексация начинается с 0. Первый элемент имеет индекс 0, второй — 1, третий — 2.

Если указать несуществующий индекс, получите исключение IndexOutOfBoundsException:

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");

String item = fruits.get(5); // IndexOutOfBoundsException!

Чтобы узнать размер списка, используйте метод size():

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");

System.out.println(fruits.size()); // 2

У ArrayList нет метода length, как у массива. Используйте size(). Это частая ошибка новичков — путают .length массива и .size() коллекции.

Изменение элементов: метод set()

Если нужно заменить элемент на определённой позиции, используйте метод set(). Он принимает два аргумента: индекс и новое значение:

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");

System.out.println("До: " + fruits);

fruits.set(1, "Манго");

System.out.println("После: " + fruits);
// До: [Яблоко, Банан, Вишня]
// После: [Яблоко, Манго, Вишня]

Метод set() возвращает старое значение, которое было на этой позиции. Это может быть полезно, если нужно запомнить, что было до замены:

String old = fruits.set(1, "Манго");
System.out.println("Было: " + old); // Было: Банан

Удаление элементов: метод remove()

Удаление элементов — одна из сильных сторон ArrayList. Есть два варианта удаления: по индексу и по объекту.

Удаление по индексу:

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");

String removed = fruits.remove(1);

System.out.println("Удалено: " + removed); // Удалено: Банан
System.out.println("Осталось: " + fruits); // Осталось: [Яблоко, Вишня]

Метод remove(int index) удаляет элемент по индексу и возвращает удалённый элемент. После удаления все элементы правее сдвигаются влево.

Удаление по объекту:

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");

boolean wasRemoved = fruits.remove("Банан");

System.out.println("Удалено: " + wasRemoved); // Удалено: true
System.out.println("Осталось: " + fruits);    // Осталось: [Яблоко, Вишня]

Метод remove(Object obj) удаляет первый найденный элемент, равный переданному объекту. Он возвращает boolean — true, если элемент был найден и удалён, false — если не найден.

Осторожно! Если ArrayList содержит числа (например, ArrayList<Integer>), вызов remove(1) удалит элемент по индексу 1, а не число 1. Чтобы удалить число, нужно явно привести: remove(Integer.valueOf(1)). Это классическая ловушка для новичков.

Другие полезные методы

Помимо основных CRUD-операций, ArrayList предоставляет множество других полезных методов. Давайте рассмотрим самые важные из них.

Проверка наличия элемента — contains() и indexOf()

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");

boolean hasBanana = fruits.contains("Банан");
System.out.println("Есть банан: " + hasBanana); // Есть банан: true

boolean hasGrape = fruits.contains("Виноград");
System.out.println("Есть виноград: " + hasGrape); // Есть виноград: false

int index = fruits.indexOf("Вишня");
System.out.println("Индекс вишни: " + index); // Индекс вишни: 2

int notFound = fruits.indexOf("Манго");
System.out.println("Индекс манго: " + notFound); // Индекс манго: -1

Метод contains() возвращает true/false. Метод indexOf() возвращает индекс первого вхождения или -1, если элемент не найден.

Проверка пустоты — isEmpty()

ArrayList<String> fruits = new ArrayList<>();
System.out.println(fruits.isEmpty()); // true

fruits.add("Яблоко");
System.out.println(fruits.isEmpty()); // false

Очистка списка — clear()

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");

System.out.println("Размер: " + fruits.size()); // 2

fruits.clear();

System.out.println("Размер: " + fruits.size()); // 0
System.out.println("Пустой: " + fruits.isEmpty()); // true

Сортировка — Collections.sort()

Сам ArrayList не имеет метода sort(), но его можно отсортировать с помощью класса Collections:

import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> fruits = new ArrayList<>();
        fruits.add("Вишня");
        fruits.add("Яблоко");
        fruits.add("Банан");

        System.out.println("До сортировки: " + fruits);

        Collections.sort(fruits);

        System.out.println("После сортировки: " + fruits);
    }
}

Результат: До сортировки: [Вишня, Яблоко, Банан] → После сортировки: [Банан, Вишня, Яблоко]. Элементы отсортированы в алфавитном порядке.

Переворот списка — Collections.reverse()

import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> fruits = new ArrayList<>();
        fruits.add("Яблоко");
        fruits.add("Банан");
        fruits.add("Вишня");

        Collections.reverse(fruits);

        System.out.println(fruits); // [Вишня, Банан, Яблоко]
    }
}

Случайное перемешивание — Collections.shuffle()

import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> fruits = new ArrayList<>();
        fruits.add("Яблоко");
        fruits.add("Банан");
        fruits.add("Вишня");

        Collections.shuffle(fruits);

        System.out.println(fruits); // Случайный порядок
    }
}

Типизация: Generic в ArrayList

Generic (параметры типа) — это механизм Java, который позволяет указать, какой тип данных будет храниться в коллекции. Без generic ArrayList может хранить что угодно, но тогда при извлечении элемента придётся делать приведение типа вручную.

// Без generic — можно добавить что угодно
ArrayList unsafe = new ArrayList();
unsafe.add("Строка");
unsafe.add(42);
unsafe.add(3.14);

// При извлечении нужно приведение типа
String s = (String) unsafe.get(0); // ОК
Integer n = (Integer) unsafe.get(1); // ОК
String x = (String) unsafe.get(2); // ClassCastException!

С generic компилятор проверяет типы на этапе компиляции:

ArrayList<String> safe = new ArrayList<>();
safe.add("Строка");
safe.add(42); // Ошибка компиляции!

Вы можете использовать ArrayList с любым ссылочным типом:

ArrayList<Integer> numbers = new ArrayList<>();
ArrayList<Double> decimals = new ArrayList<>();
ArrayList<Boolean> flags = new ArrayList<>();
ArrayList<Object> anything = new ArrayList<>();

class Car {
    String brand;
    int year;

    Car(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }
}

ArrayList<Car> cars = new ArrayList<>();

Для примитивных типов (int, double, boolean и т.д.) используйте обёртки: Integer, Double, Boolean. Java автоматически преобразует примитивы в объекты (autoboxing) и обратно (unboxing).

Автоупаковка и распаковка

Java не может хранить примитивные типы (int, double, boolean и т.д.) напрямую в коллекциях, потому что коллекции работают только с объектами. Для решения этой проблемы Java использует механизм автоупаковки (autoboxing) и распаковки (unboxing).

Autoboxing — автоматическое преобразование примитива в объект-обёртку:

ArrayList<Integer> numbers = new ArrayList<>();

numbers.add(42); // Автоупаковка: int → Integer

double pi = 3.14;
ArrayList<Double> decimals = new ArrayList<>();
decimals.add(pi); // Автоупаковка: double → Double

ArrayList<Boolean> flags = new ArrayList<>();
flags.add(true); // Автоупаковка: boolean → Boolean

Unboxing — автоматическое преобразование объекта-обёртки обратно в примитив:

ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(42);

int first = numbers.get(0); // Распаковка: Integer → int
int sum = first + 10; // 52

Вот полная таблица соответствий:

Примитив Обёртка
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

Автоупаковка работает не только при добавлении, но и при арифметических операциях:

ArrayList<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);

int a = list.get(0); // Unboxing
int b = list.get(1); // Unboxing
int sum = a + b;     // 30

list.add(sum);        // Autoboxing: int → Integer

System.out.println(list); // [10, 20, 30]

Практический пример: список студентов

Давайте соберём всё вместе и создадим программу для управления списком студентов. Это покажет, как ArrayList используется в реальном коде.

import java.util.ArrayList;
import java.util.Collections;

public class StudentList {
    public static void main(String[] args) {
        ArrayList<String> students = new ArrayList<>();

        students.add("Анна");
        students.add("Борис");
        students.add("Вика");
        students.add("Дмитрий");
        students.add("Елена");

        System.out.println("Все студенты: " + students);
        System.out.println("Количество: " + students.size());

        students.add(2, "Григорий");
        System.out.println("После добавления: " + students);

        students.remove("Вика");
        System.out.println("После удаления: " + students);

        students.set(0, "Анна К.");
        System.out.println("После изменения: " + students);

        System.out.println("Есть ли Дмитрий? " + students.contains("Дмитрий"));
        System.out.println("Индекс Елены: " + students.indexOf("Елена"));

        Collections.sort(students);
        System.out.println("В алфавитном порядке: " + students);

        students.clear();
        System.out.println("После очистки: " + students);
        System.out.println("Пустой? " + students.isEmpty());
    }
}

Эта программа демонстрирует все основные операции с ArrayList: добавление, удаление, изменение, поиск, сортировку и очистку.

Сравнение ArrayList и массива

Давайте наглядно сравним ArrayList и обычный массив, чтобы понять, когда что использовать:

Свойство Массив ArrayList
РазмерФиксированныйДинамический
Добавление в конецНужно знать размерadd()
Вставка в серединуРучное копированиеadd(index, elem)
УдалениеРучное копированиеremove()
ПоискЦикл вручнуюcontains(), indexOf()
СортировкаРеализовать вручнуюCollections.sort()
ПроизводительностьБыстрее (нет накладных расходов)Немного медленнее
ПамятьКомпактнееБольше overhead

Правило выбора: Если размер известен и не меняется (например, дни недели, координаты точки), используйте массив. Если количество элементов может меняться или нужно работать с коллекциями — используйте ArrayList.

Вывод на печать ArrayList

Когда вы печатаете ArrayList с помощью System.out.println(), он автоматически вызывает метод toString(), который выводит элементы в квадратных скобках через запятую:

ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");

System.out.println(fruits);
// Вывод: [Яблоко, Банан, Вишня]

Для красивого вывода можно использовать цикл for-each или Stream API (мы разберём их позже):

for (String fruit : fruits) {
    System.out.println("Фрукт: " + fruit);
}
// Фрукт: Яблоко
// Фрукт: Банан
// Фрукт: Вишня

Полный пример программы

Давайте создадим более сложную программу, которая демонстрирует все основные возможности ArrayList:

import java.util.ArrayList;
import java.util.Collections;

public class ArrayListDemo {
    public static void main(String[] args) {
        ArrayList<Integer> grades = new ArrayList<>();

        grades.add(85);
        grades.add(92);
        grades.add(78);
        grades.add(95);
        grades.add(88);

        System.out.println("Оценки: " + grades);
        System.out.println("Размер: " + grades.size());

        int sum = 0;
        for (int grade : grades) {
            sum += grade;
        }
        double average = (double) sum / grades.size();
        System.out.println("Средний балл: " + average);

        System.out.println("Максимальная оценка: " + Collections.max(grades));
        System.out.println("Минимальная оценка: " + Collections.min(grades));

        grades.remove(Integer.valueOf(78));
        System.out.println("После удаления 78: " + grades);

        grades.add(0, 100);
        System.out.println("После добавления 100 в начало: " + grades);

        Collections.sort(grades);
        System.out.println("По возрастанию: " + grades);

        Collections.reverse(grades);
        System.out.println("По убыванию: " + grades);

        System.out.println("Индекс оценки 92: " + grades.indexOf(92));
        System.out.println("Содержит 95? " + grades.contains(95));
    }
}

Итоги урока

  • ArrayList — динамический список, который автоматически растёт и уменьшается
  • Создаётся через new ArrayList<Type>() с указанием типа данных
  • add(elem) — добавляет элемент в конец, add(index, elem) — вставляет по индексу
  • get(index) — получает элемент по индексу
  • set(index, elem) — заменяет элемент на указанной позиции
  • remove(index) или remove(object) — удаляет элемент
  • size() — возвращает количество элементов (не length!)
  • contains(), indexOf(), isEmpty(), clear() — дополнительные полезные методы
  • Generic <Type> обеспечивает типобезопасность на этапе компиляции
  • Автоупаковка (autoboxing) автоматически преобразует примитивы в объекты-обёртки
  • ArrayList удобнее массива для динамических данных, но массив быстрее для статических

Тест по ArrayList

5 вопросов

ArrayList basics

Premium