Программирование JavaGenericsJava-разработчик, проектирующий типобезопасные API коллекций

В API параметр должен принимать коллекции списков строк, включая коллекции списков специализированных реали...

В API параметр должен принимать коллекции списков строк, включая коллекции списков специализированных реализаций. Как верхний wildcard у внешней коллекции меняет возможность записи и чтения?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Параметризация внешней коллекции как коллекции неизвестного подтипа списка строк позволяет передать, например, коллекцию ArrayList<String>, где обычная List<List<String>> этого не позволяет. Из такой коллекции можно безопасно читать элементы как List<String>, но нельзя добавлять в неё конкретный список: фактический тип элемента неизвестен.

Исторический контекст

Generics появились в Java 5, чтобы проверять типы элементов во время компиляции и уменьшить необходимость в небезопасных приведениях. При этом параметризация в Java инвариантна: List<ArrayList<String>> не является подтипом List<List<String>>, хотя ArrayList<String> является подтипом List<String>.

Wildcards добавили use-site-вариативность. Они позволяют описать не один точно известный аргумент типа, а семейство совместимых параметризаций, сохраняя безопасность операций.

Постановка проблемы

Если API принимает List<List<String>>, оно принимает только коллекцию, параметризированную именно типом List<String>. Коллекция List<ArrayList<String>> несовместима с таким параметром из-за инвариантности внешнего List.

Замена внешнего аргумента на верхний wildcard расширяет набор принимаемых значений, но ограничивает запись. Если разрешить добавлять произвольный List<String>, можно нарушить тип конкретной переданной коллекции.

Подробное решение

Тип List<? extends List<String>> означает: внешняя коллекция параметризована некоторым неизвестным типом X, где X является подтипом List<String>. Например, X может быть ArrayList<String> или LinkedList<String>.

Поэтому элемент можно прочитать как List<String>: любой допустимый X гарантированно совместим с этим типом. Но добавить конкретный список нельзя, потому что компилятор не знает, является ли X именно List<String>, ArrayList<String> или другим подтипом.

import java.util.*; List<ArrayList<String>> source = new ArrayList<>(); List<? extends List<String>> view = source; List<String> item = view.get(0); // view.add(new ArrayList<String>()); // ошибка компиляции

Запрет записи защищает инвариантность исходной коллекции. В source разрешены только ArrayList<String>, поэтому добавление произвольного List<String> могло бы быть небезопасным.

Если API должно добавлять элементы в коллекцию, нужен другой контракт, например ? super относительно типа добавляемого элемента. Если требуется и чтение, и запись именно List<String>, следует принимать точный тип List<List<String>>.

Ситуация из практики

Сервис обрабатывает наборы строк, представленные разными реализациями списка. Параметр List<List<String>> оказался слишком узким: коллекции List<ArrayList<String>> и List<LinkedList<String>> нельзя было передать без перестройки данных.

Рассматривались варианты:

  • использовать raw-тип — принимает больше значений, но отключает проверку типов и создаёт риск ClassCastException;
  • принимать List<Object> — не решает проблему инвариантности и не выражает требуемый контракт;
  • принимать List<? extends List<String>> — поддерживает разные типы вложенных списков и сохраняет безопасное чтение.

Выбран верхний wildcard. Обработчик читает каждый вложенный список через интерфейс List<String>, но не изменяет состав внешней коллекции. Это точно отражает реальные операции API и не требует небезопасных приведений.

Что кандидаты часто упускают

  1. Почему List<ArrayList<String>> нельзя присвоить List<List<String>>, если ArrayList<String> — это List<String>?

Потому что параметризация внешнего типа инвариантна. Если бы такое присваивание было разрешено, через переменную типа List<List<String>> можно было бы добавить LinkedList<String> в исходную List<ArrayList<String>>, нарушив её фактический тип.

  1. Почему из List<? extends List<String>> можно получить List<String>, но нельзя получить ArrayList<String>?

Верхняя граница гарантирует только то, что неизвестный тип элемента является подтипом List<String>. Он может быть ArrayList<String>, LinkedList<String> или другой реализацией. Поэтому безопасный статический тип чтения — List<String>, а утверждать конкретную реализацию нельзя.

  1. Можно ли безопасно изменить сам вложенный список, полученный из List<? extends List<String>>?

Да, если операция разрешена типом List<String>. Полученный элемент имеет статический тип List<String>, поэтому в него можно добавлять только строки. Ограничение относится к добавлению элементов во внешнюю коллекцию, а не к изменению уже прочитанного вложенного списка.