Найти в Дзене
Записки о Java

Почему нужно писать List list = new ArrayList<>();, а не ArrayList list = new ArrayList<>();

Краткий ответ: потому что вы программируете против абстракции, а не против конкретной реализации. Это делает ваш код гибким, тестируемым и устойчивым к изменениям. И да — это напрямую связано с принципами SOLID, особенно с Принципом подстановки Барбары Лисков (LSP) и Принципом инверсии зависимостей (DIP). Давайте разберёмся подробно — с примерами, теорией и практическими последствиями. В Java интерфейс (List, Map, Set) — это контракт: он описывает, что объект может делать, но не как он это делает. А реализация (ArrayList, LinkedList, HashMap) — это конкретный способ выполнения этого контракта. Когда вы пишете: List<String> names = new ArrayList<>(); — вы говорите: «Мне нужен объект, который умеет делать всё, что обещает List. Как именно он это делает — меня пока не волнует». А когда вы пишете: ArrayList<String> names = new ArrayList<>(); — вы привязываетесь к конкретной реализации. Вы говорите: «Мне нужен именно ArrayList, и только он». На первый взгляд — без разницы. Но на практике э
Оглавление
Рисунок: реализации List в Java
Рисунок: реализации List в Java

Программирование на интерфейсах — не просто правило, а философия гибкого кода

Краткий ответ: потому что вы программируете против абстракции, а не против конкретной реализации. Это делает ваш код гибким, тестируемым и устойчивым к изменениям. И да — это напрямую связано с принципами SOLID, особенно с Принципом подстановки Барбары Лисков (LSP) и Принципом инверсии зависимостей (DIP).

Давайте разберёмся подробно — с примерами, теорией и практическими последствиями.

🧱 Что такое «программирование на интерфейсах»?

В Java интерфейс (List, Map, Set) — это контракт: он описывает, что объект может делать, но не как он это делает.

А реализация (ArrayList, LinkedList, HashMap) — это конкретный способ выполнения этого контракта.

Когда вы пишете:

List<String> names = new ArrayList<>();

— вы говорите:

«Мне нужен объект, который умеет делать всё, что обещает List. Как именно он это делает — меня пока не волнует».

А когда вы пишете:

ArrayList<String> names = new ArrayList<>();

— вы привязываетесь к конкретной реализации. Вы говорите:

«Мне нужен именно ArrayList, и только он».

На первый взгляд — без разницы. Но на практике эта разница огромна.

Пример: почему это важно на практике?

Представьте, что вы пишете метод, который обрабатывает список пользователей:

❌ Плохой вариант — зависимость от реализации

public void processUsers(ArrayList<User> users) {

for (User user : users) {

// обработка

}

}

Теперь представьте, что где-то в коде у вас есть LinkedList<User>:

LinkedList<User> usersFromQueue = getUserQueue();

processUsers(usersFromQueue); // ❌ ОШИБКА КОМПИЛЯЦИИ!

Почему? Потому что LinkedList — это не ArrayList, хотя оба реализуют List.

Вы намеренно ограничили свой метод, хотя логика обработки абсолютно одинакова для любого списка!

✅ Хороший вариант — зависимость от интерфейса

public void processUsers(List<User> users) {

for (User user : users) {

// обработка

}

}

Теперь вы можете передавать любой список:

processUsers(new ArrayList<>()); // ✅

processUsers(new LinkedList<>()); // ✅

processUsers(Arrays.asList(user1, user2)); // ✅

Ваш код стал универсальным.

еория: как это связано с SOLID?

Да, это напрямую связано с двумя принципами SOLID:

1. LSP — Принцип подстановки Барбары Лисков

«Объекты в программе должны быть заменимы на экземпляры их подтипов без изменения корректности программы».

Если ваш метод принимает List, то любой подтип List (включая ArrayList, Vector, CopyOnWriteArrayList) должен работать без изменений.
Но если вы требуете ArrayList — вы
нарушаете LSP, потому что подтипы не взаимозаменяемы.

2. DIP — Принцип инверсии зависимостей

«Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций».

Ваш бизнес-код («обработка пользователей») — это модуль верхнего уровня.
ArrayList — это деталь реализации (низкоуровневый модуль).
Зависимость от List — это зависимость от
абстракции, а не от детали.

👉 DIP говорит: зависьте от интерфейсов, а не от классов.

💡 Дополнительные преимущества

1. Легче писать unit-тесты

Вы можете легко подменить реализацию на mock или stub:

@Test

void testProcessUsers() {

List<User> mockList = Mockito.mock(List.class);

when(mockList.size()).thenReturn(1);

service.processUsers(mockList); // ✅ работает

}

Если бы метод принимал ArrayList — вы не смогли бы передать mock.

2. Гибкость при рефакторинге

Завтра вы решите, что ArrayList не подходит — слишком много вставок в начало.
Вы меняете
одну строку:

List<String> data = new LinkedList<>(); // вместо ArrayList

И всё остальное — работает без изменений.

Если бы вы использовали ArrayList везде — пришлось бы менять десятки мест.

3. Семантическая ясность

Когда вы пишете List, вы говорите:

«Мне важны операции списка: добавление, итерация, доступ по индексу».

Когда вы пишете ArrayList, вы намекаете:

«Мне важна именно внутренняя структура — массив».

Но в 95% случаев вам не важна реализация — важен контракт.

Заключение

Программирование на интерфейсах — это не «правило хорошего тона». Это практический инструмент, который:

  • снижает связность (coupling),
  • повышает переиспользуемость,
  • упрощает тестирование,
  • делает код устойчивым к изменениям.

И да — это часть SOLID, особенно DIP и LSP. Так что, когда вы пишете List list = new ArrayList<>(), вы не просто следуете конвенции — вы строите архитектуру, которая выдержит время.