SprintCode.pro

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

Super

Собеседование на Java-разработчика: 30 вопросов с разбором

18 мин чтения
собеседование
java
карьера

Как устроено собеседование

Техническое интервью на 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 (чтобы наследник не сломал инвариант), а изменяемые поля копировать при входе и выходе.

Ценность в том, что такие объекты автоматически потокобезопасны и их можно свободно кэшировать и использовать как ключи в мапе.

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

Решай алгоритмические задачи как профи

✓ Популярные алгоритмы✓ Разбор решений✓ AI помощь
Начать сейчас
Программист за работой

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.