惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
大猫的无限游戏
大猫的无限游戏
M
MIT News - Artificial intelligence
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Announcements
Recent Announcements
IT之家
IT之家
Google DeepMind News
Google DeepMind News
罗磊的独立博客
爱范儿
爱范儿
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
U
Unit 42
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
B
Blog
博客园 - 叶小钗
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
C
Check Point Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
8 вопросов с java-собеса в банке: Junior отвечает за 10 с...
Java Jub · 2026-06-16 · via Все публикации подряд на Хабре

Средний

11 мин

974

Один вопрос — три ответа

Один вопрос — три ответа

Один и тот же вопрос, три глубины ответа. По ней интервьюер и считывает грейд.


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

Вопросы на собесах в банках от грейда к грейду почти не меняются.

«Что такое индекс» спрашивают и у стажёра, и у сеньора с десятью годами. Про equals и hashCode — тоже у всех. Про @Transactional — вообще в каждом втором интервью, кого ни возьми. Меняется не сам вопрос. Меняется глубина, на которую от тебя ждут ответ. И именно по этой глубине интервьюер за полминуты понимает твой настоящий уровень — задолго до того, как вы дойдёте до зарплаты.

Сразу оговорюсь, потому что меня за прошлую статью за это справедливо пинали: я не считал, сколько процентов народу спотыкается на каждом вопросе. Нет у меня таких цифр, и придумывать их я не стану. Зато есть сами расшифровки, где видно, как один и тот же вопрос звучит на джуне, на мидле и на сеньоре — и насколько по-разному его ждут.

Сразу про одну вещь, в которой я сам долго путался, когда раскладывал эти ответы. Граница «мидл / сеньор» проходит не там, где кажется. «Знать, как HashMap превращает список в дерево» или «как AtomicInteger работает на CAS» — это не сеньор, это крепкий мидл. Сеньор отличается не тем, что помнит больше внутренностей JDK, а тем, что видел, как это всё ломается в проде, и думает про цену решения. Дальше будет видно по каждому вопросу.

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


Чтобы было видно, о чём речь, вот тот самый «индексный» вопрос, разложенный по уровням. Дальше будет ещё семь в том же духе.

Один вопрос «что такое индекс» — три глубины ответа

Один вопрос «что такое индекс» — три глубины ответа

Где-то ниже я помечаю уровни цветом — 🟢 джун, 🟡 мидл, 🔴 сеньор, — а где история живее прозой, просто называю их по ходу. Уровни наслаиваются: мидл знает всё, что джун, плюс своё; сеньор — всё это плюс то, что приходит только с продом.


1. equals и hashCode

Баян, конечно. Но раз спрашивают везде, давайте по-честному — и без карикатур на джуна.

🟢 Джун обычно отвечает уверенно и правильно: equals — это равенство по значению, == — по ссылке; переопределил equals — переопредели и hashCode, это контракт; равные объекты обязаны вернуть равный hashCode. Генерю через IDE, Lombok или беру record. Нормальный ответ, он правда так знает.

🟡 Мидл показывает, что будет, если контракт нарушить. Переопределили только equals — и два «равных» объекта дадут разный hashCode, улетят в разные бакеты HashMap, а get() по второму ключу вернёт null, хотя элемент в карте лежит. Обратное контрактом не требуется: одинаковый хэш у разных объектов это просто коллизия, и это нормально. Сюда же правило, которое экономит нервы: hashCode нельзя строить на полях, которые потом меняются.

🔴 Сеньор говорит не про новые факты, а про то, как это спроектировать, чтобы не рвануло в реальном приложении. JPA-сущности: id генерит база, до сохранения он null, и наивный equals по id ведёт detached- и managed-объекты по-разному — Set начинает «терять» элементы между сессиями. Лечится бизнес-ключом или фиксированным hashCode. Плюс ловушка с Hibernate: ленивый прокси подменяет класс объекта, поэтому equals через getClass() ломается на прокси, и берут instanceof. Вот эта прод-боль и отличает сеньора, а не знание самого контракта.

Граница тут честная: контракт назубок знают все, а что именно отвалится в ORM — единицы. В расшифровках, кстати, у ТБанка дословно: «контракт equals и hashCode, может ли у разных объектов совпасть hashCode».


2. Как устроен HashMap

Логичное продолжение, поэтому часто идёт сразу за предыдущим.

🟢 Джун опишет верхушку: пары «ключ — значение», ключ уникален, доступ в среднем за O(1), внутри массив. TreeMap держит ключи отсортированными. Для старта достаточно, и это не стыдно.

🟡 Мидл залезает под капот — и вот это весь мидловский слой, тут нет ничего сеньорского. Массив бакетов, индекс считается как hash & (n-1), коллизии складываются в бакет списком. Load factor 0.75: заполнилось больше — resize, удвоение массива и перераскладка. С Java 8 длинный бакет (восемь элементов, когда сам массив уже не меньше 64) превращается в красно-чёрное дерево, чтобы вырожденный случай не падал в O(n); обратно в список — на шести. Перед раскладкой старшие биты хэша подмешиваются в младшие, чтобы кривой hashCode реже коллизировал. И HashMap не потокобезопасен.

🔴 Сеньор не сыплет внутренностями дальше — он выбирает инструмент и считает цену. Высокая конкуренция значит ConcurrentHashMap, и сразу оговорка, что его итератор слабосогласованный, а не fail-fast. Resize на большой мапе — это пауза на перехеширование, поэтому если размер примерно известен, мапу присайзивают через конструктор, чтобы не ловить пилу по latency. А если в ней миллионы записей с примитивами, всплывает оверхед на каждый Node, и иногда честнее специализированная коллекция.


Устройство HashMap: список и дерево

Устройство HashMap: список и дерево

Коллизии — список; при 8+ в одном бакете он становится красно-чёрным деревом. Это уровень Middle.


3. Почему String неизменяемый

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

🟢 Джун скажет: String нельзя изменить после создания, для изменений есть StringBuilder, литералы лежат в пуле, поэтому одинаковые литералы — это один объект. 🟡 Мидл добавит механику: литералы интернируются, new String("x") всегда создаёт объект мимо пула, вернуть в пул можно через intern(); склейка двух литералов схлопывается компилятором в один, а конкатенация переменных в цикле плодит мусор — отсюда вечный совет про StringBuilder. И мидл уже отвечает на «зачем»: потокобезопасность без блокировок и hashCode, посчитанный один раз и закэшированный (поэтому String так хорош как ключ).

А вот где настоящая сеньорская грань — и это надо сказать прямо: здесь она не про новый факт, а про опыт. Сеньор вспомнит безопасность как инвариант: проверенный путь к файлу, host или фрагмент SQL нельзя подменить после валидации, иначе классический TOCTOU, и неизменяемость закрывает это бесплатно. И грабли intern() на масштабе: пул живёт в куче, но если интернировать строки из пользовательского ввода, он пухнет и начинает давить на GC — такое ловили в проде не раз. Плюс Compact Strings с Java 9, latin1 против UTF-16, что реально меняет память на сервисах, где строк миллионы.

То есть граница тут проходит не по знанию определения, а по тому, видел ли человек, как intern() или склейка в горячем цикле выстреливали на продакшене.


4. Optional

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

Ловушка первая: orElse(x) вычисляет x всегда — даже когда значение есть и x не нужен. Если за x прячется поход в базу, это лишний запрос на ровном месте. Ловушка вторая: Optional в поле сущности или DTO.

🟢 Джун про Optional знает базовое и верное: обёртка, чтобы явно показать, что значения может не быть, и не возвращать null. На первую ловушку, скорее всего, не среагирует, и это ожидаемо.

🟡 Мидл обе ловушки разруливает. orElseGet(() -> x) ленивый, в отличие от orElse; get() без проверки — это тот же NPE, только сбоку; для потенциального null нужен ofNullable, а не of, который сам кинет NPE. Знает map/flatMap/filter и orElseThrow.

🔴 Сеньор смотрит на это как на дизайн API. Optional задуман как тип возвращаемого значения, и только: он не Serializable (поле ломает сериализацию), это лишняя аллокация на горячем пути, а Optional<List<T>> — нонсенс, пустой список и так говорит «ничего нет». Поэтому в полях, параметрах и коллекциях его не держат. Разница между мидлом и сеньором тут не в знании методов, а во вкусе: где этой обёртке место в публичном контракте, а где она только мусорит.


5. counter++ и сто потоков

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

🟢 Джун ответит верно по сути: нет, будет меньше, потоки мешают друг другу, это race condition, надо synchronized. Направление правильное.

🟡 Мидл объяснит, почему меньше, и это ровно его слой. counter++ — не одна операция, а три: прочитать, увеличить, записать; два потока читают одно значение, и один инкремент затирается. Чинится либо synchronized-блоком, либо AtomicInteger.incrementAndGet(). И да, мидл вполне знает, что атомик внутри на CAS: читает текущее, пытается записать новое, проверяет, что за это время никто не влез, влез — повтор по кругу, блокировки нет. Подчеркну: вот это всё ещё разговор уровня Middle, ничего сеньорского в «знаю про CAS» нет.

🔴 Сеньор начинает с того, что переформулирует задачу. Под высокой конкуренцией CAS-спин крутится вхолостую и жжёт CPU — тогда выигрывает LongAdder, который размазывает счётчик по ячейкам и складывает их лениво при чтении. Но ещё раньше сеньор спросит: а нужен ли вообще один общий счётчик? Часто можно считать локально в каждом потоке и свести в конце; иногда это вообще работа БД или Redis с атомарным инкрементом; и нужна ли точность в моменте или достаточно, чтобы значение сошлось чуть позже. «Починить counter++» — часто не та задача, которую стоило решать.


Гонка в counter++: потерянный инкремент

Гонка в counter++: потерянный инкремент

Оба потока прочитали 41, оба записали 42 — один инкремент потерян. Вот почему counter++ не атомарна.


6. volatile и happens-before

Прямое продолжение, обычно идёт сразу следом, чтобы добить тему.

🟢 Джун: volatile нужен, чтобы потоки видели актуальное значение, а не закэшированное у себя в ядре.

🟡 Мидл ставит важную оговорку: volatile даёт видимость и запрещает переупорядочивание вокруг себя, но не делает операцию атомарной — поэтому counter++ он не спасает, там по-прежнему три действия. Нормальный кейс — флаг остановки потока, while (!stopped). И happens-before мидл тоже назовёт: запись в volatile happens-before последующего чтения той же переменной и протаскивает за собой все записи, сделанные потоком до неё. Тут честно стоит сказать — в банковской базе happens-before помечен как Middle+, то есть это верх мидла, а не отдельная сеньорская планка.

🔴 Сеньор уходит в безопасную публикацию целиком, и вот это уже его территория. Почему double-checked locking до Java 5 был сломан: без гарантий модели памяти другой поток мог увидеть наполовину сконструированный объект, и чинится это volatile на поле синглтона. Почему immutable-объект с final-полями можно отдавать между потоками вообще без synchronized — JMM отдельно гарантирует видимость полностью построенного объекта через final. И обратная сторона: где volatile вешают зря — если доступ и так уже под одним замком, он не добавляет ничего, кроме шума. Сеньор отвечает не на «что такое happens-before», а на «как безопасно опубликовать состояние между потоками».


7. Индексы в базе

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

🟢 Джун: индекс ускоряет поиск, как оглавление в книге — по индексированному полю WHERE отрабатывает быстрее, чем перебор всей таблицы. В ТБанке джунам так и формулируют: что такое индекс и почему нельзя навесить его на каждое поле.

🟡 Мидл: чаще всего B-tree — равенство, диапазоны, сортировка; hash — только равенство; GIN/GiST — полнотекст, массивы, гео. На каждый столбец нельзя: индекс занимает место и замедляет запись, потому что его надо обновлять при каждом INSERT/UPDATE/DELETE. Покрывающий (INCLUDE) отвечает на запрос, не заходя в таблицу; частичный (WHERE) индексирует подмножество строк; у составного важен порядок колонок, работает левый префикс. Это ровно мидловские формулировки из тех же интервью.

🔴 Сеньор живёт там, где индекс не срабатывает и где его накатывают на проде. Не сработает не на SARGable предикате — функция над колонкой, LIKE '%x%' с процентом в начале, неявный каст типов — и при низкой селективности: если запрос вернёт большую долю таблицы, планировщик сознательно уйдёт в seq scan, потому что так дешевле, и сеньор это читает по EXPLAIN (ANALYZE, BUFFERS). А на терабайтнике обычный CREATE INDEX лочит запись и встаёт колом, поэтому только CREATE INDEX CONCURRENTLY: дольше, в два прохода, при сбое оставляет INVALID-индекс на пересоздание, и внутри транзакции его не запустить. Эта верхняя ступень в базе и помечена как Middle+: «таблица на несколько терабайт — как накатить индекс, не убив прод».

Три ступени, одна компания. Собственно, с этого наблюдения вся статья и началась.


Почему планировщик выбирает seq scan

Почему планировщик выбирает seq scan

Слева индекс работает, справа — простаивает. Сеньор знает, почему планировщик выбрал seq scan.


8. @Transactional, который держит соединение

Мой любимый, и в банках его обожают. Формулировка обычно такая: повесили @Transactional на метод, а он внутри ходит синхронным REST-вызовом в соседний сервис. Что не так?

🟢 Джун ответит про основу и не ошибётся: @Transactional открывает транзакцию, при исключении откатывает, нужен, чтобы несколько операций с базой были атомарны — всё или ничего. ACID.

🟡 Мидл знает подводные камни механики, и это именно мидловский слой. По умолчанию откат только на RuntimeException и Error, на checked-исключение транзакция не откатится — нужен rollbackFor, и на этом регулярно горят. И про прокси: @Transactional работает через AOP-прокси, поэтому вызов метода изнутри того же бина (self-invocation) идёт мимо прокси, и транзакция не стартует; по той же причине private- и final-методы не перехватываются. Сюда же propagation и уровни изоляции, если попросят.

🔴 Сеньор видит в вопросе беду подороже и обычно рассказывает её как историю с прода. Синхронный HTTP держит транзакцию открытой ровно столько, сколько длится сетевой таймаут, а вместе с ней удерживает соединение из пула и блокировки на строках. Под нагрузкой пул вычерпывается, и ложится не один метод, а сервис целиком, каскадом — разгребать такое в три ночи незабываемо. Поэтому сеть внутри транзакции это антипаттерн: транзакция должна быть короткой и только про базу. Вызов выносят наружу, а согласованность с внешним миром делают через Outbox или Saga, иначе ловишь классику «сообщение в Kafka ушло, а транзакция откатилась». И ещё: долгая транзакция в PostgreSQL из-за MVCC удерживает старые версии строк и мешает VACUUM, таблицы пухнут. Сеньор тут про границы, ресурсы и согласованность под нагрузкой, а не про то, как оно откатывается.

Это фирменный вопрос ТБанка, в нескольких расшифровках почти слово в слово.

Сеньорская ловушка: сеть внутри @Transactional

Сеньорская ловушка: сеть внутри @Transactional

Транзакция держит соединение всё время сетевого таймаута — под нагрузкой пул вычерпывается, и ложится весь сервис.


Та же восьмёрка одним экраном

Если хочется просто пробежать перед собесом — вот всё, что выше, в одной таблице. Граница «мидл/сеньор» специально сдвинута к реальности: внутренности это мидл, прод и трейд-оффы это сеньор.

#

Вопрос

🟢 Джун

🟡 Мидл

🔴 Сеньор

1

equals / hashCode

равенство по значению, переопредели оба, равные → равный хэш

только equals → разные бакеты, get() не найдёт; не на мутабельных полях

стратегия для JPA-сущностей; getClass ломается на Hibernate-прокси → instanceof

2

HashMap внутри

пары, O(1) в среднем, массив

бакеты, load factor/resize, treeify ≥8, не потокобезопасен

выбор под нагрузку/память, цена resize в hot path, когда CHM или иная структура

3

String immutable

нельзя менять, пул, литерал = один объект

интернирование, new/intern, склейка vs мусор, кэш hashCode

TOCTOU-безопасность, грабли intern() на масштабе, Compact Strings (грань — опыт)

4

Optional

обёртка вместо null

orElse жадный / orElseGet ленивый, of/ofNullable, не get()

только возвращаемый тип: не поле, не Serializable, не Optional<List>

5

counter++

race condition, synchronized

три операции, AtomicInteger на CAS

LongAdder под контеншеном; а нужен ли общий счётчик вообще

6

volatile

видеть актуальное значение

видимость без атомарности; happens-before (это Middle+)

safe publication, final-поля, почему DCL нужен volatile, когда volatile не нужен

7

Индексы

ускоряют поиск

типы, покрывающий/частичный, цена записи

SARGability, селективность и seq scan, CONCURRENTLY, чтение EXPLAIN

8

@Transactional

откат при ошибке, ACID

rollbackFor, прокси, self-invocation, propagation

держит соединение → вычерпывает пул → каскад; Outbox/Saga; MVCC и VACUUM


Никакой магии в «определении грейда» нет. Берёте любой вопрос и говорите вслух, пока есть что сказать.

Закончились на зелёном — это честный джун, и на старте абсолютно нормально; дальше растёт не список тем, а глубина по каждой. Дошли до жёлтого, можете объяснить, как оно устроено внутри и где ломается, — это мидл, и, к слову, большинство офферов закрывается именно тут. А красное — это уже не «знаю ещё больше внутренностей». Это когда вы сами, без наводящих, выходите на прод: что будет под нагрузкой, чем платим, когда это решение вообще не надо применять. Вот это и пишут в фидбэке словом «Senior».

Грейд — это не сколько фактов вы помните. Это где вы перестаёте говорить про код и начинаете про его последствия.

Грейд — это глубина ответа, а не список тем

Грейд — это глубина ответа, а не список тем

Та самая мысль одной картинкой: внутренности — это Middle, прод и трейд-оффы — Senior.


Это уже третий разбор про реальные банковские собесы. Senior System Design я по-прежнему должен, помню, он будет — просто это большой кусок, и наспех не хочется.

А пока проверим формат на вас. Возьмите в комментариях любой вопрос из восьми и допишите красную строку своими словами — как бы вы закрыли его на сеньорском уровне, через прод и трейд-оффы, а не через «ещё внутренности». Самое толковое утащу в отдельный разбор в канал, с указанием авторства. Не согласны со мной, где проходит граница мидла и сеньора, — тем интереснее, спорьте по делу, я тут не истина в последней инстанции.

И отдельное спасибо тем, кто честно признается, на какой строчке встал. Сказать себе «я пока на жёлтой» — это уже полпути до красной.


Пишу про разбор java-собесов и реальные вопросы, которые задают, без курсов и рекламы — канал @Java_Jub.