Вопросы на собеседовании Java-разработчика с ответами
Подборка помогает объяснять решения и обсуждать компромиссы на Java-интервью. Уровень вопроса — ориентир глубины разговора; требования конкретной команды определяются вакансией.
Java 21. Описание @Transactional относится к proxy-режиму Spring Framework 6.2; SQL-задание проверяет транзакционную семантику отдельно от Spring.
Вопросы и ответы
Junior: основы
ОбъектыКак связаны equals и hashCode?
equals определяет логическое равенство, hashCode помогает хеш-коллекции выбрать область поиска. Для равных объектов хеши должны совпадать; обратное неверно: одинаковый хеш ещё не означает равенство. Переопределяя equals, обычно нужно согласованно переопределить hashCode. Поля, участвующие в обоих методах, должны отражать одну и ту же идентичность. Симметричность, транзитивность и согласованность равенства важны для предсказуемого поведения коллекций.
- Пример
- Два объекта пользователя с одинаковым неизменяемым идентификатором могут считаться равными независимо от имени.
- Частая ошибка
- Переопределить equals и оставить несовместимый hashCode.
- Уточнение интервьюера
- Что сломается, если изменить поле ключа после помещения в HashMap?
ПараметрыJava передаёт объекты по ссылке?
Java передаёт аргументы по значению. Для объекта этим значением является ссылка. Метод получает копию ссылки и может изменить доступный объект, но переприсваивание параметра не заменяет переменную вызывающего кода. Такой ответ снимает кажущееся противоречие между изменением списка внутри метода и невозможностью написать обычный swap для двух внешних ссылок. Неизменяемые объекты дополнительно ограничивают наблюдаемые изменения через общую ссылку.
- Пример
- Добавление в переданный ArrayList видно вызывающему коду; присваивание параметру нового ArrayList — нет.
- Частая ошибка
- Говорить «всё передаётся по ссылке», не различая объект и переменную.
- Уточнение интервьюера
- Почему обмен значениями двух параметров не меняет переменные вызывающего кода?
СтрокиПочему String неизменяемый?
Созданная строка не позволяет менять свою последовательность символов. Операции преобразования возвращают результат, а не редактируют исходный объект. Это делает совместное использование строк предсказуемым и позволяет безопасно применять строку как ключ. Однако частая конкатенация в цикле может создавать промежуточные результаты; для построения большого текста полезен StringBuilder. Неизменяемость строки не делает потокобезопасным контейнер, в котором хранят ссылку на неё.
- Пример
- s.toUpperCase() не изменяет s, если результат никуда не присвоить.
- Частая ошибка
- Сравнивать содержимое String через ==.
- Уточнение интервьюера
- Когда StringBuilder уместнее конкатенации нескольких констант?
КоллекцииКак выбрать List, Set или Map?
List хранит последовательность и допускает повторы, Set представляет уникальные элементы, Map связывает ключи со значениями. Далее выбирают конкретную реализацию по порядку обхода, операциям и требованиям конкурентности. Нельзя выводить гарантированный порядок HashMap из того, как он выглядит в одном запуске. TreeMap требует согласованного сравнения ключей, LinkedHashMap сохраняет определённый порядок. Контракт интерфейса важнее привычки всегда использовать один класс.
- Пример
- Для упорядоченного набора уникальных посещённых адресов подходит LinkedHashSet.
- Частая ошибка
- Выбирать структуру только по средней сложности поиска, забыв порядок и обновления.
- Уточнение интервьюера
- Чем отличается equals элементов от Comparator в сортированной коллекции?
ПроизводительностьЧем ArrayList отличается от LinkedList?
ArrayList хранит элементы в массиве и даёт быстрый доступ по индексу; вставка в середину требует сдвига. LinkedList состоит из узлов, но поиск нужного индекса требует обхода. Поэтому утверждение «вставка в LinkedList всегда быстрее» неверно без указания уже найденной позиции. На практике важны также дополнительные объекты, память и локальность данных. Для очереди с двух концов часто уместнее ArrayDeque.
- Пример
- Если задача постоянно читает элементы по индексу, LinkedList обычно не соответствует её основным операциям.
- Частая ошибка
- Сравнивать только стоимость изменения ссылок, игнорируя поиск позиции.
- Уточнение интервьюера
- Как измерить поведение коллекций на реальной нагрузке?
ИсключенияЧем checked exception отличается от unchecked?
Checked-исключение требует обработки либо объявления в сигнатуре; unchecked-наследники RuntimeException не имеют такого требования компилятора. Это различие не говорит, насколько ошибка серьёзна и стоит ли её игнорировать. Контракт API должен объяснять, что вызывающий может сделать при отказе. При преобразовании ошибки сохраняйте cause. Широкий catch с пустым телом превращает понятный сбой в повреждённое состояние или неверный успешный результат.
- Пример
- Ошибка чтения файла может быть частью ожидаемого контракта, а неверный индекс — ошибкой использования API.
- Частая ошибка
- Считать все RuntimeException невосстановимыми, а все checked — безопасными.
- Уточнение интервьюера
- Где лучше преобразовать инфраструктурную ошибку в доменную?
РесурсыДля чего нужен try-with-resources?
Конструкция автоматически закрывает ресурсы, реализующие AutoCloseable, при выходе из области. Несколько ресурсов закрываются в обратном порядке. Если ошибка возникла и в работе, и при закрытии, дополнительная ошибка может оказаться suppressed, поэтому при диагностике нельзя смотреть только короткое сообщение. Управление ресурсом не заменяет понимания транзакции: закрытое соединение и успешно зафиксированные изменения — разные факты.
- Пример
- Поток чтения файла нужно закрыть и при неудачном разборе содержимого.
- Частая ошибка
- Полагаться на сборщик мусора для своевременного закрытия файлов и соединений.
- Уточнение интервьюера
- Как получить исключения, подавленные во время закрытия?
ТестированиеЧто делает unit-тест полезным?
Тест фиксирует наблюдаемый контракт на понятном примере. Он должен различать правильное поведение и вероятную ошибочную реализацию. Проверяйте результат, побочные эффекты и конкретный тип отказа, если он входит в контракт. Избыточная проверка порядка внутренних вызовов мешает безопасному рефакторингу. Для времени, сети и случайности полезны контролируемые зависимости. Один большой тест со множеством несвязанных assertions затрудняет понимание причины падения.
- Пример
- Проверка равенства ключей должна также показать успешный поиск в HashMap по другому равному экземпляру.
- Частая ошибка
- Мокировать саму проверяемую логику и утверждать, что mock вернул заданный результат.
- Уточнение интервьюера
- Какая проверка нужна дополнительно к unit-тесту репозитория?
Middle: применение
ТипизацияЧто означает PECS для generics?
Правило Producer Extends, Consumer Super помогает выбирать границы wildcard. Если структура только отдаёт значения как T, часто подходит ? extends T; если принимает T, полезен ? super T. Это не делает произвольную запись безопасной: из списка неизвестного подтипа нельзя заключить, что он принимает любой базовый тип. Для структуры, которая и читает, и пишет один конкретный тип, может быть нужен обычный параметр T без wildcard.
- Пример
- Из List<? extends Number> можно читать Number, но добавлять произвольный Integer нельзя.
- Частая ошибка
- Считать List<Integer> подтипом List<Number>.
- Уточнение интервьюера
- Как задать сигнатуру копирования элементов между двумя списками?
КоллекцииПочему изменяемый ключ опасен для HashMap?
При добавлении HashMap использует хеш ключа и его равенство. Если затем изменить поле, участвующее в hashCode или equals, поиск может пойти не в ту область, где запись оказалась изначально. Объект физически остаётся в коллекции, но обычный get может его не найти. Надёжнее использовать неизменяемый идентификатор или неизменяемый value object. Изменение других полей допустимо только если оно не затрагивает контракт идентичности.
- Пример
- Ключ с полем email потеряет ожидаемое поведение после смены email, если по нему рассчитывается hashCode.
- Частая ошибка
- Ожидать, что HashMap автоматически перестроит индекс после изменения ключа.
- Уточнение интервьюера
- Какие ограничения record помогают соблюдать для ключей?
Обработка данныхЧем Stream отличается от коллекции?
Коллекция хранит элементы, Stream описывает обработку источника. Промежуточные операции обычно ленивые, а вычисление запускает терминальная операция. Stream не предназначен для повторного использования после терминальной операции. Функции преобразования желательно делать без зависимых побочных эффектов: особенно это важно для параллельного выполнения. parallelStream не гарантирует ускорение и требует учитывать источник, объём, стоимость операции и конкуренцию за общий пул.
- Пример
- filter(...).map(...) без терминальной операции ещё не сформировал список результата.
- Частая ошибка
- Изменять общий ArrayList из параллельного forEach.
- Уточнение интервьюера
- Когда обычный цикл будет понятнее pipeline?
JVMЧто находится в heap и stack?
У каждого потока есть стек вызовов с кадрами выполняемых методов; объекты обычно рассматриваются как размещённые в общей куче. При этом JIT может оптимизировать размещение и даже устранить создание некоторых объектов, поэтому описание языка не нужно путать с конкретным машинным представлением. Утечка памяти в Java возможна, когда ненужные объекты остаются достижимыми. Сборщик мусора освобождает недостижимое, но не знает, какие записи кеша больше не нужны бизнесу.
- Пример
- Безграничная map со ссылками на старые результаты удерживает их даже при работающем GC.
- Частая ошибка
- Утверждать, что наличие GC полностью исключает утечки памяти.
- Уточнение интервьюера
- Какие данные помогут отличить утечку от кратковременного роста нагрузки?
МногопоточностьЧто гарантирует volatile?
volatile задаёт правила видимости и упорядочивания доступа к переменной, но не превращает составную операцию в атомарную. Выражение counter++ включает чтение, вычисление и запись, поэтому два потока могут потерять обновление. Для счётчика используют подходящую атомарную операцию, а для инварианта нескольких полей — общую синхронизацию. Выбирать средство нужно по защищаемому состоянию, а не по принципу «добавим volatile ко всем полям».
- Пример
- Флаг остановки можно читать через volatile; обновление связанной пары балансов требует более сильного механизма.
- Частая ошибка
- Принимать видимость за атомарность read-modify-write.
- Уточнение интервьюера
- Когда AtomicInteger не решит задачу корректности состояния?
МногопоточностьЧто защищает synchronized?
Монитор обеспечивает взаимное исключение для потоков, использующих один и тот же монитор, и необходимые отношения видимости. Если чтение защищено одной блокировкой, а запись другой, общего критического участка не получается. Длинные сетевые операции внутри блокировки увеличивают ожидание остальных потоков. Для нескольких блокировок нужен согласованный порядок захвата, иначе возможна взаимная блокировка. Само ключевое слово не выбирает корректную границу бизнес-операции.
- Пример
- Проверку остатка и списание нужно защитить вместе, если они образуют единый инвариант.
- Частая ошибка
- Синхронизировать только запись и читать изменяемое состояние без согласованного механизма.
- Уточнение интервьюера
- Как обнаружить и воспроизвести deadlock?
АсинхронностьЧем thenApply отличается от thenCompose?
thenApply преобразует успешный результат функцией. Если функция возвращает ещё один CompletionStage, получится вложенная структура; thenCompose разворачивает такую последовательную асинхронную зависимость. Независимые результаты можно объединять thenCombine. Также нужно выбрать executor для тяжёлой работы, поведение при ошибке и общий таймаут. Название метода с Async не гарантирует, что блокирующий вызов стал дешёвым или безопасным для общего пула.
- Пример
- Загрузить пользователя, затем его настройки — последовательность; загрузить две независимые справочные таблицы — объединение.
- Частая ошибка
- Вызывать join сразу после каждого запуска и тем самым последовательно ждать независимые операции.
- Уточнение интервьюера
- Как вернуть резервный результат только для определённого вида ошибки?
SpringКак внедрение зависимостей помогает тестированию?
Объект получает зависимости извне, поэтому их создание отделено от бизнес-логики. Конструктор делает обязательные зависимости явными и позволяет собрать сервис в тесте без полного приложения. Контейнер Spring управляет созданием и связыванием объектов, но не отменяет необходимость понятных границ. Слишком много зависимостей у сервиса может указывать на смешение обязанностей. Не всякий unit-тест требует запуска Spring context.
- Пример
- Сервис заказа можно проверить с контролируемым репозиторием и часами, переданными в конструктор.
- Частая ошибка
- Обращаться к глобальному контейнеру внутри каждого метода вместо явного контракта.
- Уточнение интервьюера
- Когда нужен интеграционный тест со Spring context?
Senior: компромиссы и надёжность
Spring и БДПочему @Transactional может не сработать?
В обычном proxy-режиме Spring транзакционный перехват выполняется при вызове через прокси. Внутренний вызов метода того же объекта может обойти этот механизм. Также важны выбранный transaction manager, граница операции и правила rollback. По умолчанию для распространённой конфигурации runtime-ошибки приводят к rollback, но поведение checked-исключений нужно задавать и проверять явно. Версию Spring и режим конфигурации следует указывать: аннотация не является универсальной магией вокруг любого вызова.
- Пример
- Если внешний сервис вызывает транзакционный метод через управляемый bean, перехват возможен; this.save() внутри того же объекта — иной путь.
- Частая ошибка
- Считать, что наличие аннотации само доказывает открытие транзакции.
- Уточнение интервьюера
- Как протестировать rollback на реальной тестовой базе?
МногопоточностьКогда полезны виртуальные потоки Java 21?
Виртуальные потоки уменьшают стоимость большого числа одновременно ожидающих задач и позволяют сохранить последовательный стиль блокирующего кода. Они не увеличивают производительность процессора и не снимают ограничения внешнего ресурса. Если база допускает десятки соединений, тысячи ожидающих задач всё равно должны учитывать этот предел. Для Java 21 отдельно проверяют места, где блокирующая операция удерживает carrier thread, и поведение библиотек. Решение подтверждают нагрузочным тестом.
- Пример
- Много независимых запросов с I/O может выиграть от дешёвого ожидания, но вычисление хеша большого файла требует CPU.
- Частая ошибка
- Считать виртуальный поток бесплатным без ограничений по памяти, соединениям и очередям.
- Уточнение интервьюера
- Как ограничить нагрузку на внешний сервис при большом числе потоков?
КонсистентностьКак защититься от конкурентного обновления данных?
Выбор зависит от инварианта и частоты конфликтов. Оптимистический подход проверяет версию записи и при конфликте предлагает повтор или отказ. Пессимистическая блокировка сериализует доступ, но требует коротких транзакций и контроля порядка захвата. Уникальный индекс защищает ограничения лучше, чем отдельная предварительная проверка в коде. Повтор транзакции должен учитывать уже выполненные внешние эффекты: письмо или платёж нельзя автоматически повторять вместе с запросом базы.
- Пример
- Два запроса бронирования последнего места не должны оба пройти только потому, что каждый сначала прочитал остаток 1.
- Частая ошибка
- Решать межпроцессную гонку synchronized в одном экземпляре приложения.
- Уточнение интервьюера
- Когда достаточно UPDATE с условием, а когда нужна блокировка строки?
ДиагностикаКак расследовать рост задержек JVM-сервиса?
Сначала сравните пропускную способность, распределение задержек, ошибки и очереди с обычным периодом. Затем проверьте паузы GC, использование heap, загрузку CPU, состояние потоков и ожидание внешних сервисов. Увеличение heap может скрыть симптом, но не устранить удержание ненужных объектов. Среднее время ответа скрывает редкие долгие операции, поэтому нужны перцентили и корреляция с конкретными запросами. Изменения проверяйте на сопоставимой нагрузке.
- Пример
- Растущая очередь пула соединений объяснит задержки даже при низком CPU приложения.
- Частая ошибка
- Менять параметры GC наугад без профиля и воспроизводимого сценария.
- Уточнение интервьюера
- Какие признаки отличают блокировку потоков от недостатка CPU?
Практические задания
1. Неизменяемый ключ HashMap
Создать ключ аккаунта из tenant и id. Два равных ключа должны находить одну запись.
Ожидаемый результат. Поиск по новому ключу с теми же значениями возвращает сохранённое имя.
Решение и объяснение
record автоматически формирует equals и hashCode по компонентам. Компоненты String и long в этом примере не содержат изменяемого пользовательского состояния.
import java.util.HashMap;
class KeyExample {
record AccountKey(String tenant, long id) {}
public static void main(String[] args) {
var values = new HashMap<AccountKey, String>();
values.put(new AccountKey("team", 7), "Alice");
if (!"Alice".equals(values.get(new AccountKey("team", 7))))
throw new AssertionError("equal key not found");
if (values.containsKey(new AccountKey("other", 7)))
throw new AssertionError("different tenant matched");
}
}2. Объединить независимые результаты
Объединить две успешные асинхронные операции сложением. При ошибке одной не возвращать успешную сумму.
Ожидаемый результат. 20 и 22 дают 42; ошибочная операция даёт exceptional completion.
Решение и объяснение
thenCombine строит зависимость от обоих результатов. В примере completedFuture делает проверку детерминированной; реальный запуск и executor выбираются отдельно.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
class FutureExample {
public static void main(String[] args) {
var a = CompletableFuture.completedFuture(20);
var b = CompletableFuture.completedFuture(22);
if (a.thenCombine(b, Integer::sum).join() != 42)
throw new AssertionError("wrong sum");
var failed = CompletableFuture.<Integer>failedFuture(new IllegalStateException("unavailable"));
try {
a.thenCombine(failed, Integer::sum).join();
throw new AssertionError("failure disappeared");
} catch (CompletionException expected) {
if (!(expected.getCause() instanceof IllegalStateException)) throw expected;
}
}
}3. Проверить транзакционный rollback
В тестовой базе выполнить списание и зачисление в одной транзакции. Между ними вызвать ошибку. Проверить состояние через новое соединение.
Ожидаемый результат. Оба исходных баланса сохранены; частичное списание отсутствует.
Решение и объяснение
Операции объединяются одной транзакцией; при ошибке выполняется rollback. Для Spring отдельно нужен интеграционный тест через управляемый bean с @Transactional, а не прямое создание класса. SQL ниже иллюстрирует именно границу транзакции, не настройки Spring.
CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER NOT NULL);
INSERT INTO accounts VALUES (1, 100), (2, 50);
BEGIN;
UPDATE accounts SET balance = balance - 20 WHERE id = 1;
-- Симулируем отказ до зачисления и выполняем rollback:
ROLLBACK;
SELECT id, balance FROM accounts ORDER BY id;
-- Ожидается: (1, 100), (2, 50).План подготовки на семь дней
- Выпишите требования вакансии и отметьте знакомые и незнакомые темы.
- Ответьте на вопросы Junior вслух, затем проверьте себя по объяснениям.
- Разберите вопросы Middle. Для каждой ошибки запишите свой небольшой пример.
- Решите практические задания без подсказок и проверьте граничные случаи.
- Разберите вопросы Senior и объясните компромиссы применительно к своему проекту.
- Проведите пробное интервью: уточняйте условия, рассуждайте и проверяйте выводы.
- Повторите ошибки, подготовьте реальные истории опыта и проверьте технику.