Почему нужно возвращать интерфейс List вместо конкретной реализации ArrayList в Java
Что проверяют
Понимаете ли вы, что публичная сигнатура метода это контракт, который потом дорого менять. Вопрос про интерфейсы, но по ответу видно, думаете ли вы о совместимости кода.
Как отвечать
Тип возвращаемого значения это обещание вызывающему коду. Если метод объявлен как ArrayList<User>, вы пообещали не просто «список», а конкретную реализацию, и смена её на LinkedList или на неизменяемый список станет ломающим изменением для всех, кто вызывает метод.
Объявив List<User>, вы оставляете себе свободу: внутри можно вернуть ArrayList, результат List.of(...), Collections.unmodifiableList(...) или список из стрима, и ни один вызывающий класс не придётся править.
Дополнительные аргументы, которые стоит назвать:
- инкапсуляция: вызывающий видит поведение, а не устройство, и не может опереться на детали вроде
ensureCapacity; - тестируемость: параметры и возвращаемые типы по интерфейсам проще подменять в тестах;
- принцип инверсии зависимостей: код опирается на абстракцию, а не на реализацию.
Тот же принцип применяется к параметрам метода, причём даже строже: принимать стоит самый общий тип, который вам достаточен, например Collection<T> или Iterable<T>.
Есть и обратная сторона, о ней полезно сказать самому. Иногда конкретный тип в сигнатуре осмыслен: ArrayList обещает доступ по индексу за константу, а EnumMap и TreeMap дают гарантии, ради которых их и выбирают. Если гарантия является частью контракта, честнее объявить её типом, чем прятать.
Где спотыкаются
Возвращают List, но отдают изменяемый внутренний список объекта. Наружу уезжает ссылка на состояние, и его меняют мимо вашего кода. Возвращайте копию или неизменяемое представление.
Путают уровни: интерфейс в сигнатуре не мешает выбрать любую реализацию внутри, это решение по производительности принимается отдельно.
Отвечают «так требует SOLID» без объяснения, какую конкретно проблему это решает.