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

Интерфейс Collection в Java: методы и контракт

Вызов List.of("a", "b").remove("z") бросает UnsupportedOperationException, хотя удалять нечего, а Arrays.asList("a", "b").remove("z") спокойно возвращает false. Оба списка реализуют один и тот же интерфейс Collection — и оба делают это правильно. Разница в том, как реализация трактует его необязательные операции.

Интерфейс Collection (java.util.Collection<E>) — корневой интерфейс ветки списков, множеств и очередей в Collection Framework. Он расширяет Iterable<E> и объявляет операции, общие для List, Set и Queue: добавление и удаление элементов, проверку наличия, размер, перебор, преобразование в массив и в поток. Map интерфейс Collection не наследует. Место Collection в общей схеме показано в уроке Иерархия коллекций Java.

Методы интерфейса Collection

Методы интерфейса Collection делятся на три группы: изменение коллекции (add, remove, addAll, removeAll, retainAll, removeIf, clear), проверки (contains, containsAll, size, isEmpty) и перебор с преобразованием (iterator, toArray, stream). Методы, которые меняют коллекцию, возвращают true, если коллекция изменилась.

Знаки вопроса в сигнатурах вроде Collection<? extends E> — это wildcard; зачем они нужны, объясняет урок принцип PECS.

Изменение коллекции

Метод Что делает Возвращает Граничный случай
boolean add(E e) Добавляет элемент true, если коллекция изменилась Set с таким элементом вернёт false; ArrayDeque.add(null) — NullPointerException
boolean addAll(Collection<? extends E> c) Добавляет все элементы c true, если добавлен хотя бы один элемент Для HashSet, где часть элементов уже есть, всё равно true
boolean remove(Object o) Удаляет одно вхождение элемента, равного o по equals() true, если элемент найден и удалён Удаляется только первое вхождение; элемента нет — false
boolean removeAll(Collection<?> c) Удаляет все элементы, которые есть в c, включая повторы true, если что-то удалено removeAll(List.of()) ничего не меняет — false
boolean retainAll(Collection<?> c) Оставляет только элементы, которые есть в c true, если что-то удалено retainAll(List.of()) очищает коллекцию
default boolean removeIf(Predicate<? super E> filter) Удаляет элементы, для которых условие истинно (Java 8) true, если удалён хотя бы один filter == null — NullPointerException
void clear() Удаляет все элементы — List.of().clear() — UnsupportedOperationException даже для пустого списка

Проверки и размер

Метод Что делает Граничный случай
boolean contains(Object o) Проверяет, есть ли элемент, равный o по equals() List.of(1).contains(null) — NullPointerException
boolean containsAll(Collection<?> c) Проверяет, есть ли все элементы c Для пустой c всегда true; повторы в c не учитываются
int size() Возвращает число элементов Больше Integer.MAX_VALUE элементов — вернёт Integer.MAX_VALUE
boolean isEmpty() Возвращает true, если элементов нет Читается лучше, чем size() == 0, и в некоторых коллекциях быстрее

Перебор, массивы и потоки

Метод Что делает Граничный случай
Iterator<E> iterator() Возвращает итератор для обхода Порядок обхода задаёт реализация: у HashSet он не гарантирован
default void forEach(Consumer<? super T> action) Выполняет действие для каждого элемента (метод Iterable, Java 8) Изменение коллекции внутри действия — ConcurrentModificationException
Object[] toArray() Создаёт новый массив Object[] Приведение к String[] — ClassCastException
<T> T[] toArray(T[] a) Создаёт массив нужного типа (или заполняет a, если он достаточного размера) Несовместимый тип элементов — ArrayStoreException
default <T> T[] toArray(IntFunction<T[]> generator) Создаёт массив через генератор, например String[]::new (Java 11) Тот же ArrayStoreException при несовместимом типе
default Stream<E> stream() Возвращает последовательный поток (Java 8) Поток не меняет исходную коллекцию
default Stream<E> parallelStream() Возвращает поток, который может выполняться параллельно (Java 8) На маленьких коллекциях обычно медленнее stream()
default Spliterator<E> spliterator() Возвращает «разделяемый итератор», на котором построены потоки (Java 8) Напрямую вызывается редко

Ещё два метода объявлены в Collection заново, чтобы уточнить их контракт: boolean equals(Object o) и int hashCode(), который возвращает хеш-код коллекции. Как они работают, разобрано в разделе Как Collection сравнивает элементы.

Пример: add, remove, removeAll, retainAll

Чтобы понять методы Collection, проще всего следить за их возвращаемым значением: true означает, что коллекция изменилась, false — что осталась прежней. Переменная объявлена типом Collection, поэтому в примере доступны только методы этого интерфейса, без индексов.

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

public class CollectionMethodsExample {
    public static void main(String[] args) {
        Collection<String> names = new ArrayList<>(List.of("Анна", "Олег", "Анна", "Ира"));

        System.out.println(names.add("Олег"));                          // true
        System.out.println(names.remove("Анна"));                       // true
        System.out.println(names);                                      // [Олег, Анна, Ира, Олег]
        System.out.println(names.removeAll(List.of("Олег", "Пётр")));   // true
        System.out.println(names);                                      // [Анна, Ира]
        System.out.println(names.retainAll(List.of("Анна", "Ира")));    // false
        System.out.println(names.containsAll(List.of("Ира", "Анна")));  // true
        System.out.println(names.addAll(List.of()));                    // false
        names.clear();
        System.out.println(names.isEmpty() + " " + names.size());       // true 0

        Collection<String> tags = new HashSet<>(List.of("java"));
        System.out.println(tags.add("java"));                           // false
        System.out.println(tags.addAll(List.of("java", "sql")));        // true
    }
}

Вывод:

true
true
[Олег, Анна, Ира, Олег]
true
[Анна, Ира]
false
true
false
true 0
false
true

Обратите внимание на три момента. remove("Анна") удалил только первую «Анну», а removeAll() убрал оба «Олега». Отсутствующий «Пётр» не помешал removeAll() вернуть true. addAll() для множества вернул true, хотя добавилась только строка «sql»: метод сообщает, изменилась ли коллекция, а не то, добавились ли все элементы. Те же методы на ArrayList, а также методы с индексами разобраны в уроке Интерфейс List и класс ArrayList.

Метод removeIf: удаление по условию

Метод removeIf(Predicate) удаляет из коллекции все элементы, для которых условие возвращает true, и возвращает true, если удалён хотя бы один. Он появился в Java 8 как default-метод и заменяет ручной цикл с итератором: одна строка вместо пяти и без риска получить ConcurrentModificationException.

Collection<Integer> numbers = new ArrayList<>(List.of(5, 12, 7, 20, 3));

System.out.println(numbers.removeIf(n -> n > 10));   // true
System.out.println(numbers);                         // [5, 7, 3]
System.out.println(numbers.removeIf(n -> n > 100));  // false — ничего не удалено

Если удалять элементы вызовом numbers.remove(n) внутри цикла for-each, следующий шаг цикла обычно падает с ConcurrentModificationException. Почему так происходит и как удалять через Iterator.remove(), показано в уроке Iterator.

Метод toArray: три способа получить массив

Метод toArray() копирует элементы коллекции в новый массив. Вариантов три: toArray() возвращает Object[], toArray(T[] a) — массив нужного типа, а toArray(IntFunction) из Java 11 создаёт массив по ссылке на конструктор, например String[]::new.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class ToArrayExample {
    public static void main(String[] args) {
        Collection<String> langs = new ArrayList<>(List.of("Java", "Kotlin"));

        Object[] objects = langs.toArray();
        String[] strings = langs.toArray(new String[0]);
        String[] strings11 = langs.toArray(String[]::new);   // Java 11+
        System.out.println(Arrays.toString(strings11));       // [Java, Kotlin]

        String[] wrong = (String[]) langs.toArray();          // ClassCastException
    }
}

Вывод:

[Java, Kotlin]
Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')

toArray() у ArrayList создаёт именно Object[], и привести его к String[] нельзя, даже если внутри только строки. Если тип элементов не подходит под тип массива, toArray(new String[0]) бросит ArrayStoreException. Пример с массивом заранее заданного размера — в уроке Получение массива из ArrayList.

Массив новый, объекты те же

Массив из toArray() не связан с коллекцией: замена array[0] = ... коллекцию не изменит. Но элементы не копируются — в массив попадают ссылки на те же объекты. Если положить в список new StringBuilder("a"), вызвать ((StringBuilder) list.toArray()[0]).append("!") и вывести список, получится [a!]. Для неизменяемых String и Integer разница незаметна, для изменяемых объектов — важна.

Перебор: iterator, forEach и stream

Любую коллекцию можно перебрать тремя способами: циклом for-each (он работает, потому что Collection расширяет Iterable), методом forEach() с лямбдой и через поток stream(). Итератор iterator() нужен явно, когда при обходе элементы удаляются.

Collection<String> words = List.of("set", "list", "queue");

words.forEach(System.out::println);
long longWords = words.stream()
        .filter(w -> w.length() > 3)
        .count();
System.out.println("Длиннее трёх букв: " + longWords);

Вывод:

set
list
queue
Длиннее трёх букв: 2

stream() не меняет исходную коллекцию: filter() отбирает элементы только для дальнейшей обработки. Чтобы удалить элементы из самой коллекции, нужен removeIf(). Методы hasNext(), next(), remove() и ListIterator разобраны в уроке Интерфейс Iterator.

Необязательные операции и UnsupportedOperationException

Методы, которые меняют коллекцию (add, addAll, remove, removeAll, retainAll, removeIf, clear), в документации Collection помечены как необязательные (optional operations). Реализация вправе их не поддерживать и бросать UnsupportedOperationException. Так устроены неизменяемые коллекции: List.of(), Collections.unmodifiableCollection(), а для операций изменения размера — Arrays.asList().

Вернёмся к примеру из начала урока:

import java.util.Arrays;
import java.util.List;

public class OptionalOperationsExample {
    public static void main(String[] args) {
        List<String> fixed = Arrays.asList("a", "b");
        List<String> immutable = List.of("a", "b");

        System.out.println(fixed.remove("z"));       // false
        System.out.println(immutable.remove("z"));   // исключение
    }
}

Вывод:

false
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)

Документация разрешает обе реакции: если вызов ничего не изменил бы, необязательная операция может бросить исключение, но не обязана. Список Arrays.asList() ищет элемент и бросает исключение, только когда действительно нужно удалить: fixed.remove("a") закончится UnsupportedOperationException. Коллекции из List.of() отказывают сразу, не глядя на аргумент. То же различие видно у removeIf(): на Arrays.asList(1, 2, 3) условие n -> n > 5 вернёт false, а n -> n > 1 бросит исключение.

Тип переменной не гарантирует изменяемость

По объявлению Collection<String> c нельзя понять, поддерживает ли объект add(). Если метод получает коллекцию извне и собирается её менять, надёжнее скопировать её: new ArrayList<>(c). Код, который «работает» на тестах с ArrayList, упадёт в продакшене, как только ему передадут List.of(...).

Сравнение способов создать неизменяемую коллекцию (List.of, List.copyOf, Collections.unmodifiableList, stream().toList()) — в разделе Неизменяемые коллекции, а почему Arrays.asList() связан с исходным массивом — в уроке Backed Collections.

Как Collection сравнивает элементы: equals и hashCode

Методы contains(), remove(), containsAll(), removeAll() и retainAll() ищут элементы через equals(): элемент найден, если o.equals(e) возвращает true (для null — если e == null). Поэтому для собственных классов, которые хранятся в коллекциях, нужно переопределять метод equals(), иначе сравниваются ссылки.

class PointClass {
    int x, y;
    PointClass(int x, int y) { this.x = x; this.y = y; }
}

record Point(int x, int y) {}   // equals() и hashCode() генерируются автоматически

Collection<PointClass> a = new ArrayList<>(List.of(new PointClass(1, 2)));
Collection<Point> b = new ArrayList<>(List.of(new Point(1, 2)));

System.out.println(a.contains(new PointClass(1, 2)));  // false — разные объекты
System.out.println(b.contains(new Point(1, 2)));       // true

Хеш-коллекции (HashSet, ключи HashMap) сначала сравнивают hashCode(), и только потом equals(), поэтому там методы переопределяют в паре. Контракт hashCode() и equals() разобран в уроке Интерфейс Set и HashSet.

Когда две коллекции равны

Сам Collection не задаёт правило равенства коллекций — его уточняют подинтерфейсы. Два List равны, если содержат равные элементы в том же порядке, независимо от класса. Два Set равны, если содержат одни и те же элементы. Список никогда не равен множеству. Что выведет этот код?

List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3));
List<Integer> linkedList = new LinkedList<>(List.of(1, 2, 3));
Set<Integer> hashSet = new HashSet<>(List.of(1, 2, 3));
Set<Integer> treeSet = new TreeSet<>(List.of(3, 2, 1));
Deque<Integer> deque1 = new ArrayDeque<>(List.of(1, 2, 3));
Deque<Integer> deque2 = new ArrayDeque<>(List.of(1, 2, 3));

System.out.println(arrayList.equals(linkedList));
System.out.println(hashSet.equals(treeSet));
System.out.println(arrayList.equals(hashSet));
System.out.println(deque1.equals(deque2));

Вывод:

true
true
false
false

Последняя строка удивляет чаще всего. ArrayDeque не является ни List, ни Set и не переопределяет equals(), поэтому две очереди с одинаковыми элементами сравниваются по ссылке. То же относится к PriorityQueue и к коллекции map.values(). Хеш-код тоже зависит от подинтерфейса: у List он вычисляется с учётом порядка элементов, у Set равен сумме хеш-кодов элементов.

Как менялся Collection по версиям Java

Интерфейс Collection появился в Java 1.2 вместе со всем Collection Framework. С тех пор его расширяли default-методами: они имеют реализацию прямо в интерфейсе, поэтому новые методы добавились без поломки старых классов, которые реализуют Collection.

Версия Что изменилось
Java 1.2 Интерфейс Collection и абстрактные методы: add, remove, contains, size, iterator, toArray и другие
Java 5 Интерфейс стал обобщённым: Collection<E>; родителем стал Iterable, отсюда цикл for-each
Java 8 Default-методы removeIf(), stream(), parallelStream(), spliterator(); forEach() в Iterable
Java 11 Default-метод toArray(IntFunction<T[]>)
Java 21 Подинтерфейс SequencedCollection с getFirst(), getLast(), reversed() — см. урок Иерархия коллекций

Методы конкретных ветвей иерархии разобраны в отдельных уроках: List (доступ по индексу), Set (уникальные элементы), Queue и Deque (очереди и стек). Что такое список, стек, очередь и множество как структуры данных, объясняет урок Структуры данных в Java.

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

Почему contains() и remove() принимают Object, а не E?

Потому что элемент ищется через equals(), а по контракту равными могут быть объекты разных классов. Обратная сторона — компилятор не поймает ошибку типа: для List<Long> ids = List.of(1L, 2L) вызов ids.contains(1) скомпилируется и вернёт false, потому что 1 упаковывается в Integer, а Integer не равен Long. Правильно — ids.contains(1L).

Почему в Collection нет метода get(int index)?

У множества и очереди нет позиций элементов, поэтому индекс есть только в подинтерфейсе List. Чтобы взять «первый» элемент произвольной коллекции, используют iterator().next() или stream().findFirst(); для HashSet «первый» при этом не определён заранее. Методы с индексами — в уроке Интерфейс List.

Как написать свою коллекцию, не реализуя все методы Collection?

Унаследоваться от абстрактного класса AbstractCollection и реализовать два метода: iterator() и size(). Остальные методы (contains, toArray, containsAll, toString) уже написаны на их основе. Для изменяемой коллекции нужно ещё переопределить add() и метод remove() у итератора, иначе они бросают UnsupportedOperationException.

Что вернёт addAll(), если добавились не все элементы?

true, если добавился хотя бы один элемент: метод сообщает, изменилась ли коллекция. Для HashSet, где часть элементов уже была, результат тоже true, а false будет, только когда не добавилось ничего. Пример с выводом — в разделе Пример: add, remove, removeAll, retainAll.

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

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

Комментарии

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