Всем привет!

Сейчас я работаю 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.count = 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. Конечно, это не универсальный сценарий - у каждого интервью свои нюансы. Но это реальный опыт, который был у меня, и я постарался показать, какие вопросы и ситуации могут встретиться, а на что стоит обратить особое внимание. Надеюсь, это поможет вам подготовиться и подойти к собесу более уверенно.

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

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


  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)

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


        1. SabMakc
          22.11.2025 13:13

          Нюансов хватает в алгоритмах. И O(1) не всегда быстрее O(log N) или O(N) - вопрос алгоритма и размеров данных.

          Та же сортировка - да, сортировка Хоара (quicksort) быстра (O(N * log N)). Но на небольших массивах тот же "пузырек" быстрее (O(N*N)).

          А в некоторых случаях сортировать можно и за O(N) - например, подсчитав количество разных элементов (если память позволяет) - хотя и не факт, что это будет быстрее более традиционных алгоритмов )

          Так что выбор TreeMap может быть оправданным. Как минимум - когда важен порядок ключей )
          Хотя HashMap - крайне универсальное решение, спору нет.


          1. cpud47
            22.11.2025 13:13

            Про константы замечание понятное. Но как раз суть в том, что у хешмапа (даже с учётом онлайн ресайзинга) константы ниже на любых размерах. Разве что может быть проблема с потреблением памяти — но кого оно волнует в жвм :)

            "Когда важен порядок ключей" — непонятно что значит. Либо у нас есть какой-то дополнительный запрос, который учитывает порядок ключей (например lower bound) — об этом писал в самом начале.

            Либо же нас интересует порядок итерации (например при выводе). Здесь есть странный хак: можно использовать хешмап, а при выводе сначала копировать в массив, а потом массив сортировать. Что удивительно, такой подход чаще будет быстрее чем TreeMap.

            Поэтому действительно оправданым TreeMap становится тогда, когда мы на нём начинаем считать всякую статистику, или начинаем строить кастомные структуры данных. А это уже какая-то экзотика для типичного бэка.


            1. SabMakc
              22.11.2025 13:13

              Хак не странный, а вполне популярный )
              Хотя лично для меня, контр-интуитивно, что отдельная сортировка будет быстрее. Хотя замеров на этот счет я не делал, да и не интересовался в целом этим нюансом с TreeMap. Надо будет как-нибудь "поиграться" с TreeMap и HashMap )


              1. cpud47
                22.11.2025 13:13

                Тут вся фишка в том, что массивы — это архитектурный костыль с точки зрения CS, но при этом очень хорошо оптимизированный. Поэтому, у нас срабатывает два эффекта:

                1. Сортировка массивом на порядок быстрее сортировки через дерево

                2. Хешмапы на порядок быстрее деревьев, т.к. хешмапы в среднем находят ключ за 2-3 шага, а деревья вполне себе могут и 20 шагов потратить.

                В оптимизации констант массивы, кеши и прочее — гораздо больше срабатывает.


        1. sawarilinn
          22.11.2025 13:13

          просто создают новый массив

          Это O(n), если массив не какой-то фиксированной длины.


          1. cpud47
            22.11.2025 13:13

            Если использовать calloc и аналоги, будетO(1). И емнип, в jvm будет именно calloc, а не ручное зануление.


            1. sawarilinn
              22.11.2025 13:13

              Как оно может быть O(1), если новый массив размера N все равно кто-то должен занулить?


              1. cpud47
                22.11.2025 13:13

                Ну, да, но опять же нет острой необходимости занулять всё и сразу при аллокации. Можно по мере работы с массивом постепенно его занулять.

                На практике, плюс-минус везде используются всякие хаки с page fault-ами для того, чтобы лениво занулять страницы данных.


                1. sawarilinn
                  22.11.2025 13:13

                  Кажется, на пейдж фолты можно полагаться только при первом выделении памяти, далее же если рантайм не отказывается от переиспользования памяти или не переизобретает виртуальную память внутри себя(ни того, ни другого голанг не делает, насколько я знаю), то переиспользуемую память придется занулять.

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

                  В общем, я могу что-то упускать, но у меня есть большие сомнения в том, что в голанге есть гарантированные O(1) вместо O(тоже-N-но-не-такое-большое-как-с-полным-разовым-перехешированием).


                  1. sawarilinn
                    22.11.2025 13:13

                    Хотя вот сейас подумал, технически же память можно инициализировать не в момент выделения, а "на всякий случай" занулять в момент удаления/сборки мусора.

                    Хотя это кажется контр-продуктивным, поскольку это лишние операции записи, но, кажется, конкретно в данном случае это можно бы сработать.


                  1. cpud47
                    22.11.2025 13:13

                    Если говорить конкретно про го, то логика у них примерно такая (только большие аллокации):

                    1. Если при аллокации памяти кусок ещё не занулён — зануляем его ручками, целиком

                    2. Но, там, вроде бы, есть фоновое зануление памяти, если есть лишние вычислительные ресурсы.

                    Если же говорить про "а как это вообще можно реализовать", то дело тоже не шибко хитрое. Опишу принцип, без какой либо оптимизации:

                    1. В каждый момент времени у нас есть два массива: текущий и следующий

                    2. В момент, когда наш текущий массив достиг заполненности r, мы аллоцируем новый массив, но не зануляем его

                    3. Дальше, при каждой выставке в текущий массив, мы зануляем R элементов в новом массиве (просто помним сколько мы уже занулили, сколько ещё нужно занулить)

                    4. Когда мы достигнет заполненности m, нам нужно переместить элементы в новый массив. Делать мы это будем лениво.

                    5. Соответственно теперь при каждой вставке мы будем перемещать по M элементов в новый массив

                    6. Когда мы достигнем заполненности x, мы просто сделаем новый массив текущим. Ожидается, что к этому моменту все элементы уже были перемещены.

                    В целом, нетрудно подобрать такие r, m и такие R, M, что каждый из этапов гарантированно будет успевать закончиться. Например: r=4/8, m=5/8, x=6/8, R=16, M=6, фактор роста массива в 2 раза. В такой схеме на каждой операции будет выполняться O(1) действий (по модулю коллизий и стоимости malloc).

                    Но, честно говоря, в тот момент, когда паузы от реллокаций станут действительно существенными, я бы просто перешёл на запрос сырой памяти от ОС (т.к. это будет проще и быстрее).


  1. lil_master
    22.11.2025 13:13

    Подскажите пожалуйста, здесь

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

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

    вместо speed имелось ввиду count или в java есть предопределеные именованные переменные?


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

      Это была опечатка, спасибо большое, так да, там count


  1. Femistoklov
    22.11.2025 13:13

    • 1-4 выкинуть

    • 5 и 6 заменить на вопрос/задачу по решению наиболее распространённых проблем с памятью в приложении

    • 7 и 8 заменить на вопрос/задачу по решению наиболее распространённых проблем с многопоточностью в приложении

    • 9 и 10 заменить на что-то, что не решит LLM за 5 секунд (напр. рассказать историю про опыт сложной работы с БД)

    • 11 заменить на реальный пример

    • 12 выкинуть (ну или заменить на пример из жизни)

    • в 14 поменять "самый любимый паттерн и как вы его применяли" => "самый сложный паттерн и как вы его применяли"

    • 15 заменить на вопрос, с какими типами БД работали и как

    • во имя богов, добавить хоть что-нибудь на умение проектировать на уровне приложения, помимо "любимого паттерна"

    • добавить что-то на проверку, умеет ли человек реализовывать сложные бизнес-задачи, рефакторить, пользоваться инструментами, делать ревью, работать с изменениями и конфликтами, писать тесты, документацию, делать интеграцию, конфигурирование, логирование, обработку ошибок, знает ли нужные библиотеки, может ли поддерживать свою развёрнутую систему, хорошо ли разбирается в своей предметной области, ..., в общем, умеет ли он вообще работать или только знает контракт между Equals и HashCode.

    Если это был собес там, куда устроились - передайте, что могут меня не благодарить. Если нет - ну и хурма с ними.


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

      Все собесы разные, у меня был такой, бывают, конечно, собесы гораздо сложнее.

      Зачастую все основные вопросы и понимание у собеседуего приходит на систем дизайне, как по мне, там и доп вопросы и почему так и зачем, вот там и раскрывается человек)

      Я же рассказал свой реальный опыт)


      1. Femistoklov
        22.11.2025 13:13

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

        За опыт спасибо, но было бы полезней, если бы у вас не было всё обезличенным: "какой-то банк", "где-то был собес".

        Берите пример с https://habr.com/ru/articles/926214/.


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

          Возможно вы и правы, спасибо вам за комментарий)


  1. kmatveev
    22.11.2025 13:13

    Я читал все "шпаргалки", и, конечно же, никаким Senior developer в банке автор не является, тянет на студента или недавнего студента.

    По этой статье:

    1. содержимое раздела "Знание контракта между Equals и HashCode" совершенно не относится к контракту между equals() и hashCode().

    2. SQL-задачка. Почему product - это заказ клиента? Нафига group by содержит c.id ?

    3. Задачка на System design - кошмарный overengineering.


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

      Ваше мнение

      Если будут одинаковые имена, то группировка по айди не даст 1 запись, а несколько, по каждому клиенту)

      Мне доказывать нечего, да и не нужно, я просто делюсь своим опытом))

      Вам же спасибо, что следите за мной, мне очень приятно)))


    1. barker
      22.11.2025 13:13

      никаким Senior developer в банке автор не является, тянет на студента или недавнего студента.

      А это вы с точки зрения как должно бы быть или с точки зрения текущих (увы) реалий?


  1. igorp1024
    22.11.2025 13:13

    Без обид :) Bucket != Basket.


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

      Что-то я не могу найти где это в статье, вроде и прошелся еще раз))


      1. igorp1024
        22.11.2025 13:13

        HashMap — это массив корзин.

        HashMap — это массив "вёдер". :)

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

        Возможно, это субъективный мой "пунктик" и я зря цепляюсь.


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

          А у нас всегда называли так, у всех свое, видимо))


        1. cpud47
          22.11.2025 13:13

          В русскоязычном сообществе устоявшийся перевод - "корзин". Это не имеет отношения к bucket vs basket


          1. igorp1024
            22.11.2025 13:13

            К устоявшемуся переводу претензий нет, но слово "bucket" - официальное.

            (Просто поясняю свою позицию, не хочу пускаться в споры)


  1. ruslooob2
    22.11.2025 13:13

    По system design я бы первым делом подумал бы про кеширование и его прогрев (или репликации внешней бд на своей стороне) . А если так нельзя, то уже в сторону асинхронного взаимодействия и т д.