ArrayList
В прошлом уроке список рос сам, и размер ему никто не задавал. Но внутри ArrayList лежит обычный массив, а массив расти не умеет. Как же это устроено?
Вернёмся в гардероб. Шапки лежат на полке с ячейками. Когда свободные ячейки кончаются, гардеробщик берёт полку побольше, перекладывает на неё все шапки по порядку, а старую убирает. ArrayList поступает так же со своим массивом.
Но сначала посмотрим, что умеет список:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> shelf = new ArrayList<>();
shelf.add("hat");
shelf.add("scarf");
shelf.add("gloves");
shelf.set(1, "cap");
System.out.println(shelf); // [hat, cap, gloves]
System.out.println(shelf.get(2)); // gloves
System.out.println(shelf.contains("scarf")); // false
}
}Давайте разберём программу по строкам:
- Три вызова
addкладут"hat","scarf"и"gloves"в конец списка по очереди. Они получают индексы0,1и2. shelf.set(1, "cap")заменяет элемент с индексом1: место"scarf"занимает"cap".System.out.println(shelf)печатает[hat, cap, gloves].shelf.get(2)читает элемент с индексом2, то естьgloves.shelf.contains("scarf")проверяет, есть ли в списке такой элемент, и отвечаетfalse: шарф уже заменили. Элементы он сравнивает черезequals.
Индекс за границей списка останавливает программу, как и у массива: shelf.get(3) бросит исключение IndexOutOfBoundsException.
Как растёт ArrayList
Внутри ArrayList лежит массив. Его длину называют ёмкостью списка, а число занятых ячеек — размером. Это разные числа: в массиве почти всегда есть запас.
Вот что происходит в JDK 25:
new ArrayList<>()пока не выделяет места под элементы.- Первый
addсоздаёт массив на 10 ячеек. - Пока есть свободная ячейка,
addкладёт элемент в неё, и это быстро. - Когда ячейки кончились, следующий
addсоздаёт новый массив, примерно в полтора раза длиннее, копирует в него все элементы и добавляет новый.
Поэтому ёмкость растёт так: 10, 15, 22, 33, 49. Копирование случается редко, и в среднем добавление в конец остаётся быстрым. Как растёт ArrayList — частый вопрос на собеседовании.
Вставка в начало или в середину стоит дороже. add(0, x) кладёт элемент в начало и сдвигает все остальные на одну ячейку вправо. remove по индексу, наоборот, сдвигает влево все элементы после удалённого. В списке из миллиона элементов это до миллиона перекладываний.
Здесь картинка с полкой неточна. Настоящий гардеробщик переносит шапки у всех на глазах, а переменная shelf переезда не замечает. Она по-прежнему ведёт к тому же объекту ArrayList, а новый массив он держит внутри себя.
remove по индексу и по значению
У списка два метода remove:
remove(int index)удаляет элемент с этим индексом;remove(Object o)ищет первый элемент, равныйoпоequals, и удаляет его.
В списке строк путаницы нет: shelf.remove("cap") может означать только второй метод. А в List<Integer> оба подходят, и здесь часто ошибаются:
List<Integer> nums = new ArrayList<>();
nums.add(7);
nums.add(5);
nums.add(1);
nums.remove(1);
System.out.println(nums); // [7, 1]
nums.remove(Integer.valueOf(1));
System.out.println(nums); // [7]Разберём оба вызова:
- В
nums.remove(1)аргумент —int. Компилятор сначала ищет метод, который подходит без автоупаковки, и находитremove(int index). Удаляется элемент с индексом1, то есть5. Integer.valueOf(1)— уже объект, поэтому вызываетсяremove(Object o). Он находит число1и удаляет его.
Частая ошибка
Из списка баллов хотели убрать число 70:
List<Integer> scores = new ArrayList<>();
scores.add(50);
scores.add(70);
scores.add(90);
scores.remove(70); // ✗Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 70 out of bounds for length 3Программа компилируется, но останавливается при запуске. Java поняла 70 как индекс, а элемента с индексом 70 в списке из трёх нет. Чтобы удалить значение, передайте объект: scores.remove(Integer.valueOf(70)).
Главное
ArrayListхранит элементы в массиве:addкладёт в конец,getиsetчитают и меняют по индексу,sizeиcontainsотвечают о содержимом.- Когда массив заполнен,
ArrayListсоздаёт новый, примерно в полтора раза длиннее (10, 15, 22, 33…), и копирует в него элементы. add(0, x)иremoveпо индексу сдвигают все элементы после этого места.- У
List<Integer>вызовremove(1)удаляет элемент с индексом 1, а число 1 удаляют черезremove(Integer.valueOf(1)).