Вопросы на собеседовании Java junior и middle: 50 вопросов с ответами

Разбираем частые вопросы, ловушки и код-ревью Spring-сервиса

Вопросы на собеседовании Java junior и middle: 50 вопросов с ответами

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

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

ВАМ ПРИШЛО ПРИГЛАШЕНИЕ 💌
Приходите к нам в соцсети поделиться своим мнением и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!

Коротко

ТемаJuniorMiddle
Java CoreJDK, JRE, JVM; примитивы и ссылки; == и equals(); String и StringBuilderКонтракт equals() и hashCode(); final и неизменяемость; передача аргументов
ООПАбстрактный класс и интерфейс; перегрузка и переопределениеПорядок инициализации при наследовании; неизменяемые классы и record
ИсключенияChecked и unchecked; try-with-resources; finallyOutOfMemoryError; suppressed-исключения
КоллекцииИерархия Collection; ArrayList и LinkedList; устройство HashMapИзменение ключа в HashMap; ConcurrentHashMap; ConcurrentModificationException
Stream APIПромежуточные и терминальные операции; map() и flatMap(); OptionalЛовушки peek() и allMatch() на пустом стриме; функциональные интерфейсы
Современная JavaВерсии LTS; recordsealed-классы и pattern matching; виртуальные потоки и pinning
МногопоточностьRunnable и Callable; ExecutorServicevolatile и happens-before; монитор static-метода; deadlock; CAS
JVMОбласти памятиСборщик мусора; утечки памяти; загрузка классов
SpringIoC и DI; внедрение через конструкторScope бинов; @Transactional и его ловушки; Spring Boot 4
Hibernate и SQLLEFT JOIN и INNER JOINПроблема N+1; LazyInitializationException; уровни изоляции; блокировки

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

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

Дальше идёт блок теории. Формат зависит от компании. Одни прогоняют больше тридцати тем за полтора часа — это блиц, где важно не растеряться и успеть сказать главное по каждому пункту. Другие берут три-четыре темы и копают глубоко: если вопрос про HashMap, то с коллизиями, деревом и поведением при изменении ключа. 

Подготовка под эти два формата отличается. В блиц-режиме важнее структура ответа и способность переключаться. В глубоком режиме — готовность объяснить каждое «почему» на два уровня вниз.

После теории идёт live coding или код-ревью. Например, код-ревью Spring-сервиса встречается примерно в 8% интервью. Кандидату дают фрагмент на 50–200 строк и просят найти не меньше восьми проблем. Это ближе к реальной работе, чем задачи на алгоритмы. Многие джуны здесь теряются, потому что привыкли решать задачки, хотя на работе чаще читают чужой код.

В финале уже сам кандидат задаёт вопросы компании. По вопросам понятно, читал ли человек что-то о команде и проекте, или пришёл без подготовки.

Вопросы по Java Core

JDK, JRE и JVM — это три уровня одной пирамиды. JVM исполняет байт-код и управляет памятью. JRE включает JVM и стандартные библиотеки, которых хватает, чтобы запустить готовое приложение. JDK добавляет компилятор javac, отладчик и инструменты вроде jstack или jmap. На практике разработчику нужен JDK, а на сервере раньше хватало JRE. Сейчас отдельного JRE в поставке Oracle нет, поэтому рантайм собирают через jlink или ставят полный JDK.

Чем примитивные типы отличаются от ссылочных? [Junior]

Примитивы (int, boolean, double) хранят само значение и занимают фиксированный объём памяти. Ссылочные типы хранят ссылку на объект в куче, а переменная лежит в стеке или в поле другого объекта. Примитивы не могут быть null, у них нет методов, и они быстрее в операциях. Ссылочные типы дают полиморфизм, но добавляют накладные расходы на заголовок объекта и разыменование.

Подвох здесь связан с автоупаковкой. Кэш Integer работает в диапазоне от −128 до 127, и только в нём сравнение через == даёт true для одинаковых чисел. За пределами кэша создаются разные объекты, и == вернёт false, даже если значения равны. На собеседовании на этом ловят почти всегда.

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true, кэш

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false, разные объекты
System.out.println(c.equals(d)); // true

Почему String неизменяемый и когда нужен StringBuilder? [Junior] 

String неизменяемый, потому что его значение хранится в final-массиве, и после создания объект нельзя изменить. Это даёт безопасность при передаче строк между потоками, позволяет кэшировать hashCode и делает строки безопасными ключами в HashMap. 

Если нужно собрать строку из кусков в цикле, каждый + создаёт новый объект. StringBuilder меняет внутренний массив и не плодит мусор. В многопоточном коде используют StringBuffer, но на практике StringBuilder внутри одного потока закрывает почти все задачи.

Чем == отличается от equals()? [Junior] 

== сравнивает значения для примитивов и ссылки для объектов. equals() — метод, который класс может переопределить и задать свою логику сравнения. По умолчанию в Object он ведёт себя как ==. Строки, обёртки и коллекции переопределяют equals(), поэтому new String(“a”).equals(new String(“a”)) вернёт true, а == — false.

Какой контракт у equals() и hashCode()? [Junior/Middle]

Это самый частый вопрос выборки. Контракт простой. Если два объекта равны по equals(), у них обязан совпадать hashCode(). Обратное неверно: одинаковый hashCode не гарантирует равенство. Если equals() переопределён, а hashCode() нет, объекты, равные по содержимому, могут попасть в разные бакеты HashMap, и поиск сломается.

Подвох, который любят интервьюеры: что будет, если hashCode() всегда возвращает 42? Формально контракт не нарушен, но все элементы окажутся в одном бакете. Поиск выродится в перебор связного списка или дерева, и HashMap потеряет смысл. В худшем случае сложность станет линейной.

Что означает final для переменной, метода и класса? [Junior]

final-переменная не может быть переприсвоена. Для примитива это значит, что значение нельзя изменить. Для ссылки — что нельзя поменять саму ссылку, но объект внутри остаётся изменяемым. final-метод нельзя переопределить в наследнике. final-класс нельзя расширить. Подвох в том, что final List<String> list запрещает list = new ArrayList<>(), но не запрещает list.add(“x”).

Объекты передаются в метод по ссылке или по значению? [Junior]

В Java всегда передаётся значение. Для примитива копируется число. Для объекта копируется ссылка на него. Поэтому внутри метода можно изменить состояние объекта, на который указывает ссылка, но нельзя переприсвоить саму ссылку так, чтобы это увидел вызывающий код. Классический пример с перестановкой двух объектов в методе swap не сработает: снаружи ссылки останутся прежними.

void modify(List<String> list) {
    list.add("new");        // изменит исходный список
    list = new ArrayList<>(); // не повлияет на ссылку снаружи
}

Вопросы по ООП

ООП на собеседовании проверяют не через определения, а через код. Кандидата просят показать, где в его проекте есть абстракция, зачем понадобился интерфейс и что случилось бы без наследования. Если ответ сводится к пересказу учебника, интервьюер почти наверняка перейдёт к следующей теме.

Какие принципы ООП вы применяете в своём коде? [Junior]

Лучший ответ строится на примере. Например: в сервисе оплаты есть интерфейс PaymentProcessor с методом pay(), и несколько реализаций для карт, кошельков и кредитов. Это инкапсуляция деталей и полиморфизм: код, который вызывает pay(), не знает, какой именно способ оплаты выбран. Наследование применяется реже, потому что композиция чаще даёт более гибкую связь. Абстракция проявляется в том, что интерфейс описывает только контракт, а не внутреннее устройство.

Чем абстрактный класс отличается от интерфейса? [Junior]

Абстрактный класс может хранить состояние и содержать конструкторы. Интерфейс описывает контракт и до Java 8 не мог иметь реализации. Сейчас в интерфейсах есть default, static и private-методы, поэтому граница размылась. Абстрактный класс выбирают, когда нужна общая база с полями и частичной реализацией. Интерфейс — когда разные классы должны поддерживать одно поведение, но не имеют общего предка.

Чем перегрузка отличается от переопределения? [Junior]

Перегрузка (overload) — это несколько методов с одним именем, но разными параметрами в одном классе. Компилятор выбирает подходящий по типу аргументов. Переопределение (override) — это замена реализации метода из родительского класса в наследнике. Переопределение работает через динамическую диспетчеризацию во время выполнения, перегрузка разрешается на этапе компиляции.

В каком порядке инициализируются поля и блоки при наследовании? [Junior/Middle]

Сначала статические поля и статические блоки родителя, потом статические поля и блоки наследника. Затем при создании объекта: поля и нестатические блоки родителя, конструктор родителя, поля и нестатические блоки наследника, конструктор наследника. 

Это любимая ловушка многих компаний — в частности, Сбера. Если в конструкторе родителя вызывается переопределённый метод, а в наследнике поле ещё не инициализировано, метод увидит null или 0.

class Parent {
    Parent() { print(); }
    void print() { System.out.println("parent"); }
}
class Child extends Parent {
    int value = 42;
    Child() { super(); }
    @Override
    void print() { System.out.println(value); } // выведет 0, не 42
}

Как сделать класс неизменяемым? [Junior/Middle]

Поля делают private final, класс — final, чтобы его нельзя было расширить. Ссылочные поля копируют при создании и при возврате из геттеров (defensive copy). Если поле — изменяемая коллекция, её оборачивают в Collections.unmodifiableList() или копируют. 

С Java 16 для этого удобно использовать record, но record не делает класс глубоко неизменяемым: если внутри лежит массив, его содержимое всё ещё можно поменять.

Вопросы по исключениям

Исключения — тема, в которой джуны часто путают иерархию, а мидлы забывают про suppressed и поведение finally. На собеседовании важно назвать классы и показать, когда какое исключение уместно.

Чем checked-исключения отличаются от unchecked? [Junior]

Throwable делится на Error и Exception. Exception делится на RuntimeException и всё остальное. Checked-исключения наследуют Exception, но не RuntimeException. Компилятор требует обработать их или объявить в throws. Unchecked наследуют RuntimeException и не проверяются компилятором. Error вроде OutOfMemoryError указывает на проблему в самой JVM: память закончилась или сломался загрузчик классов. 

Ловить такое исключение бессмысленно, потому что после него приложение уже не может нормально работать. Единственное, что остаётся, — записать в лог факт падения и корректно завершить процесс. 

Как работает try-with-resources? [Junior]

Ресурс, реализующий AutoCloseable, закрывается автоматически в конце блока try. Если при закрытии возникает исключение, а в теле try уже было другое, второе исключение подавляется и добавляется в первое через getSuppressed(). Это позволяет не потерять исходную ошибку. До Java 7 приходилось закрывать ресурсы в finally и писать проверки на null.

Что вернёт метод, если return есть и в try, и в finally? [Junior]

Вернётся значение из finally. Если finally содержит return, он перекрывает возврат из try. Это считается плохой практикой, потому что теряется исходная логика и исключения. Компилятор такое пропускает, но на код-ревью за это бьют.

int test() {
    try { return 1; }
    finally { return 2; } // вернётся 2
}

Можно ли перехватывать OutOfMemoryError? [Middle]

Технически можно: OutOfMemoryError наследует Error, а catch (Throwable t) его поймает. Практического смысла мало. После OutOfMemoryError куча, скорее всего, в нестабильном состоянии, и попытка продолжить работу приведёт к новым ошибкам. Ловить его имеет смысл только для логирования и корректной остановки, но не для восстановления.

Вопросы по коллекциям Java

Коллекции — второй по частоте вопросов блок после Java Core. Здесь важно не только знать устройство, но и понимать, почему LinkedList на практике почти не используют, и что происходит с HashMap при изменении ключа.

Как устроена иерархия Collections Framework? [Junior]

На вершине Iterable, ниже Collection. От Collection идут List, Set и Queue. Map стоит отдельно и не наследует Collection. Это частая ловушка: HashMap не является Collection, хотя и работает с коллекциями. List хранит элементы по порядку и допускает дубли. Set не допускает дублей. Queue работает по правилам очереди.

Чем ArrayList отличается от LinkedList? [Junior]

ArrayList внутри держит массив. Доступ по индексу — O(1), вставка и удаление в середину — O(n) из-за сдвига. LinkedList — двусвязный список. Доступ по индексу — O(n), вставка и удаление по известной ссылке — O(1). На практике LinkedList проигрывает почти везде: из-за плохой локальности кэша и накладных расходов на узлы. Даже для очередей чаще берут ArrayDeque.

Как устроен HashMap? [Junior/Middle]

Внутри — массив бакетов. При вставке вычисляется hashCode ключа, хэш преобразуется, и элемент попадает в бакет. При коллизии элементы складываются в связный список. Когда в одном бакете набирается восемь элементов, список превращается в красно-чёрное дерево. При уменьшении до шести — обратно в список. 

Дерево строится, только если ёмкость таблицы не меньше 64. Иначе таблица сначала расширяется. Load factor по умолчанию 0,75: когда заполнение достигает 75%, массив увеличивается вдвое и элементы перераспределяются.

Что будет, если изменить ключ, который уже лежит в HashMap? [Middle]

Если ключ изменяемый и после вставки поменять поле, которое участвует в hashCode(), хэш станет другим. Элемент останется в старом бакете, но get() будет искать его в новом месте и не найдёт. Формально элемент есть, но достать его нельзя. Это ловушка ASTON. Поэтому ключи в HashMap делают неизменяемыми.

class Key {
    int id;
    Key(int id) { this.id = id; }
    @Override public int hashCode() { return id; }
    @Override public boolean equals(Object o) {
        return o instanceof Key && ((Key) o).id == id;
    }
}

Map<Key, String> map = new HashMap<>();
Key k = new Key(1);
map.put(k, "value");
k.id = 2;
System.out.println(map.get(k)); // null

Чем ConcurrentHashMap отличается от Collections.synchronizedMap()? [Middle]

synchronizedMap блокирует весь объект на каждой операции. ConcurrentHashMap использует более мелкую гранулярность: в современных версиях — CAS и блокировки на уровне бакетов, а не всей таблицы. Чтение в ConcurrentHashMap вообще не блокируется. Поэтому под нагрузкой он масштабируется лучше. Но ConcurrentHashMap не допускает null-ключей и значений, а synchronizedMap допускает.

Почему возникает ConcurrentModificationException? [Junior/Middle]

Итераторы большинства коллекций fail-fast: они проверяют счётчик модификаций. Если во время итерации коллекцию изменить напрямую, а не через итератор, счётчик изменится, и следующий вызов next() бросит исключение. Удалять элементы во время итерации нужно через iterator.remove(). В ConcurrentHashMap итераторы слабо консистентны и такого исключения не бросают.

Вопросы по Stream API

Stream API часто спрашивают у джунов: здесь видно, понимает ли человек разницу между описанием операции и её выполнением. Ловушки простые, но валят многих. 

Чем промежуточные операции отличаются от терминальных? [Junior]

Промежуточные операции (map, filter, sorted) ленивы. Они не выполняются, пока не вызвана терминальная операция (collect, forEach, count). Без терминальной операции стрим ничего не делает. Отсюда классический подвох: peek() без терминальной операции не выведет ничего, потому что конвейер не запущен.

Ещё один подвох: allMatch() на пустом стриме возвращает true. Логика формальная: нет элемента, который нарушил бы условие. На собеседовании это проверяют, чтобы увидеть, думает ли кандидат о граничных случаях.

Stream.of(1, 2, 3).peek(System.out::println); // ничего не выведет
Stream.<Integer>empty().allMatch(x -> x > 0); // true

Чем map() отличается от flatMap()? [Junior]

map() преобразует каждый элемент в другой объект: один к одному. flatMap() преобразует каждый элемент в стрим и затем «разглаживает» все стримы в один. Если map() вернёт Stream<Stream<String>>, flatMap() вернёт Stream<String>. flatMap() нужен, когда функция возвращает коллекцию или Optional для каждого элемента.

Зачем нужен Optional и как им пользоваться неправильно? [Junior]

Optional явно говорит, что значения может не быть, и заставляет вызывающий код это обработать. Неправильно — вызывать get() без проверки isPresent(). Это сводит защиту к нулю и приводит к NoSuchElementException. Лучше использовать orElse(), orElseGet(), orElseThrow() или функциональные цепочки. Optional не стоит применять для полей класса и параметров методов: он создан для возвращаемых значений.

Что такое функциональный интерфейс? [Junior]

Интерфейс с одним абстрактным методом. @FunctionalInterface — подсказка компилятору, но аннотация не обязательна. Лямбда-выражение реализует такой интерфейс без создания отдельного класса. Переменные, которые лямбда захватывает извне, должны быть effectively final: их значение не меняется после инициализации.

Вопросы по современной Java

Версии Java меняются каждые полгода, и на собеседовании middle уже спрашивают не только про record, но и про виртуальные потоки и sealed-классы. Важно не путать LTS с промежуточными релизами.

Какая версия Java сейчас LTS и что изменилось между 17, 21 и 25? [Junior/Middle]

На 2026 год актуальная LTS — Java 25. До неё LTS была Java 21, ещё раньше — Java 17. LTS выходит раз в два года, и на ней стоит строить продакшен: промежуточные релизы живут полгода и обновлений не получают.

Разница между 17 и 21 заметная. В 21 появились виртуальные потоки, pattern matching для switch и record-паттерны. В 25 к этому добавили scoped values — более предсказуемую замену ThreadLocal в ряде сценариев, компактные исходные файлы для маленьких программ и гибкие тела конструкторов. 

Плюс оптимизации в библиотеках и JIT: по данным Oracle, приложения на JDK 25 работают быстрее, чем на JDK 21, хотя конкретный выигрыш зависит от нагрузки.

Java 26 вышла 17 марта 2026 года. Это не LTS: обновлений безопасности для неё не будет, и в продакшен её ставят редко. Structured concurrency в ней всё ещё в превью — уже шестом. На собеседовании про новинки это стоит помнить: если спросят про Java 26, не говорите, что там есть стабильный API для structured concurrency. Java 27 вышла 15 сентября 2026 года, это тоже не LTS.

Что такое record и чем он отличается от обычного класса? [Junior/Middle]

record — компактный способ объявить неизменяемый класс данных. Компилятор сам генерирует конструктор, геттеры, equals(), hashCode() и toString(). Поля в record неявно final. Наследование от record запрещено, но record может реализовывать интерфейсы. 

Подвох: record не делает глубоко неизменяемым. Если компонент — массив, его элементы всё ещё можно изменить.

Зачем нужны sealed-классы и pattern matching в switch? [Middle]

sealed ограничивает круг наследников. Компилятор знает все возможные подтипы, и в switch по типу можно не писать default, если покрыты все варианты. Это даёт исчерпывающий анализ и ловит ошибки на этапе компиляции. Pattern matching позволяет проверять тип и сразу извлекать поля, без явных приведений.

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.r() * c.r();
        case Square sq -> sq.side() * sq.side();
    };
}

Что такое виртуальные потоки и когда они не дают выигрыша? [Middle]

Виртуальные потоки — лёгкие потоки, которыми управляет JVM, а не операционная система. На I/O-задачах, где поток часто блокируется в ожидании сети или диска, они дают масштабирование: можно держать миллионы виртуальных потоков без миллиона потоков ОС. На CPU-bound задачах выигрыша нет: процессор всё равно один, и количество ядер ограничивает параллелизм.

Отдельный вопрос — pinning. До Java 24 виртуальный поток «прилипал» к платформенному потоку, если блокировался внутри synchronized-метода или блока. JEP 491 в Java 24 устранил почти все такие случаи: теперь при блокировке внутри synchronized виртуальный поток освобождает носитель. Это важно для вопросов про конкурентность: старый совет «не используйте synchronized с виртуальными потоками» устарел, хотя остаточные сценарии pinning всё ещё существуют и диагностируются.

Вопросы по многопоточности

Многопоточность — блок, где junior отвечает на уровне «что такое поток», а middle показывает понимание модели памяти и умеет читать thread dump. Вопросы идут от простого к сложному, и на каждом уровне есть своя ловушка.

Чем Runnable отличается от Callable и зачем нужен ExecutorService? [Junior/Middle]

Runnable возвращает void и не может бросить checked-исключение. Callable<V> возвращает V и может бросить исключение. ExecutorService управляет пулом потоков: принимает задачи, распределяет их по потокам и позволяет получить результат через Future. Вручную создавать new Thread() на каждую задачу расточительно: потоки ОС дорогие, а пул переиспользует их.

Что гарантирует volatile и что такое happens-before? [Middle]

volatile гарантирует видимость записи для других потоков и запрещает переупорядочивание операций вокруг этой переменной. Он не делает операции атомарными: count++ для volatile int всё равно небезопасен, потому что это чтение, сложение и запись. 

Happens-before — правило, которое определяет, когда результат одной операции гарантированно виден другой. Запись в volatile происходит до последующего чтения этой же переменной. Захват и освобождение монитора тоже создают отношение happens-before.

На каком объекте берётся монитор у static-метода с synchronized и у обычного? [Middle]

У обычного synchronized-метода монитор — это this, текущий экземпляр. У static synchronized-метода монитор — объект Class, соответствующий классу. Поэтому два потока могут одновременно выполнять обычный и статический синхронизированный метод одного класса: они берут разные мониторы.

Как возникает deadlock и как его найти? [Middle]

Deadlock возникает, когда два или больше потоков держат ресурсы и ждут ресурсы, которые держат другие. Классика: поток A захватил lock1 и ждёт lock2, поток B захватил lock2 и ждёт lock1. 

Найти можно через thread dump: jstack покажет состояние потоков и заблокированные мониторы. В выводе будет строка Found one Java-level deadlock. Профилактика — упорядоченный захват блокировок и таймауты в tryLock().

Как работают Atomic-классы и CAS? [Middle]

AtomicInteger, AtomicLong и другие используют CAS (compare-and-swap) — атомарную операцию процессора. Она сравнивает текущее значение с ожидаемым и заменяет его на новое, только если они совпадают. Если не совпадают, операция повторяется в цикле. 

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

Чем thenApply() отличается от thenCompose() в CompletableFuture? [Middle]

thenApply() принимает функцию, которая возвращает обычное значение. thenCompose() принимает функцию, которая возвращает CompletableFuture. Если использовать thenApply() с функцией, возвращающей CompletableFuture, получится вложенная структура CompletableFuture<CompletableFuture<T>>. thenCompose() «разглаживает» её, как flatMap() в стримах.

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "user");
CompletableFuture<CompletableFuture<String>> nested =
    f1.thenApply(u -> CompletableFuture.supplyAsync(() -> u + "@mail"));
CompletableFuture<String> flat =
    f1.thenCompose(u -> CompletableFuture.supplyAsync(() -> u + "@mail"));

Вопросы по JVM и памяти

JVM и память — тема, в которой junior должен знать слова «heap» и «stack», а middle — объяснять, как устроен metaspace, почему случаются утечки в языке со сборщиком мусора и как загружаются классы. 

На какие области делится память JVM? [Junior/Middle]

Основные области — heap, stack, metaspace и native memory. Heap хранит объекты и делится на поколения в большинстве сборщиков. Stack хранит кадры вызовов и локальные переменные каждого потока. Metaspace хранит метаданные классов и заменяет PermGen, который убрали в Java 8. Native memory — всё, что JVM выделяет вне кучи: буферы, код JIT, структуры сборщика.

Как работает сборщик мусора? [Middle]

Сборщик находит объекты, до которых нет ссылок от корней (GC roots), и освобождает память. В поколенческих сборщиках молодые объекты собираются в Eden и survivor-областях, долгоживущие переезжают в old generation. 

G1 — сборщик по умолчанию с Java 9, он делит кучу на регионы и старается укладываться в паузы. ZGC нужен для очень больших куч и очень коротких пауз: он работает параллельно с приложением и на Java 25 даёт паузы в доли миллисекунды даже на терабайтных кучах. Выбор зависит от требований к задержкам и доступной памяти.

Бывают ли в Java утечки памяти? [Middle]

Да, несмотря на сборщик мусора. Объект не соберётся, если на него осталась ссылка, даже если логически он больше не нужен. Типичные источники: статические коллекции, которые растут и не очищаются; ThreadLocal, который не удалили после использования; незакрытые ресурсы; слушатели, которые не отписались. Утечку ищут через heap dump: jmap -dump или автоматический дамп при OutOfMemoryError, затем анализ в Eclipse MAT или VisualVM.

Как загружаются классы? [Middle]

ClassLoader загружает байт-код, связывает его и инициализирует класс. По умолчанию работает делегирование родителю: загрузчик сначала просит родителя загрузить класс и только потом пытается сам. Это защищает от подмены стандартных классов. Иерархия в стандартной Java: bootstrap, platform, system (application). Пользовательские загрузчики позволяют изолировать приложения или подгружать классы во время работы.

Полезный блок со скидкой

По промокоду: KOD (можно просто нажать) — вы получите скидку на все курсы Практикума.

На курсе Java-разработчик разбирают Java Core, Spring и многопоточность, а в расширенном тарифе есть отдельный блок подготовки к собеседованию. Тем, кто уже пишет на Java и метит в мидлы, подойдёт курс Мидл Java-разработчик: там работают с PostgreSQL через Spring JDBC и JPA и ведут миграции в Liquibase.

Бесплатные вводные курсы в Практикуме тоже есть — по всем направлениям, от Python до аналитики.

Вопросы по Spring

Вопросы по Spring звучат почти на каждом собеседовании для миддла. Важно разбираться в аннотациях и понимать, как прокси и контейнер меняют поведение кода. Главная ловушка — @Transactional: вопрос встречается примерно в 13% интервью, и правильно с первой попытки отвечают единицы. 

Что такое IoC и DI и почему внедрение через конструктор лучше, чем через поле? [Junior/Middle]

IoC (инверсия управления) означает, что объект не создаёт зависимости сам, а получает их извне. DI — конкретный способ это сделать. Внедрение через конструктор делает зависимости явными: без них объект не скомпилируется. 

Поля можно сделать final, что защищает от случайной замены. Такой класс проще тестировать без Spring: зависимости передаются вручную. Внедрение через поле скрывает зависимости и требует рефлексии для тестов.

Какие scope бывают у бинов и как внедрить prototype в singleton? [Middle]

Основные scope: singleton (один бин на контейнер), prototype (новый экземпляр при каждом запросе), а также веб-scope вроде request и session. Если singleton зависит от prototype, при внедрении через поле или конструктор prototype создастся один раз и останется тем же. Решения: @Lookup, ObjectFactory<T>, Provider<T> или scoped proxy. @Lookup подменяет метод, и каждый вызов возвращает новый экземпляр.

Как работает @Transactional и чем отличаются REQUIRED, REQUIRES_NEW и NESTED? [Middle]

Spring оборачивает бин в прокси и открывает транзакцию до вызова метода, а коммитит или откатывает после. REQUIRED присоединяется к текущей транзакции или создаёт новую, если её нет. REQUIRES_NEW приостанавливает текущую и всегда открывает новую. NESTED создаёт savepoint внутри текущей транзакции: при откате вложенной части откатывается только она, внешняя может продолжить.

Почему @Transactional не срабатывает при вызове метода из того же класса? [Middle]

Прокси перехватывает только внешние вызовы. Если метод a() вызывает this.b() внутри того же класса, вызов идёт напрямую через this, минуя прокси. Аннотация на b() не срабатывает. Это самая частая причина, почему транзакция «не открылась». Решения: вынести метод в другой бин, использовать self-injection или AopContext.currentProxy().

@Service
class PaymentService {
    @Transactional
    public void pay() {
        this.log(); // вызов мимо прокси, @Transactional на log() не сработает
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log() { }
}

Что изменилось в Spring Boot 4? [Middle]

Spring Boot 4.0 вышел 20 ноября 2025 года на базе Spring Framework 7. Кодовая база разделена на модули, jars стали меньше и специализированнее. Добавлена поддержка JSpecify для null-safety, API versioning и HTTP Service Clients. 

Минимальная версия Java осталась 17, но Java 25 поддерживается первоклассно. Бесплатная поддержка Spring Boot 3.5 закончилась в июне 2026 года: последний OSS-релиз ветки вышел 25 июня, и дальше обновлений без коммерческой подписки не будет. Поэтому переход на четвёрку — практическая необходимость, для тех, кто хочет получать патчи безопасности. 

Вопросы по Hibernate и базам данных

На собеседовании уровня middle вопросы по Hibernate и SQL строятся вокруг проблем, которые встречаются в продакшене: N+1, ленивая загрузка, уровни изоляции и выбор блокировки. Джуну достаточно понимать разницу между видами JOIN и знать, что такое транзакция. 

Что такое проблема N+1 и как её решить? [Middle]

N+1 возникает, когда приложение загружает список сущностей одним запросом, а затем для каждой сущности делает отдельный запрос за связанными данными. Один плюс N запросов вместо одного. 

Решения: JOIN FETCH в JPQL, @EntityGraph или настройка batch size, которая группирует загрузку связей. 

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

Почему возникает LazyInitializationException? [Middle]

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

Решения: загрузить связь внутри транзакции (JOIN FETCH, @EntityGraph), использовать DTO и не возвращать сущности наружу, или открыть сессию на время рендеринга, что обычно считается костылём.

Какие бывают уровни изоляции транзакций? [Middle]

Стандарт SQL описывает четыре уровня: Read Uncommitted, Read Committed, Repeatable Read и Serializable. Read Uncommitted допускает грязное чтение. Read Committed защищает от грязного чтения, но допускает неповторяющееся. Repeatable Read гарантирует, что повторное чтение в транзакции вернёт те же данные, но в стандарте допускает фантомы. Serializable исключает все аномалии ценой производительности. 

В PostgreSQL Read Uncommitted фактически отсутствует: он ведёт себя как Read Committed. Repeatable Read за счёт MVCC защищает и от фантомов, что отличается от формального стандарта.

Когда выбирать оптимистичную блокировку, а когда пессимистичную? [Middle]

Оптимистичная блокировка предполагает, что конфликты редки. Используется @Version: при обновлении Hibernate проверяет, что версия не изменилась, и бросает исключение при конфликте. Она хороша для редактирования профилей, статей и других сценариев с низкой конкуренцией.

Пессимистичная блокировка (SELECT FOR UPDATE) блокирует строку на время транзакции. Она нужна для платежей, списания баланса и других операций, в которых конфликт ожидаем и повторная попытка обойдётся дороже, чем ожидание. При высокой конкуренции, например в массовых платежах, пессимистичная блокировка проще и предсказуемее: не нужно обрабатывать исключения версий и повторять транзакцию.

Чем LEFT JOIN отличается от INNER JOIN? [Junior]

INNER JOIN возвращает только строки, у которых есть совпадение в обеих таблицах. LEFT JOIN возвращает все строки из левой таблицы и совпадающие из правой, а где совпадений нет, подставляет NULL. 

Подвох: если условие на правую таблицу поставить в WHERE, а не в ON, LEFT JOIN превращается во внутренний. Строки с NULL отфильтруются, и левое соединение потеряет смысл.

-- LEFT JOIN, который стал INNER
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.active = true; -- отбросит заказы без пользователя

Задачи на live coding и код-ревью

Код-ревью на собеседовании проверяет, умеет ли кандидат видеть последствия в чужом коде. Обычно дают фрагмент на 50–200 строк и просят найти не меньше восьми проблем. Чаще всего попадается Spring-сервис с ошибками, которые встречаются в продакшене. 

Короткий пример на 20 строк, написанный для статьи, чтобы было на чём тренироваться. 

@Service
public class PaymentService {
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private PaymentRepository paymentRepository;
    private double amount;
    public void pay(Long userId, double sum) {
        System.out.println("pay " + userId);
        List<Payment> all = paymentRepository.findAll();
        Payment p = all.stream().filter(x -> x.getUserId().equals(userId))
                       .findFirst().orElse(null);
        restTemplate.postForObject("http://bonus/api", sum, String.class);
        p.setAmount(sum);
        paymentRepository.save(p);
    }
}

Что здесь не так:

  • Внедрение через поле: @Autowired на private RestTemplate и PaymentRepository. Такой класс сложнее тестировать, а зависимости не видны в конструкторе. Внедрение через конструктор делает их явными и позволяет объявить поля final.
  • double для денег. Двоичная дробь не представляет копейки точно, и на сотне платежей накапливается расхождение. Для сумм нужен BigDecimal.
  • System.out.println вместо логгера. Такая строка не попадёт в общий лог, у неё нет уровня, и фильтровать её потом нечем.
  • findAll() вместо findByUserId(). Метод тянет все платежи из базы, хотя нужен один. На большой таблице это лишняя нагрузка и память.
  • orElse(null), а следом p.setAmount(sum) без проверки. Если платежа нет, следующая строка упадёт с NullPointerException. Правильнее orElseThrow() с понятным исключением.
  • Внешний HTTP-вызов до сохранения. Если бонусный сервис ответит ошибкой, платёж уже уйдёт наружу, а в базе останется старое значение. Данные разойдутся.
  • Внешний вызов внутри транзакции. Если метод транзакционный, соединение с базой держится, пока идёт сетевой запрос. Под нагрузкой пул соединений заканчивается, и падает всё приложение.
  • RestTemplate как поле без настройки. По умолчанию у него нет таймаутов, и зависший внешний сервис повесит поток навсегда.

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

Как подготовиться к собеседованию Java

Выстраивайте подготовку с учетом частоты тем. equals() и hashCode() всплывают примерно в 17% интервью, HashMap — в 15%, @Transactional — в 13%. Эти темы дают больше отдачи, чем редкие вопросы про экзотические API. Начинайте с них, а полное оглавление учебника оставьте на потом.

Проговаривайте ответы вслух. На собеседовании вы говорите. Формулировка «я знаю» не работает, если из неё не складывается объяснение за две минуты. Каждый ответ привязывайте к примеру из своего кода: где вы это использовали, что сломалось, как исправили.

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

Разберите чужой код. Найдите открытые примеры плохого кода или возьмите свой старый сервис. Ищите синтаксические ошибки и архитектурные проблемы: транзакции, N+1, обработку null, логирование.

Частые вопросы

Сколько длится техническое собеседование Java?

Обычно от 60 до 90 минут. Блиц-формат укладывается в полтора часа и охватывает 30+ тем. Глубокое интервью может длиться час, но концентрируется на нескольких темах.

Спрашивают ли алгоритмы на junior?

Реже, чем принято думать. Акцент обычно на Java Core, коллекциях, многопоточности и Spring. Алгоритмы встречаются, но чаще в компаниях с отдельным алгоритмическим этапом. Для junior важнее уверенно объяснить HashMap и написать простой цикл, чем решать задачи на динамическое программирование.

Какую версию Java учить в 2026 году?

Java 25 LTS. Она актуальна и поддерживается. Java 21 всё ещё встречается в проектах, но для собеседования знание различий 21 и 25 показывает, что вы следите за версиями. Java 17 как минимальная версия Spring Boot 4 тоже остаётся релевантной.

Можно ли пользоваться ИИ на собеседовании?

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

Что делать, если не знаете ответа?

Сказать прямо и показать, как бы вы искали ответ. «Я не помню точную сигнатуру, но знаю, что это связано с конкурентностью, и стал бы смотреть в сторону java.util.concurrent» звучит лучше, чем попытка угадать. Интервьюер проверяет не только знания, но и поведение в неопределённости.

Собеседование Java junior и middle в 2026 году проверяет не набор заученных фактов, а способность объяснить, как код работает и где ломается. Версии Java и Spring меняются, но вопросы про HashMap, @Transactional и виртуальные потоки остаются, потому что за ними стоят реальные проблемы продакшена. Готовьтесь по частоте, проговаривайте ответы вслух и разбирайте чужой код — тогда вопросы на собеседовании Java перестанут быть сюрпризом. Курс «Java-разработчик» Практикума помогает пройти этот путь системно, с практикой и разбором типичных ошибок.

Советуем дополнительно почитать

Монолит и микросервисы: как выбрать архитектуру проекта — тема, в которую переходят middle-интервью после Spring и транзакций.

SQL-инъекции: механика атак, типы и методы защиты с примерами — параметризованные запросы и ORM как защита. Частый вопрос в связке с Hibernate.

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

Как перейти из 1С в Java: что поменяется в инструментах и зарплате — IntelliJ IDEA, Git и CI/CD в Java-команде.

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

Бонус для читателей

Если готовитесь к собеседованию и хотите закрыть пробелы системно, а не по разрозненным статьям, — держите промокод Практикума на любой платный курс: KOD. Он даст скидку при покупке и позволит сэкономить на обучении. Курс «Java-разработчик» разбирает темы из этой статьи на практике: коллекции, многопоточность, Spring и работу с базами.

Бесплатные курсы в Практикуме тоже есть — по всем специальностям и направлениям, начать можно в любой момент, карту привязывать не нужно.

Вам может быть интересно