Зачем ArrayDeque запрещает добавление null, если интерфейс Collection в целом допускает null-элементы?
ArrayDeque запрещает null, чтобы однозначно различать пустую очередь и извлечённое значение. Методы poll, peek и некоторые другие возвращают null, когда операция не может вернуть элемент; разрешение null сделало бы результат неоднозначным.
Java Collections Framework разделяет общий контракт коллекций и специализированные контракты очередей. Для очередей Deque предусмотрены пары методов: одни сообщают о невозможности операции исключением, другие используют специальное возвращаемое значение, например null.
Такой API удобен для безопасной проверки состояния без обработки исключения, но требует, чтобы специальное значение не было обычным элементом очереди. Поэтому реализация ArrayDeque выбирает инвариант: null не является допустимым элементом.
Если бы очередь одновременно разрешала хранить null и возвращала null при отсутствии элемента, результат poll() нельзя было бы интерпретировать однозначно. Нельзя было бы понять, извлечён ли настоящий null или очередь была пуста.
Кроме того, ArrayDeque использует массив и циклические индексы для хранения элементов. Запрет null согласуется с внутренним представлением: пустые позиции массива могут обозначаться значением null, что упрощает поддержание инвариантов структуры.
При добавлении null методы добавления ArrayDeque, такие как add или offer, завершаются NullPointerException. Это не случайное ограничение конкретного метода, а свойство реализации: null не может находиться среди элементов дека.
ArrayDeque хранит элементы в циклическом массиве: начало и конец дека перемещаются по индексам, а при достижении границы переходят к началу массива. Добавление и удаление с обоих концов обычно имеют сложность O(1), а расширение массива выполняется редко, поэтому амортизированная сложность таких операций остаётся O(1).
Запрет null не означает, что все очереди обязаны вести себя так же. Например, некоторые реализации могут допускать null, но тогда вызывающий код должен учитывать неоднозначность методов, возвращающих специальное значение. Для строгой логики предпочтительны методы с исключением (remove, element) либо явная модель результата.
Сервис формирует очередь задач, где null ошибочно используется как признак «задача пока не создана». Рассматривались три варианта.
LinkedList допускает null, но poll() не позволяет отличить пустую очередь от извлечённого null; это повышает риск скрытых ошибок. Использование исключающих методов делает ошибку заметнее, но требует обработки исключений и не устраняет проблему некорректной модели данных.
ArrayDeque<Task> с запретом null сразу обнаруживает нарушение инварианта на границе системы. Был выбран этот вариант: входные данные валидируются до помещения в очередь, а отсутствие задачи представляется отдельным состоянием. В результате ошибка обнаруживается при формировании сообщения, а не позже при обработке очереди.
Если отсутствие значения действительно является частью предметной области, можно использовать Deque<Optional<Task>>, помещая только ненулевые объекты Optional. Это явно выражает состояние, но добавляет обёртки и усложняет код, поэтому применять такой подход стоит только при реальной необходимости.
offer(null) и add(null) в ArrayDeque?Нет. Оба метода не принимают null, но согласно контракту операции добавления в ArrayDeque завершаются NullPointerException. Различие между add и offer относится прежде всего к поведению при невозможности добавить элемент из-за ограничений ёмкости: add обычно сообщает об этом исключением, а offer возвращает false. Для ArrayDeque ёмкость динамическая, поэтому типичный случай отказа связан именно с недопустимым null.
poll() == null корректна для ArrayDeque?Потому что в ArrayDeque гарантированно нет элемента null. Следовательно, возвращённый null однозначно означает, что дек пуст. Такая проверка была бы недостаточной для реализации, разрешающей null, поскольку там одинаковый результат мог бы означать как пустую коллекцию, так и настоящий элемент.
Не всегда. Помимо различия в допустимости null, реализации имеют разные характеристики памяти и производительности: ArrayDeque использует массив, а LinkedList — отдельные связанные узлы. Операции на концах у обеих структур имеют постоянную асимптотику, но ArrayDeque обычно лучше использует память и локальность данных. Замена также может изменить поведение кода, который полагается на запрет null или на обнаружение ошибочного значения через исключение.