Подготовка к алгоритмическим задачам

Собеседование на Java-разработчика: 30 вопросов с разбором
Как устроено собеседование
Техническое интервью на Java обычно состоит из четырёх блоков: ядро языка, коллекции, многопоточность и JVM. Для позиций middle и выше добавляется Spring и вопросы про архитектуру. Алгоритмическая секция может быть отдельной, а может встроиться в живое кодирование.
Важное наблюдение: почти все вопросы ниже задают не ради самого факта, а чтобы понять, различаете ли вы как работает и как этим пользоваться. Ответ «HashMap быстрый» ничего не стоит, ответ «HashMap даёт O(1) в среднем за счёт хеширования, но деградирует до O(log n) при коллизиях, потому что с восьмого элемента корзина превращается в красно-чёрное дерево» — стоит.
Блок 1. Ядро языка
Чем отличается == от equals
== для примитивов сравнивает значения, для объектов — ссылки. equals сравнивает содержимое, если метод переопределён; в базовой реализации Object.equals делает то же самое, что ==.
Классическая ловушка:
String a = "hello"; String b = "hello"; System.out.println(a == b); // true — обе ссылки из пула строк String c = new String("hello"); System.out.println(a == c); // false — новый объект в куче System.out.println(a.equals(c)); // true
Похожая история с целыми числами: Integer кэширует значения от −128 до 127, поэтому Integer.valueOf(100) == Integer.valueOf(100) даёт true, а для 1000 — уже false. Этот пример любят на собеседованиях.
Контракт equals и hashCode
Если переопределяете equals, обязаны переопределить hashCode. Правило: равные объекты обязаны иметь равные хеш-коды. Обратное неверно — разные объекты могут случайно совпасть по хешу, это коллизия.
Что будет, если нарушить:
class Point { int x, y; // переопределили только equals @Override public boolean equals(Object o) { /* ... */ } } Set<Point> set = new HashSet<>(); set.add(new Point(1, 2)); set.contains(new Point(1, 2)); // false!
HashSet сначала ищет корзину по хеш-коду. У двух объектов хеши разные (наследуются от Object и завязаны на адрес), поэтому поиск уходит не в ту корзину, и equals даже не вызывается.
Что такое immutable-объект и зачем он нужен
Объект, состояние которого нельзя изменить после создания. Канонический пример — String. Чтобы сделать свой класс неизменяемым: пометить поля final, не давать сеттеров, класс объявить final (чтобы наследник не сломал инвариант), а изменяемые поля копировать при входе и выходе.
Ценность в том, что такие объекты автоматически потокобезопасны и их можно свободно кэшировать и использовать как ключи в мапе.
Решай алгоритмические задачи как профи

Checked и unchecked исключения
Checked наследуются от Exception и обязаны быть либо обработаны, либо объявлены в сигнатуре. Unchecked наследуются от RuntimeException и таких требований не несут.
Полезно иметь мнение по этому поводу: checked-исключения — спорная особенность Java, которой нет в большинстве языков. На практике они часто приводят к пустым catch-блокам. Многие современные фреймворки, включая Spring, оборачивают их в unchecked.
Блок 2. Коллекции
Как устроен HashMap
Самый частый вопрос на всём собеседовании. Ответ по шагам:
Внутри — массив корзин. При вставке считается hashCode ключа, к нему применяется дополнительное перемешивание битов, и по остатку от деления на размер массива определяется корзина.
При коллизии элементы складываются в связный список внутри корзины. Начиная с восьми элементов в одной корзине (и при размере таблицы не меньше 64) список превращается в красно-чёрное дерево — так поиск в худшем случае деградирует до O(log n), а не до O(n).
Когда заполненность превышает load factor (по умолчанию 0.75), таблица увеличивается вдвое, и все элементы перераспределяются.
Итого: O(1) в среднем, O(log n) в худшем.
Чем ArrayList отличается от LinkedList
ArrayList — динамический массив. Доступ по индексу O(1), вставка в середину O(n) из-за сдвига. LinkedList — двусвязный список. Вставка при наличии итератора O(1), доступ по индексу O(n).
Честный ответ, который ценится: на практике ArrayList быстрее почти всегда, даже там, где теория обещает обратное. Причина — локальность данных и кеш процессора. LinkedList оправдан в основном как Deque.
Чем ConcurrentHashMap лучше синхронизированной мапы
Collections.synchronizedMap вешает блокировку на всю мапу — параллельные чтения выстраиваются в очередь. ConcurrentHashMap блокирует только отдельную корзину при записи, а чтения вообще не блокирует. На конкурентной нагрузке разница огромная.
Дополнительно: ConcurrentHashMap не допускает null ни в ключах, ни в значениях — иначе было бы невозможно отличить «ключа нет» от «значение null» без внешней синхронизации.
Fail-fast и fail-safe итераторы
Итераторы обычных коллекций fail-fast: если коллекцию изменили во время обхода, бросается ConcurrentModificationException. Итераторы конкурентных коллекций fail-safe — работают со снимком и исключения не бросают, но могут не увидеть свежих изменений.
Удалять элемент во время обхода нужно через iterator.remove() либо через removeIf.
Блок 3. Многопоточность
Разница между процессом и потоком, и зачем нужен пул
Потоки живут в общей памяти одного процесса, поэтому переключение между ними дешевле, но требует синхронизации. Создание потока — дорогая операция, поэтому в реальном коде используют пулы через ExecutorService.
Что делает ключевое слово volatile
Гарантирует две вещи: видимость (запись одним потоком сразу видна остальным, значение не кэшируется в регистре) и запрет на переупорядочивание операций вокруг обращения.
Чего volatile не даёт — атомарности составных операций. Классический контрпример:
volatile int counter = 0; counter++; // НЕ атомарно: чтение, инкремент, запись
Для счётчика нужен AtomicInteger или synchronized.
synchronized против ReentrantLock
synchronized — встроен в язык, блокировка снимается автоматически при выходе из блока, в том числе при исключении. ReentrantLock — гибче: поддерживает tryLock с таймаутом, прерываемое ожидание, честную очередь и несколько условий через Condition. Платой идёт обязательный unlock в блоке finally.
Правило простое: если хватает synchronized, берите его.
Что такое deadlock и как его избежать
Взаимная блокировка: поток A держит ресурс 1 и ждёт ресурс 2, поток B держит ресурс 2 и ждёт ресурс 1. Основной способ профилактики — всегда захватывать блокировки в одном и том же порядке. Дополнительно помогают таймауты через tryLock.
Стоит знать и соседние понятия: livelock (потоки активны, но не продвигаются) и starvation (поток не получает доступ из-за более приоритетных).
Зачем нужен CompletableFuture
Позволяет описывать асинхронные цепочки без блокирующего get(). Комбинаторы thenApply, thenCompose, thenCombine, allOf дают возможность собрать граф зависимых задач. На интервью полезно упомянуть, что для CPU-задач подойдёт общий ForkJoinPool, а для блокирующего ввода-вывода нужен отдельный пул, иначе можно исчерпать общий.
Блок 4. JVM и память
Как устроена память JVM
Куча (heap) — объекты, делится на молодое и старое поколения. Стек — свой у каждого потока, хранит фреймы вызовов и локальные переменные примитивов. Metaspace — метаданные классов, живёт вне кучи (с Java 8 заменил PermGen).
Отсюда следуют два разных исключения: OutOfMemoryError при нехватке кучи и StackOverflowError при слишком глубокой рекурсии.
Как работает сборщик мусора
Основа — гипотеза о поколениях: большинство объектов умирает молодыми. Поэтому куча делится на Eden, два Survivor-пространства и Old Generation. Новые объекты попадают в Eden, пережившие сборку переезжают в Survivor, а пережившие несколько циклов — в Old.
Сборщик находит недостижимые объекты, идя от корней (GC roots): статические поля, локальные переменные на стеках, ссылки из JNI.
Полезно ориентироваться в сборщиках: G1 — по умолчанию с Java 9, ориентирован на предсказуемые паузы; ZGC и Shenandoah — с околонулевыми паузами на больших кучах; Parallel — максимальная пропускная способность в ущерб паузам.
Что такое JIT-компиляция
Байт-код сначала интерпретируется, а горячие участки компилируются в машинный код на лету. Отсюда эффект прогрева: первые вызовы метода медленнее последующих. Это критично при бенчмаркинге — измерять надо после прогрева, иначе результаты бессмысленны.
Блок 5. Spring (для middle и выше)
Что такое IoC и Dependency Injection
Инверсия управления: объект не создаёт свои зависимости сам, их предоставляет контейнер. Это упрощает подмену реализаций и тестирование.
Из трёх способов внедрения предпочтителен конструктор: он позволяет сделать поля final, делает зависимости явными и не даёт создать объект в неполном состоянии. Внедрение в поле через @Autowired удобно, но скрывает зависимости и мешает тестам.
Области видимости бинов
singleton (по умолчанию) — один экземпляр на контейнер. prototype — новый на каждый запрос. В веб-приложениях также request, session, application.
Классическая ловушка: если внедрить prototype-бин в singleton напрямую, он создастся один раз и дальше будет вести себя как singleton. Решается через ObjectProvider или @Lookup.
Как работает @Transactional
Через прокси. Отсюда два ограничения, про которые обязательно спросят: аннотация не работает на приватных методах и не работает при вызове метода изнутри того же класса — вызов идёт напрямую, минуя прокси.
По умолчанию откат происходит только на unchecked-исключениях. Для checked нужно явно указать rollbackFor.
Как готовиться
Разберитесь в устройстве, а не в фактах. Вопрос про HashMap задают не чтобы услышать «это мапа», а чтобы понять глубину. Умение рассказать про корзины, коллизии и переход в дерево отличает кандидата, который читал документацию, от того, кто читал шпаргалку.
Готовьте примеры из своего опыта. На вопрос про многопоточность гораздо сильнее звучит «у нас была гонка в кеше, мы её нашли так-то и починили через ConcurrentHashMap.computeIfAbsent», чем пересказ учебника.
Не бойтесь сказать «не знаю». Признать незнание и предложить, как бы вы это выяснили, — лучше, чем выдумывать. Выдуманный ответ интервьюер распознаёт мгновенно, и дальше доверие ко всем остальным ответам падает.
Тренируйте алгоритмы отдельно. Знание языка и умение решать задачи — разные навыки. Хеш-таблицы, два указателя, обход графов и динамическое программирование дадут основную массу задач на живом кодировании.
Что запомнить
- Ядро:
equals/hashCodeв паре, immutable-объекты, разница checked и unchecked. - Коллекции: устройство
HashMap— самый частый вопрос всего интервью. - Многопоточность:
volatileдаёт видимость, но не атомарность. - JVM: поколения объектов, GC roots, эффект прогрева JIT.
- Spring: внедрение через конструктор, ограничения проксирования в
@Transactional.
