Всем привет!

Сейчас я работаю Senior Java Developer в банке, и за последние годы мне довелось пройти немало собеседований - разных по уровню, стилю и степени жесткости. Сегодня я хочу рассказать об одном из них и поделиться опытом, который может быть полезен тем, кто тоже готовится к новым вызовам.

В моем профиле есть шпаргалки для подготовки к собесам:

  1. Многопоточность без боли

  2. JVM + Память + GC без боли

  3. Spring без боли

  4. БД без боли

  5. Kafka без боли

1. Задача на понимание работы наследования

Есть следующий код:

public class First {
    protected int count;

    public First() {
        System.out.println("First");
        calculate();
    }

    public void calculate() {
        System.out.println(count);
    }

    @Override
    public int hashCode() {
        return 0;
    }
}

class Second extends First {

    public Second() {
        this.count = 5;
        System.out.println("Second");
        calculate();
    }

    public void calculate() {
        this.count++;
        System.out.println(count);
    }

    @Override
    public int hashCode() {
        return 0;
    }
}

class Main {
    public static void main(String[] args) {
        Second s = new Second();
    }
}

Что выведет код?

Ответ и пояснение

First
1
Second
6

Пояснение
  1. При создании объекта Second s = new Second(); сначала вызывается конструктор родительского класса First

  2. В конструкторе First()

    • System.out.println("First") - выводит "First"

    • calculate() → вызывает переопределенный метод из класса Second

  3. Затем выполняется конструктор Second()

    • this.speed = 5 → speed становится 5

    • System.out.println("Second") → выводитSecond

    • calculate() → снова вызывается Second.calculate() где идет count++

2. Знание контракта между Equals и HashCode

Есть код(классы такие же как и в задаче выше), что он выведет:

public static void main(String[] args) {
  HashSet<Object> set = new HashSet<>();
  set.add(new First());
  set.add(new Second());
  set.add(new Second());
  System.out.println("Размер:" + set.size());
}
Ответ

Размер:3

Пояснение
  1. new First() - добавляется (хэшкод = 0)

  2. new Second() - проверяется:

    • Хэшкод = 0 (совпадает)

    • equals() по умолчанию сравнивает ссылки → разные объекты → добавляется

  3. new Second() - еще один новый объект:

    • Хэшкод = 0 (совпадает)

    • equals() сравнивает ссылки → это третий уникальный объект → добавляется

Метод equals() по умолчанию (из класса Object) сравнивает ссылки на объекты, а не их содержимое. Поэтому каждый new Second() создает новый объект с новой ссылкой, и все они считаются разными.

Тут важно отменить, что даже в случае, если hashCode не будет совпадать, то все равно получим Размер:3

Но если мы переопределим equals и hashCode:

@EqualsAndHashCode // в качестве примера взял аннотацию из lombok
public class First {

}

@EqualsAndHashCode
class Second extends First {

}

То результат уже будет Size:2

3. Устройство HashMap

Я понимаю, что уже почти в каждом углу говорилось про HashMap. Я расскажу очень коротко(если хотите почитать подробнее и углубиться, то можете посмотреть тут)

Ответ(короткий)

HashMap — это массив корзин. Индекс выбираем по hashCode, а столкновения (коллизии) решаем сравнениями через equals.

Алгоритм вставки:

  1. Считаем hashCode() и определяем корзину.

  2. Если корзина пуста - вставляем.

  3. Если в корзине есть элементы:

    • ищем тот же ключ через equals

    • если найден → заменяем значение

    • если нет → добавляем новый элемент (список → дерево при >8 элементов)

  4. При заполненности > loadFactor происходит resize.

4. Зачем нужны бинарные деревья и их сложность

Про бинарные деревья тоже говорили уже везде, подробная инфа тут

Ответ(короткий)

Зачем нужны бинарные деревья:
Чтобы хранить данные в отсортированном виде и быстро выполнять поиск, вставку и удаление.

Сложность поиска в BST:

  • лучший случай (сбалансировано): O(log n)

  • худший случай (вырождено в список): O(n)

Сложность вставки:

  • лучший случай: O(log n)

  • худший случай: O(n)

5. Какие есть виды GC и в чем их отличие

Об этом я рассказывал тут

Ответ

Serial GC

  • Использует один поток для всех фаз GC.

  • Подходит для однопоточных приложений и небольших heap.

  • Алгоритм: "copying" (в Young Gen) и "mark-sweep-compact" (в Old Gen).

  • Параметр: -XX:+UseSerialGC

Parallel GC (Throughput Collector)

  • Использует несколько потоков для работы в Young и Old Gen.

  • Цель — максимальная пропускная способность, а не минимизация пауз.

  • Подходит для серверных приложений без строгих требований к задержкам.

  • Параметр: -XX:+UseParallelGC

CMS (Concurrent Mark Sweep) [устарел]

  • Работает параллельно с приложением (concurrent), уменьшая stop-the-world паузы.

  • Этапы: initial mark, concurrent mark, remark, sweep.

  • Не компактизирует память (может привести к фрагментации).

  • Устарел начиная с Java 9 и удалён в Java 14

  • Параметр: -XX:+UseConcMarkSweepGC

G1 GC (Garbage First)

  • Делит heap на множество регионов.

  • Каждый регион может быть частью Young или Old Generation.

  • Этапы GC включают: Initial Mark, Concurrent Mark, Remark, Cleanup, Copy.

  • Работает по принципу "сборка сначала самых мусорных регионов" (Garbage First).

  • Использует предсказуемые паузы и старается не превышать MaxGCPauseMillis

  • Поддерживает инкрементальную, concurrent и компактизирующую сборку Old Gen.

  • G1 ведёт статистику "полезности" регионов и выбирает наиболее эффективные для сборки.

  • Параметр: -XX:+UseG1GC

  • По умолчанию используется с Java 9+

ZGC (Z Garbage Collector)

  • Поддерживает heap до терабайт.

  • Работает с паузами менее 10 мс, независимо от размера heap.

  • Полностью concurrent (почти все фазы выполняются параллельно с приложением).

  • Подходит для latency-чувствительных систем.

  • Параметр: -XX:+UseZGC

Shenandoah

  • Похож на ZGC, с акцентом на короткие паузы.

  • Использует concurrent compacting.

  • Поддерживается OpenJDK.

  • Параметр: -XX:+UseShenandoahGC

6. Типы ссылок в java

Ответ

Strong Reference (сильная ссылка)

  • Это обычные ссылки, которые мы используем каждый день.

  • Пока на объект существует хотя бы одна сильная ссылка - он не подлежит сборке мусора.

  • Чтобы объект мог быть собран, все сильные ссылки на него должны быть обнулены.

Object obj = new Object();

Soft Reference (мягкая ссылка)

  • Объект удаляется только при нехватке памяти.

  • Используется в кешах, чтобы не загружать память, но сохранить объект, если он ещё полезен.

  • Можно получить объект через ref.get(), но если GC уже удалил его - вернётся null.

SoftReference<Object> ref = new SoftReference<>(new Object());

Weak Reference (слабая ссылка)

  • Объект может быть собран немедленно, даже если только слабые ссылки на него остались.

  • Используется для реализации структур с автоудалением (например, WeakHashMap).

  • Часто применяется, когда объект должен быть доступен «до тех пор, пока он кому-то нужен».

WeakReference<Object> ref = new WeakReference<>(new Object());

Phantom Reference (фантомная ссылка)

  • Объект уже помечен как удаляемый, но ещё не собран GC.

  • Метод get() всегда возвращает null.

  • Используется для контроля финализации и освобождения ресурсов вне heap (например, off-heap, native).

  • Требует ReferenceQueue, через которую можно узнать, что объект вот-вот будет удалён.

PhantomReference<Object> ref = new PhantomReference<>(new Object(), referenceQueue);

7. Назови 5 классов из пакеты concurrent и зачем они нужны

Ответ

1) CompletableFuture - позволяет запускать асинхронные задачи, комбинировать их, цеплять колбэки и работать без блокировок. Упрощает параллелизм.

2) ConcurrentHashMap - потокобезопасный HashMap. Позволяет многим потокам одновременно читать и обновлять данные без общего большого локa.

3) Phaser - продвинутый синхронизатор, позволяет синхронизировать потоки по фазам (этапам). Гибче, чем CyclicBarrier/CountDownLatch.

4) AtomicInteger - примитив для атомарных операций над int без использования локов (CAS). Нужен для счетчиков, флагов и инкрементов между потоками.

5) ReentrantLock - явная блокировка с расширенными возможностями: tryLock, fairness, condition-переменные. Более гибкая альтернатива synchronized.

8. Расскажи про DeadLock и LiveLock

Ответ

Deadlock (взаимная блокировка)

Потоки навсегда блокируют друг друга, каждый ждёт ресурс, удерживаемый другим.
Итог: система стоит, прогресса нет.

Пример:
Поток A держит ресурс 1 и ждёт ресурс 2.
Поток B держит ресурс 2 и ждёт ресурс 1.

Как избегать:

  1. Всегда блокировать ресурсы в одном порядке.

  2. Использовать таймауты при захвате блокировок (tryLock(timeout)).

  3. Минимизировать количество одновременно захватываемых блокировок.

Livelock (ожившая блокировка)

Потоки не заблокированы, но бесполезно двигаются, постоянно пытаясь избежать конфликта и мешая друг другу.
Итог: система работает, но прогресса тоже нет.

Пример:
Два потока уступают друг другу ресурс, отказываются и пытаются снова, но синхронно, и бесконечно.

Как избегать:

  1. Добавлять рандомные задержки или экспоненциальный бэкофф при повторных попытках.

  2. Использовать явные таймауты и прекращать попытки через определённое время.

  3. Пересматривать алгоритм кооперации, чтобы не блокировать друг друга непрерывно.

9. SQL задачка

Есть такая структура бд:

структура бд
структура бд

Нужно написать запрос, который вернет имя и общую сумму заказов клиента. Клиент при этом должен быть активным, а сумма заказов больше 0

Ответ

Мой запрос выглядит так:

SELECT
    c.name AS company_name,
    SUM(p.price) AS product_sum
FROM client c
JOIN product p ON c.id = p.client_id
WHERE c.is_active
GROUP BY c.id, c.name
HAVING SUM(p.price) > 0
ORDER BY product_sum DESC;

10. В чем разница между having и where

Ответ

Время применения

  • WHERE - фильтрация до группировки (на уровне отдельных строк)

  • HAVING - фильтрация после группировки (на уровне групп)

С чем работают

  • WHERE - работает с отдельными записями и обычными полями

  • HAVING - работает с результатами агрегатных функций (SUMCOUNTAVG и т.д.)

Использование с GROUP BY

  • WHERE - может использоваться без GROUP BY

  • HAVING - используется вместе с GROUP BY

11. Что такое explain plan и для чего он нужен

Ответ

EXPLAIN PLAN — это план выполнения SQL-запроса, показывающий, какие операции СУБД будет выполнять (сканирование таблицы, использование индекса, типы join’ов и т.д.). Он нужен для понимания того, где находятся узкие места и что можно оптимизировать — например, добавить индекс, поменять тип соединения или переписать запрос.

12. Какие есть уровни изоляции транзакции и какие проблемы в них присутствуют?

Ответ

Есть прекрасная табличка из официальной:

https://www.postgresql.org/docs/current/transaction-iso.html

13. Понимание proxy в spring

Есть базовые задачки с транзакциями, но на этом собесе мне задавали вопросы про @Cacheable. Есть примера кода, в котором кэш не работает, как сделать так, чтобы он заработал:

@Service
@EnableCaching
public class CacheClass {
    
    @SneakyThrows
    @PostConstruct
    public void init() {
        System.out.println(test(1));
        Thread.sleep(1000);
        System.out.println(test(2));
        Thread.sleep(1000);
        System.out.println(test(1));
    }

    @Cacheable(cacheNames = "test", key = "#integer")
    public String test(int integer) {
        return LocalDateTime.now().toString();
    }
}
Ответ

Здесь можно сразу предложить несколько вариантов по аналогии с транзакциями

  1. Сделать self inject и вызвать метод через него

  2. Использовать applicationContext.getBean(CacheClass.class);

Для @Transaction это было 100% сработало бы, но в Cacheable есть подводный камень.

Ни один из верхних вариантов не сработает. Подумай еще, как можно это решить?

Ответ для Cacheable

Для @Cacheable ситуация ещё хуже: кеш-аспект инициализируется позднее, поэтому вызов @Cacheable из @PostConstruct обычно не срабатывает — об этом есть явное issue в Spring

Тут вы можете применить ApplicationRunner или CommandLineRunner, которые смогу помочь, просто заимплементить его:

@Service
@EnableCaching
public class CacheClass implements ApplicationRunner {

    @Autowired
    @Lazy
    private CacheClass self;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(self.test(1));
        Thread.sleep(1000);
        System.out.println(self.test(2));
        Thread.sleep(1000);
        System.out.println(self.test(1));
    }

    @Cacheable(cacheNames = "test", key = "#integer")
    public String test(int integer) {
        return LocalDateTime.now().toString();
    }
}

14. Приведите пример каждого из типа паттернов(поведенческий, порождающий, структурный) и назовите ваш самый любимый паттерн и как вы его применяли.

Ответ

Паттернов очень много, поэтому я приведу вам эту картинку:

паттерны
паттерны

Мой же самый любимый паттерн это шаблонный метод.
Шаблонный метод используют, когда есть общий алгоритм, который повторяется у разных обработчиков, но некоторые шаги в нём должны отличаться. Общая логика фиксируется в базовом классе, а изменяющиеся части выносятся в абстрактные методы и уже конкретные реализации определяют, что происходит на этих этапах. Это позволяет избежать дублирования кода и чётко разделить общие и различающиеся шаги алгоритма.

Этот паттерн выручал меня не один раз)

15. Когда использовать реляционные базы, а когда нет. В чем разница между SQL и NoSQL базой

Ответ

SQL бд:

  1. Сложные запросы и JOIN — когда нужны агрегации и связи между таблицами

  2. Транзакции ACID — банковские операции, финансовые системы

  3. Структурированные данные — четкая схема, предсказуемая структура

  4. Целостность данных — внешние ключи, constraints

  5. Отчетность и аналитика — сложные SQL-запросы

Подходят для: банковские системы (транзакции), медицинские записи (целостность данных), интернет-магазины (заказы, inventory)

NoSQL бд:

  1. Большие объемы данных — Big Data, логгирование

  2. Горизонтальное масштабирование — распределенные системы

  3. Гибкая схема — часто меняющаяся структура данных

  4. Высокая производительность записи — IoT, сенсоры, clickstream

  5. Неструктурированные данные — JSON, документы, графы

Подходят для: проекты, где работают с документами, где нужно обрабатывать огромные объемы данных(например я знаю, что в VK Video используют cassandra в качестве БД), структура данных часто меняется

16. Задачка на system design

Мы работаем в страховой компании и предоставляем клиентам услуги по оформлению полисов. Когда приходит запрос от клиента, нам нужно обратиться к сторонней SOAP-системе через HTTP, чтобы получить необходимые данные для расчёта. По требованиям мы должны дать клиенту ответ в течение двух минут. Если за это время расчёт не завершён — мы обязаны вернуть ошибку.

Проблема в том, что эта внешняя SOAP-система в среднем отвечает около 40 секунд, но работает нестабильно: иногда очень медленно, иногда вообще не отвечает. При этом ожидаемая нагрузка — до 15 000 запросов в секунду, то есть система должна выдерживать высокий поток обращений и не зависеть от её нестабильности.

Скажу сразу, в этой задаче нет четкого одного ответа. Я же расскажу свой ответ:

Ответ

Я пойду по порядку:

  1. Я бы сделал gate-way шлюз для авторизации и аутентификации пользователя(OAuth, Keycloak)

  2. Асинхронная модель(архитектура событий) - SOAP-система нестабильная, выдает ответ +-40 сек, SLA ≤ 2 минут - асинхронный подход максимально оправдан. Клиенту не нужно ждать - даёте taskId и он проверяет статус

  3. Микросервисная архитектура, я выделил несколько сервисов:

    • Main-service - только бизнес-логика: создание задач, хранение статуса, оркестрация.

    • Adapter-service - работа с внешней системой, ретраи, circuit breaker.

    • Timeout-service - только таймеры и SLA. (Я бы использовал delay queue для отслеживания таймера)

    • Gate-way service - авторизация, аутентификация, переадресация пользователей

  4. В Adapter-service нужны ретраи, circuit breaker, fallback

  5. Main-service обрабатывает всю логику, также он прослушивает ответ от adapter-service и timeout-service + main хранит информацию о сообщениях(outbox таблица)

  6. Timeout-service можно реализовать с помощью delay queue для отслеживания таймера

  7. Мы будем использовать kafka + outbox + de-dup таблица, чтобы гарантировать доставку и обработку 1 раз.

    • Outbox гарантирует доставку события.

    • Идентификаторы сообщений — защита от дублей в потребителе.

    • Kafka — выдержит твой TPS с огромным запасом.

  8. Для отслеживания состояний можно использовать distributed tracing

  9. Архивный топик (fan-out topic) - мы отбрасываем сообщения в отдельный топик, который на данный момент никто не слушает, чтобы была возможность к нему подключиться и прослушать все сообщения, даже, если kafka уже удалила их из другого топика.(Это можно сделать, если в будущем видится добавление сервисов обработки и есть на это доп ресурсы)

  10. Если в дальнейшем видится рост клиентов, можно использовать Kafka Streams для работы с большими данными

Минусы такого подхода:

  • сложнее логирование

  • сложнее трассировка

  • нагрузка на инфраструктуру

  • сложнее тестирование

Плюсы такого подхода:

  • масштабируемость

  • гибкость / расширяемость

  • отказоустойчивость

  • высокая пропускная способность

Примерная схема взаимодействия сервисов:

архитектура задачи
архитектура задачи

Итог

Сегодня мы прошли через пример полного собеседования на позицию Senior Java Developer. Конечно, это не универсальный сценарий - у каждого интервью свои нюансы. Но это реальный опыт, который был у меня, и я постарался показать, какие вопросы и ситуации могут встретиться, а на что стоит обратить особое внимание. Надеюсь, это поможет вам подготовиться и подойти к собесу более уверенно.

Всем спасибо за внимание, удачных собесов и хорошего дня!)

Комментарии (18)


  1. mantiscorp
    22.11.2025 13:13

    Какие есть виды GC и в чем их отличие

    очень важный вопрос для разработчика, особенно уровня senior.

    лично Вы сколько раз за свою карьеру задумывались, какой же gc используется в Вашем проекте?

    нисколько?


    1. MishaBucha Автор
      22.11.2025 13:13

      Наверное как и большинство разработчиков, пока не будет с ними проблем - никто не задумывается)))


      1. Sequoza
        22.11.2025 13:13

        Можно узнать, какой примерно процент ваканский, где нужно модифицировать gc? Ну или юзкейс хотя бы. На мой взгляд, если до этого дошло, не лучше ли использовать другие языки?


        1. MishaBucha Автор
          22.11.2025 13:13

          Примерно 0 в моей практике)) обычно ты приходишь и за тебя уже все настроено, а именно выбор GC стоит очень редко, обычно дефолтный покрывает все, что нужно, за редким исключением


        1. Akon32
          22.11.2025 13:13

          Опции gc модифицировать часто приходилось, иногда менять тип gc.


          1. Sequoza
            22.11.2025 13:13

            О, отлично. Тогда не подскажите, как вы это делали? Гугл+попытки или вы точно знали что менять.

            Спрашиваю потому, что большая часть моей работы и работы коллег - восставление знаний, которыми когда-то обладали или закрытие пробелов в процессе работы.


    1. VGoudkov
      22.11.2025 13:13

      Пока не покажут пальцем на GC паузы :). Сейчас то хорошо, а раньше CMS (и что было до него) иногда давал прикурить, порождая такие артефакты в кодовой базе, как самописный пул объектов.


  1. VGoudkov
    22.11.2025 13:13

    Спасибо за материал, кратко и со вкусом!

    На мой взгляд в шпаргалках (и видимо вопросах на собесах) не хватает чего-то наподобие "Как вы будете разбираться с приложением, которое упало по OOM", "Как вы будете выяснять, почему приложение иногда перестаёт отвечать в рамках SLA (а потом опять начинает, само да...).
    Я про то, что JMC, VisualVM, Profiler в IDEA и это вот наше всё :)


    1. MishaBucha Автор
      22.11.2025 13:13

      Профайлер вообще имба, выручал не один раз))


  1. Elinkis
    22.11.2025 13:13

    Спасибо!


  1. KIL2
    22.11.2025 13:13

    • HAVING - работает с результатами агрегатных функций (SUMCOUNTAVG и т.д.)

    Это утверждение не корректно. Оператор SELECT , содержащий функции SUMCOUNTAVG и т.д., отрабатывает ПОСЛЕ оператораHAVING , т.е. HAVING никак не может работать с РЕЗУЛЬТАТАМИ агрегатных функций. Как раз, наоборот, агрегатные функции работают с результатами работы оператора HAVING.


    1. VGoudkov
      22.11.2025 13:13

      HAWING - это WHERE для результатов агрегатов. Т.е. отобрать всех клиентов, которые делали больше, чем три покупки за последнюю неделю.


      1. KIL2
        22.11.2025 13:13

        HAVING - это аналог WHERE именно для результата группировки (GROUP BY), а не агрегатов. Да, в HAVING возможно использование результатов агрегатов, но не только их. Но в HAVING могут использоваться не только результаты агрегатов, но и значения столбцов, использованных в группировке (не результаты агрегатов)


        1. KIL2
          22.11.2025 13:13

          В данном случае под агрегатами я имел ввиду результаты работы агрегатных функций SUM, COUNT и т.д.


  1. cpud47
    22.11.2025 13:13

    Часть про бинарные деревья не очень хорошая. Если нужно только искать и вставлять бинарные деревья никому не нужны — просто используйте хешмап.

    Бинарные деревья нужны, когда нужны всякие нестандартные запросы: lower_bound, range, агрегат на отрезке и прочее.

    Ну и про сложность операций не очень удачно выразились: нет никакого лучшего и худшего случая. Либо Вы работаете со сбалансированным деревом и тогда сложностьO(\log n)в худшем случае. Либо Вы делаете что-то сильно нетривиальное и сложность операций зависит от контекста.


    1. MishaBucha Автор
      22.11.2025 13:13

      Спасибо, да, вы правы, я не совсем корректно выразился, спасибо)


    1. SabMakc
      22.11.2025 13:13

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

      HashMap имеет сложность вставки O(N) в худшем случае (ресайз).
      Так что бинарные деревья могут быть нужны просто ради более-менее предсказуемой скорости вставки. Хотя, если честно, не сказать что это распространенная проблема - HashMap обычно более чем достаточно )


      1. cpud47
        22.11.2025 13:13

        Тоже об этом подумал, но потом понял что это всё ещё недостаточно причина. Можно сделать хешмап со сложностью вставки заO(1)— нужно просто делать ресайз постепенно. Да и в целом, для большинства структур с аммортизацией можно сделать структуру без аммортизации с сохранением сложности операций: нужно просто размазать аммортизированную работу по всем операциям.

        Собственно, например, в го именно так и устроен хешмап: они в момент ресайза просто создают новый массив, но не удаляют старый. Далее, при поиске они проверяют нет ли двух массивов. Если есть два массива, они мигрируют сколько-то элементов из старого в новый, после чего делают лукап в обоих. Таким образом получается честныйO(1)

        Такая схема сильно замедляет операции с хешмапами, конечно. Но деревья поиска прям очень медленные на самом деле. Поэтому стоит их избегать пока есть возможность.