Prog Academy
RU UA
Вопросы на собеседовании Java-разработчика: 40 вопросов с ответами для Junior
Карьера в IT

Вопросы на собеседовании Java-разработчика: 40 вопросов с ответами для Junior

Короткий ответ: на собеседовании Junior Java Developer проверяют три вещи — понимаете ли вы Java Core (ООП, строки, коллекции, исключения), можете ли объяснить, что делает ваш код, и знаете ли базовый рабочий стек: SQL, Spring Boot, Git. В статье 40 вопросов с короткими ответами, сгруппированных по семи темам, — в том виде, в каком их задаёт интервьюер, с кодом, который можно запустить.

Я преподаю программирование больше 12 лет, и разбор вопросов и задач с собеседований входит в программу курса Java. По моему опыту, кандидаты чаще спотыкаются не на редких темах, а на базовых: человек пользуется HashMap каждый день, но не может объяснить, зачем переопределять hashCode(). Поэтому ниже — не «все вопросы по Java», а те, на которых проверяется понимание. К каждому ответу я добавил, что именно хочет услышать интервьюер.

Актуально на октябрь 2026 года

Ответы верны для актуальных LTS-версий Java — 17, 21 и 25. Весь код на Java из статьи запущен на OpenJDK 25.0.2, SQL-запросы — в SQLite 3.51; вывод в примерах настоящий. Пример с JDBC скомпилирован, но не запускался (нужна база и драйвер); примеры со Spring минимальны и локально не запускались. Отбор вопросов — авторский, по опыту преподавания, а не статистика частоты.

Как проходит собеседование Java-разработчика?

Обычно собеседование на Junior Java Developer состоит из четырёх этапов: скрининг с рекрутером, техническое интервью, практическое задание и финальная встреча. Порядок и количество этапов у компаний разные: где-то тестовое дают до технического интервью, где-то всё проходит за один созвон. Уточните формат у рекрутера на первом же разговоре.

ЭтапЧто проверяютКак готовиться
Скрининг с рекрутеромМотивацию, опыт, уровень английского, ожидания по формату работы и оплатеРассказ о себе на две минуты, рассказ о проекте, ответ на вопрос «почему Java»
Техническое интервьюJava Core, коллекции, многопоточность, SQL, Spring; умение рассуждать вслухОтвечать на вопросы из этой статьи вслух, а не про себя; каждый ответ — с примером
Практическое заданиеLive coding, разбор чужого кода или тестовое на дом: читаемость, тесты, работа с GitРешать небольшие задачи без автодополнения, проговаривая ход решения
Финальная встречаСовместимость с командой, вопросы кандидата, условияПодготовить свои вопросы о проекте, команде и процессе адаптации

Ориентиры по оплате лучше смотреть до скрининга: медианные зарплаты по уровням собраны на странице зарплаты Java-разработчиков. Резюме под вакансию удобно собрать в конструкторе резюме.

Какие темы спрашивают у Junior и Middle?

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

ТемаЧто проверяютУровень
Java Core и ООПТипы данных, передача параметров, принципы ООП, интерфейсы и абстрактные классы, static и finalJunior
String, equals/hashCodeПул строк, сравнение объектов, контракт equals() и hashCode(), неизменяемые классы и recordsJunior
КоллекцииВыбор между ArrayList, LinkedList, HashMap, TreeMap, HashSet; сложность операцийJunior
Устройство HashMapБакеты, коллизии, расширение таблицы, изменяемые ключиJunior / Middle
Исключения и genericsChecked и unchecked, try-with-resources, стирание типов, wildcard-ыJunior; wildcard-ы — Middle
Лямбды и Stream APIФункциональные интерфейсы, промежуточные и терминальные операции, ленивостьJunior
МногопоточностьСоздание потоков, synchronized, volatile, deadlock, ExecutorService, виртуальные потокиJunior — основы; Middle — глубже
JVM и памятьСтек и куча, сборка мусора, утечки памятиJunior — основы; Middle — настройка и диагностика
SQL, JDBC, JPAJOIN, группировка, транзакции, PreparedStatement, состояния сущностей, проблема N+1Junior; N+1 — Middle
Spring и Spring BootIoC и DI, бины и области видимости, REST-контроллер, @TransactionalJunior — основы; Middle — прокси и автоконфигурация

Java Core и ООП

С этого блока техническое интервью обычно начинается: вопросы простые, но по ответам сразу видно, выучил человек определения или понимает, как работает Java. Первоисточник для всех ответов — спецификация языка Java и учебник dev.java.

Чем отличаются JDK, JRE и JVM?

JVM — виртуальная машина, которая выполняет байт-код. JRE — это JVM плюс стандартная библиотека классов, то есть всё, что нужно для запуска программы. JDK — это JRE плюс инструменты разработчика: компилятор javac, jar, javadoc, отладчик, jshell.

Исходный файл .java компилируется в байт-код .class, а JVM интерпретирует его и компилирует «горячие» участки в машинный код (JIT). Поэтому один и тот же .jar работает на любой системе, где есть JVM. Что хотят услышать: начиная с Java 11 Oracle не выпускает отдельный JRE — ставят JDK, а урезанный образ среды выполнения при необходимости собирают утилитой jlink.

Какие примитивные типы есть в Java и чем они отличаются от ссылочных?

Примитивных типов восемь: byte, short, int, long, float, double, char и boolean. Переменная примитивного типа хранит само значение, а переменная ссылочного типа — ссылку на объект в куче; ссылка может быть null, примитив — нет.

У каждого примитива есть класс-обёртка (Integer, Long, Boolean…), и компилятор сам преобразует одно в другое — это autoboxing и unboxing. В коллекциях и generics можно использовать только обёртки. Типичная ошибка: забыть, что распаковка null бросает NullPointerException, — например, int count = map.get(key), когда ключа нет.

Как в Java передаются параметры: по значению или по ссылке?

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

import java.util.ArrayList;
import java.util.List;

public class PassByValue {
    static void change(int number, List<String> list) {
        number = 100;              // changes only the local copy
        list.add("added");         // changes the object both references point to
        list = new ArrayList<>();  // reassigns only the local copy of the reference
        list.add("lost");
    }

    public static void main(String[] args) {
        int number = 1;
        List<String> list = new ArrayList<>();
        change(number, list);
        System.out.println(number); // 1
        System.out.println(list);   // [added]
    }
}

Что хотят услышать: слова «копия ссылки». Ответ «объекты передаются по ссылке» считается неточным: при передаче по ссылке строка list = new ArrayList<>() изменила бы и переменную в main.

Назовите принципы ООП и объясните их на примере

Принципов четыре: инкапсуляция, наследование, полиморфизм и абстракция. Инкапсуляция — состояние объекта скрыто за методами (поля private, изменения только через проверяющие методы). Наследование — класс получает поля и методы родителя через extends. Полиморфизм — код работает с типом-родителем или интерфейсом, а выполняется реализация конкретного объекта. Абстракция — наружу выставляется только то, что нужно пользователю класса.

Пример из стандартной библиотеки убедительнее, чем «кошка наследует животное»: переменная типа List может ссылаться на ArrayList или LinkedList, и вызывающему коду всё равно — это полиморфизм и абстракция сразу. Подробнее о парадигме — в глоссарии: ООП. Что хотят услышать: пример из вашего проекта и понимание, что наследование часто стоит заменять композицией.

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

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

С Java 8 в интерфейсах есть default- и static-методы, с Java 9 — private-методы, так что наличие реализации больше не отличие. Практическое правило: интерфейс описывает, что объект умеет (Comparable, Runnable), абстрактный класс — общую основу для близких классов с общими полями. Плюс к ответу: упомянуть sealed-классы и интерфейсы (Java 17), которые ограничивают список наследников.

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

Перегрузка (overloading) — несколько методов с одним именем, но разными параметрами в одном классе; какой вызвать, компилятор решает по объявленным типам аргументов. Переопределение (overriding) — наследник даёт свою реализацию метода родителя с той же сигнатурой; какую реализацию вызвать, решается во время выполнения по реальному типу объекта.

public class Overload {
    static class Animal {
        String sound() { return "..."; }
    }

    static class Cat extends Animal {
        @Override
        String sound() { return "Meow"; }
    }

    static String describe(Object o) { return "Object"; }
    static String describe(String s) { return "String"; }

    public static void main(String[] args) {
        Animal animal = new Cat();
        System.out.println(animal.sound());   // overriding: chosen at runtime

        Object value = "text";
        System.out.println(describe(value));  // overloading: chosen at compile time
        System.out.println(describe("text"));
    }
}

Программа выводит Meow, Object, String. Вторая строка — та самая ловушка: переменная объявлена как Object, поэтому выбран describe(Object), хотя внутри лежит строка. Что хотят услышать: «перегрузка — на этапе компиляции, переопределение — в рантайме» и зачем нужна аннотация @Override: компилятор проверит, что метод действительно что-то переопределяет.

Что означают модификаторы static и final?

static — член принадлежит классу, а не объекту: статическое поле одно на все экземпляры, статический метод вызывается без создания объекта и не имеет доступа к this. final запрещает изменение: переменную нельзя присвоить повторно, метод нельзя переопределить, от класса нельзя наследоваться.

Типичная ошибка — считать, что final делает объект неизменяемым. Он фиксирует только ссылку: в final List<String> names = new ArrayList<>() можно вызывать names.add(...), нельзя только написать names = другойСписок. Ещё один частый вопрос — почему статический метод нельзя переопределить: он привязан к классу, поэтому в наследнике он скрывается (hiding), а не переопределяется, и полиморфизм на него не действует.

String, equals/hashCode и неизменяемость

Блок про строки и сравнение объектов — самый «ловушечный»: здесь любят спрашивать «что выведет этот код». Поведение описано в документации классов String и Object.

Почему String неизменяемый и что такое пул строк?

Объект String нельзя изменить после создания: любой метод вроде toUpperCase() или concat() возвращает новую строку. Это делает строки безопасными как ключи HashMap, позволяет свободно делить их между потоками и хранить одинаковые литералы в одном экземпляре — в пуле строк.

public class StringPool {
    public static void main(String[] args) {
        String a = "java";
        String b = "java";
        String c = new String("java");

        System.out.println(a == b);          // true: one object from the string pool
        System.out.println(a == c);          // false: new always creates a new object
        System.out.println(a.equals(c));     // true: same characters
        System.out.println(a == c.intern()); // true: intern() returns the pooled string
    }
}

Литералы попадают в пул автоматически, а new String(...) всегда создаёт отдельный объект в куче. Что хотят услышать: строки сравнивают через equals(), а == для них сравнивает ссылки.

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

Оператор == для примитивов сравнивает значения, а для объектов — ссылки, то есть проверяет, один ли это объект. Метод equals() сравнивает объекты по смыслу, если класс его переопределил; реализация по умолчанию в Object делает то же, что ==.

public class IntegerCache {
    public static void main(String[] args) {
        Integer a = 127, b = 127;
        Integer c = 128, d = 128;

        System.out.println(a == b);      // true: values from -128 to 127 are cached
        System.out.println(c == d);      // false: two different objects
        System.out.println(c.equals(d)); // true
    }
}

Это классическая задача про кеш Integer: при автоупаковке значения от −128 до 127 всегда дают один и тот же объект — так требует спецификация языка, раздел 5.1.7, — а за пределами диапазона создаются разные объекты. Вывод: обёртки, как и строки, сравнивают только через equals().

Какой контракт между equals() и hashCode()?

Если два объекта равны по equals(), у них обязан быть одинаковый hashCode(). Обратное неверно: одинаковый хеш у разных объектов допустим — это коллизия. Поэтому оба метода переопределяют вместе и по одним и тем же полям.

import java.util.HashSet;
import java.util.Set;

public class EqualsHashCode {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }

        @Override
        public boolean equals(Object o) {
            return o instanceof Point p && x == p.x && y == p.y;
        }
        // hashCode() is not overridden: the contract is broken
    }

    record SafePoint(int x, int y) {} // equals() and hashCode() are generated together

    public static void main(String[] args) {
        Set<Point> points = new HashSet<>();
        points.add(new Point(1, 2));
        System.out.println(points.contains(new Point(1, 2)));         // false

        Set<SafePoint> safePoints = new HashSet<>();
        safePoints.add(new SafePoint(1, 2));
        System.out.println(safePoints.contains(new SafePoint(1, 2))); // true
    }
}

HashSet сначала ищет бакет по хешу и только потом вызывает equals(). У двух объектов Point хеши из Object разные, поэтому поиск уходит в другой бакет и элемент «теряется». У equals() свои требования: рефлексивность, симметричность, транзитивность, согласованность и x.equals(null) == false. Что хотят услышать: на практике методы генерирует IDE, Objects.hash(...) или record.

В чём разница между String, StringBuilder и StringBuffer?

String неизменяем, StringBuilder и StringBuffer — изменяемые буферы символов для сборки строки по частям. Методы StringBuffer синхронизированы, поэтому он потокобезопасен, но медленнее; StringBuilder без синхронизации — выбор по умолчанию.

Конкатенация через + в цикле создаёт новую строку на каждой итерации, поэтому в циклах используют StringBuilder.append(). Вне цикла a + b + c писать нормально: компилятор сам превращает такое выражение в эффективный код. Для склейки с разделителем удобнее String.join() или Collectors.joining().

Как сделать класс неизменяемым и что такое record?

Неизменяемый (immutable) класс — тот, состояние объектов которого нельзя изменить после создания. Для этого класс объявляют final, все поля — private final, не пишут сеттеры, а изменяемые поля (списки, массивы, даты) копируют в конструкторе и не отдают наружу оригиналы.

import java.util.ArrayList;
import java.util.List;

public class Immutable {
    record Order(String id, List<String> items) {
        Order {
            items = List.copyOf(items); // defensive copy, unmodifiable
        }
    }

    public static void main(String[] args) {
        List<String> source = new ArrayList<>(List.of("book"));
        Order order = new Order("A-1", source);

        source.add("pen");                 // does not affect the record
        System.out.println(order);

        try {
            order.items().add("laptop");
        } catch (UnsupportedOperationException e) {
            System.out.println("items cannot be modified");
        }
    }
}

Программа выводит Order[id=A-1, items=[book]] и items cannot be modified. Record (в языке с Java 16) — краткая запись такого класса: поля final, конструктор, методы доступа, equals(), hashCode() и toString() создаются автоматически. Типичная ошибка: считать record неизменяемым «целиком». Он неизменяем поверхностно — без List.copyOf в примере список внутри можно было бы изменить извне.

Коллекции

Коллекции — главный блок собеседования на Junior: по ним проверяют и знание библиотеки, и понимание сложности операций. Сложность и гарантии каждой коллекции описаны в её Javadoc, например у ArrayList и HashMap.

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

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

ОперацияArrayListLinkedList
get(i)O(1)O(n)
Добавление в конецO(1) амортизированноO(1)
Вставка или удаление в началеO(n)O(1)
Вставка или удаление в серединеO(n) — сдвиг элементовO(n) — поиск позиции

Что хотят услышать: на практике почти всегда выбирают ArrayList — массив компактнее в памяти и быстрее обходится. А когда нужна очередь или стек, берут ArrayDeque, а не LinkedList.

Как устроен HashMap?

HashMap — это массив бакетов. При put(key, value) вычисляется key.hashCode(), из него получается индекс бакета, и пара кладётся туда. Если в бакете уже есть элементы (коллизия), ключи сравниваются через equals(): совпал — значение заменяется, не совпал — пара добавляется в цепочку.

Детали, которые спрашивают дальше:

  • начальная ёмкость по умолчанию — 16, коэффициент загрузки — 0,75; когда элементов становится больше, чем ёмкость × 0,75, таблица удваивается и элементы перераспределяются;
  • в OpenJDK длинная цепочка в бакете (от 8 элементов при таблице от 64 бакетов) превращается в красно-чёрное дерево, и худший случай поиска становится O(log n) вместо O(n);
  • get и put в среднем работают за O(1);
  • допускается один ключ null и сколько угодно значений null; порядок обхода не гарантирован;
  • HashMap не потокобезопасен — для многопоточного доступа есть ConcurrentHashMap.

Что хотят услышать: связку «hashCode выбирает бакет, equals находит ключ внутри бакета».

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

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MutableKey {
    public static void main(String[] args) {
        Map<List<String>, String> map = new HashMap<>();
        List<String> key = new ArrayList<>(List.of("a"));
        map.put(key, "value");

        key.add("b"); // hashCode of the key has changed

        System.out.println(map.get(key));          // null
        System.out.println(map.containsKey(key));  // false
        System.out.println(map.size());            // 1: the entry is still inside
    }
}

Отсюда правило: ключи HashMap и элементы HashSet должны быть неизменяемыми — String, Integer, enum, record с неизменяемыми полями. Этот вопрос связывает сразу три темы: контракт hashCode(), устройство таблицы и неизменяемость.

Чем отличаются HashSet, LinkedHashSet и TreeSet?

Все три хранят только уникальные элементы, а отличаются порядком и ценой операций. HashSet не гарантирует порядок, операции в среднем O(1). LinkedHashSet сохраняет порядок добавления, тоже O(1). TreeSet держит элементы отсортированными, операции — O(log n).

Для списка pear, apple, kiwi, apple, banana на OpenJDK 25 получилось: HashSet — [banana, apple, pear, kiwi], LinkedHashSet — [pear, apple, kiwi, banana], TreeSet — [apple, banana, kiwi, pear]. Что хотят услышать: HashSet внутри — это HashMap, где элементы хранятся как ключи; элементы TreeSet должны реализовывать Comparable, либо в конструктор передают Comparator. То же разделение у HashMap, LinkedHashMap и TreeMap.

Что такое Iterator и почему возникает ConcurrentModificationException?

Iterator — объект для последовательного обхода коллекции с методами hasNext(), next() и remove(); цикл for-each использует его неявно. ConcurrentModificationException возникает, когда коллекцию изменили во время обхода в обход итератора.

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;

public class Cme {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));

        try {
            for (Integer n : numbers) {
                if (n % 2 == 0) numbers.remove(n); // modifies the list during iteration
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("ConcurrentModificationException");
        }

        List<Integer> fixed = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
        fixed.removeIf(n -> n % 2 == 0); // or Iterator.remove() inside a while loop
        System.out.println(fixed);       // [1, 3, 5]
    }
}

Несмотря на название, второй поток для этой ошибки не нужен. Итераторы стандартных коллекций fail-fast: они сверяют внутренний счётчик изменений и бросают исключение при расхождении. Что хотят услышать: два правильных способа удаления — Iterator.remove() и removeIf(), — и что эта проверка не гарантирована, поэтому строить на ней логику программы нельзя.

В чём разница между Comparable и Comparator?

Comparable реализует сам класс: метод compareTo() задаёт его естественный порядок, и такой порядок один. Comparator — отдельный объект с методом compare(): их может быть сколько угодно, и они позволяют сортировать даже чужие классы, которые изменить нельзя.

Естественный порядок используют list.sort(null), Collections.sort(list) и TreeSet без компаратора. Компаратор удобно собирать из готовых частей: Comparator.comparingInt(Employee::age).thenComparing(Employee::name). Типичная ошибка: писать в сравнении return a.age - b.age — при больших по модулю значениях возможно переполнение; правильно Integer.compare(a.age, b.age).

Исключения, generics, лямбды и Stream API

В этом блоке проверяют, умеете ли вы писать современный код на Java: аккуратно обрабатывать ошибки, пользоваться типизированными коллекциями и стримами. Учебные разделы первоисточника — исключения и generics на dev.java.

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

Checked-исключения компилятор заставляет обработать или объявить в throws; unchecked — нет. К unchecked относятся RuntimeException и её наследники, а также Error; всё остальное, что наследуется от Exception, — checked. Правила описаны в разделе 11 спецификации.

По смыслу: checked — ожидаемые внешние сбои, после которых можно восстановиться (IOException, SQLException); unchecked — ошибки в коде (NullPointerException, IllegalArgumentException, IndexOutOfBoundsException); Error — проблемы самой JVM (OutOfMemoryError, StackOverflowError), которые обычно не перехватывают. Типичная ошибка: пустой блок catch или catch (Exception e) «на всякий случай» — ошибка проглатывается, и её потом невозможно найти.

Всегда ли выполняется finally и что такое try-with-resources?

Блок finally выполняется и после нормального завершения try, и после исключения, и даже если в try стоит return. Он не выполнится, только если JVM остановится раньше — например, из-за System.exit() или аварийного завершения процесса. try-with-resources — конструкция, которая сама закрывает ресурсы, реализующие AutoCloseable.

public class FinallyDemo {
    static class Resource implements AutoCloseable {
        private final String name;
        Resource(String name) { this.name = name; System.out.println("open " + name); }

        @Override
        public void close() { System.out.println("close " + name); }
    }

    static int value() {
        try {
            return 1;
        } finally {
            System.out.println("finally runs before the method returns");
        }
    }

    public static void main(String[] args) {
        System.out.println(value());

        try (Resource first = new Resource("first"); Resource second = new Resource("second")) {
            throw new IllegalStateException("failure");
        } catch (IllegalStateException e) {
            System.out.println("caught: " + e.getMessage());
        }
    }
}
finally runs before the method returns
1
open first
open second
close second
close first
caught: failure

Вывод показывает два правила: finally срабатывает до возврата значения, а ресурсы закрываются в порядке, обратном открытию, и раньше, чем выполняется catch. Типичная ошибка: писать return внутри finally — он подменяет результат и скрывает исключение.

Что такое generics и что такое стирание типов?

Generics — параметризованные типы: List<String> вместо «списка чего угодно». Компилятор проверяет типы и сам подставляет приведения, поэтому ошибка обнаруживается при компиляции, а не в виде ClassCastException во время работы.

Стирание типов (type erasure) означает, что информация о параметре существует только на этапе компиляции: в байт-коде List<String> и List<Integer> — один и тот же класс. Проверка new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() возвращает true. Отсюда ограничения: нельзя написать new T(), instanceof List<String>, создать массив T[] или использовать примитив как параметр типа — List<int> не скомпилируется.

Что означают ? extends T и ? super T?

Это ограниченные wildcard-ы. List<? extends Number> — список какого-то подтипа Number: из него можно читать элементы как Number, но нельзя добавлять. List<? super Integer> — список какого-то супертипа Integer: в него можно добавлять Integer, но читать получится только как Object.

Правило для запоминания — PECS: producer extends, consumer super. Источник данных объявляют через extends, приёмник — через super. Метод с сигнатурой <T> void copy(List<? extends T> source, List<? super T> target) копирует List<Integer> в List<Number>. Что хотят услышать: почему это нужно — List<Integer> не является подтипом List<Number>, иначе в список целых можно было бы положить Double.

Что такое лямбда-выражение и функциональный интерфейс?

Функциональный интерфейс — интерфейс с одним абстрактным методом, например Runnable, Comparator, Predicate. Лямбда-выражение — короткая запись реализации такого интерфейса: x -> x * 2 вместо анонимного класса.

Основные интерфейсы из java.util.function: Predicate<T> — проверка, возвращает boolean; Function<T, R> — преобразование; Consumer<T> — действие без результата; Supplier<T> — поставщик значения. Ссылка на метод String::length — ещё более короткая форма лямбды s -> s.length(). Что хотят услышать: лямбда может использовать локальные переменные только если они effectively final, то есть не изменяются после инициализации.

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

Промежуточные операции (filter, map, sorted, distinct, limit) возвращают новый стрим и ничего не выполняют сами. Терминальная операция (toList, collect, forEach, count, reduce, findFirst) запускает весь конвейер и возвращает результат. Это свойство называется ленивостью, оно описано в документации пакета java.util.stream.

import java.util.List;
import java.util.stream.Stream;

public class Streams {
    public static void main(String[] args) {
        List<String> names = List.of("Olha", "Ivan", "Andrii", "Maria");

        Stream<String> pipeline = names.stream()
                .filter(name -> {
                    System.out.println("filter " + name);
                    return name.length() > 4;
                })
                .map(String::toUpperCase);
        System.out.println("nothing has run yet");

        List<String> result = pipeline.toList(); // terminal operation starts the work
        System.out.println(result);
    }
}
nothing has run yet
filter Olha
filter Ivan
filter Andrii
filter Maria
[ANDRII, MARIA]

Ещё три факта, о которых спрашивают: стрим одноразовый — повторный вызов терминальной операции бросает IllegalStateException; стрим не изменяет исходную коллекцию; map превращает один элемент в один, а flatMap — один элемент в несколько и склеивает их в общий стрим (список заказов → список всех товаров).

J

Те же темы — с преподавателем и проверкой кода

Курс Java в Prog Academy идёт 5,5 месяца: AI Start, Java Start, Java ООП, Java Pro и SQL. В модуле Java Pro — Spring, JPA и Hibernate, разбор более 100 вопросов и задач для собеседований и дипломный проект с code review от преподавателя.

Программа курса Java →

Многопоточность, память и сборка мусора

От джуна здесь ждут основ: как запустить задачу в другом потоке, что такое гонка и как её избежать, где живут объекты и кто их удаляет. Глубже — модель памяти, тонкости java.util.concurrent, настройка сборщика — спрашивают на Middle. Первоисточники: раздел 17 спецификации и документация java.util.concurrent.

Как создать поток и зачем нужен ExecutorService?

Поток создают, передав задачу в конструктор: new Thread(() -> ...).start(); задача — это Runnable (без результата) или Callable (с результатом и checked-исключениями). ExecutorService — пул потоков: вы отдаёте ему задачи, а он переиспользует готовые потоки и возвращает Future с результатом.

В рабочем коде потоки вручную почти не создают: создание потока дорого, а их неограниченное количество исчерпывает память. Пул получают через Executors.newFixedThreadPool(n), задачу отправляют через submit(), а пул обязательно закрывают — shutdown() или, начиная с Java 19, блоком try-with-resources. Типичная ошибка: вызвать run() вместо start() — тогда код выполнится в текущем потоке, новый не запустится.

Что такое состояние гонки и как работает synchronized?

Состояние гонки (race condition) возникает, когда несколько потоков одновременно читают и изменяют общие данные и результат зависит от порядка выполнения. synchronized устраняет её взаимным исключением: в блок, защищённый одним и тем же монитором, одновременно может войти только один поток.

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    static int unsafe = 0;
    static int guarded = 0;
    static final Object lock = new Object();
    static final AtomicInteger atomic = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                unsafe++;                      // read, add, write: not atomic
                synchronized (lock) {
                    guarded++;
                }
                atomic.incrementAndGet();
            }
        };

        Thread first = new Thread(task);
        Thread second = new Thread(task);
        first.start();
        second.start();
        first.join();
        second.join();

        System.out.println("unsafe:  " + unsafe);
        System.out.println("guarded: " + guarded);
        System.out.println("atomic:  " + atomic.get());
    }
}
unsafe:  199984
guarded: 200000
atomic:  200000

Первое число при каждом запуске разное: unsafe++ — это три действия (прочитать, прибавить, записать), и потоки перезаписывают результаты друг друга. Синхронизированный метод экземпляра блокирует монитор this, статический — объект класса. Что хотят услышать: альтернативы — атомарные классы (AtomicInteger), ReentrantLock, потокобезопасные коллекции, а лучше всего — не иметь общего изменяемого состояния.

Что делает volatile и чем он отличается от synchronized?

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

Типичное применение — флаг остановки: один поток пишет running = false, другой читает его в цикле. Без volatile читающий поток вправе не увидеть изменение никогда. Короткая формула: volatile — про видимость, synchronized — про видимость и атомарность блока.

Что такое deadlock и как его избежать?

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

Способы избежать: всегда захватывать блокировки в одном и том же порядке; держать их как можно меньше времени и не вызывать под блокировкой чужой код; использовать tryLock() с таймаутом; заменять ручные блокировки готовыми средствами из java.util.concurrent. Плюс к ответу: знать, как найти deadlock, — снять дамп потоков через jstack или jcmd <pid> Thread.print, в нём взаимная блокировка отмечается явно.

Что такое виртуальные потоки?

Виртуальные потоки — лёгкие потоки, которыми управляет JVM, а не операционная система; они стали постоянной частью языка в Java 21 (JEP 444). Когда виртуальный поток блокируется на вводе-выводе или sleep, JVM снимает его с потока ОС и отдаёт тот другой задаче, поэтому таких потоков можно запускать очень много.

import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class VirtualThreads {
    public static void main(String[] args) {
        AtomicInteger done = new AtomicInteger();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(200)); // blocking call parks the virtual thread
                    return done.incrementAndGet();
                });
            }
        } // close() waits for all tasks

        System.out.println(done.get());
    }
}

Программа выводит 10000: десять тысяч задач, каждая из которых «спит» 200 мс, завершаются без пула на десять тысяч потоков ОС. Что хотят услышать: виртуальные потоки помогают задачам, которые в основном ждут (запросы к базе, HTTP-вызовы), и не ускоряют вычисления; их не объединяют в пулы — на каждую задачу создают новый. Это вопрос скорее уровня Middle, но на знание новых версий он показателен.

Чем стек отличается от кучи?

Стек есть у каждого потока: в нём лежат кадры вызовов методов с локальными переменными — значениями примитивов и ссылками на объекты. Куча общая для всех потоков: в ней живут объекты и массивы, и ею управляет сборщик мусора. Области памяти JVM описаны в спецификации виртуальной машины.

Кадр стека исчезает при выходе из метода, а объект в куче живёт, пока на него есть ссылки. Слишком глубокая рекурсия даёт StackOverflowError, нехватка места в куче — OutOfMemoryError. Метаданные классов хранятся отдельно, в Metaspace (с Java 8). Что хотят услышать: в строке User user = new User() ссылка user лежит в стеке, а сам объект — в куче.

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

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

Куча делится на поколения: новые объекты создаются в молодом поколении, которое очищается часто и быстро; пережившие несколько сборок переходят в старое. Основание — наблюдение, что большинство объектов живёт недолго. В современных версиях HotSpot по умолчанию обычно работает сборщик G1, есть и другие — Parallel, Serial, ZGC; подробности — в руководстве по настройке сборки мусора. Что хотят услышать: System.gc() — только просьба, а утечки памяти в Java возможны: например, статическая коллекция, в которую добавляют объекты и никогда не удаляют.

SQL, JDBC и JPA

Серверный разработчик работает с базой данных каждый день, поэтому SQL на собеседовании спрашивают часто — обычно просят написать запрос с JOIN и группировкой. Освежить основы помогут SQL для начинающих и бесплатный курс по SQL.

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

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

-- employees: Olha (Backend), Ivan (Backend), Maria (QA), Petro (no department)
SELECT e.name, d.name AS department
FROM employees e
INNER JOIN departments d ON d.id = e.department_id;  -- 3 rows: Petro is missing

SELECT e.name, d.name AS department
FROM employees e
LEFT JOIN departments d ON d.id = e.department_id;   -- 4 rows: Petro with NULL

Типичная ошибка: после LEFT JOIN поставить условие на правую таблицу в WHERE (WHERE d.name = 'QA') — строки с NULL отфильтруются, и соединение превратится в INNER. Такое условие переносят в ON.

Чем PreparedStatement лучше Statement?

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

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JdbcExample {
    static String findEmail(String url, String name) throws SQLException {
        String sql = "SELECT email FROM users WHERE name = ?";
        try (Connection connection = DriverManager.getConnection(url);
             PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setString(1, name); // the value is sent separately from the SQL text
            try (ResultSet rs = statement.executeQuery()) {
                return rs.next() ? rs.getString("email") : null;
            }
        }
    }
}

Statement со склейкой строк ("... WHERE name = '" + name + "'") уязвим: значение ' OR '1'='1 меняет смысл запроса. Описание интерфейса — в документации PreparedStatement. Что хотят услышать: соединение, выражение и ResultSet — ресурсы, их закрывают через try-with-resources; в реальном приложении соединения берут из пула.

Что такое JPA и Hibernate и в каких состояниях бывает сущность?

JPA (Jakarta Persistence) — спецификация, которая описывает, как отображать Java-объекты на таблицы базы данных; Hibernate — её самая распространённая реализация. Вы работаете с объектами-сущностями, а SQL генерирует ORM.

У сущности четыре состояния: new (transient) — объект создан, но о нём не знает EntityManager; managed — объект привязан к контексту персистентности, и его изменения сами попадут в базу при фиксации транзакции (dirty checking); detached — контекст закрыт, изменения не отслеживаются; removed — помечен на удаление. Определения — в спецификации Jakarta Persistence.

Вопрос уровня Middle — проблема N+1: загрузили список из N заказов одним запросом, а при обращении к ленивой связи order.getItems() ORM делает ещё по запросу на каждый заказ. Лечится запросом с JOIN FETCH, entity graph или пакетной загрузкой. Полезно помнить умолчания: связи @OneToMany и @ManyToMany — ленивые, @ManyToOne и @OneToOne — жадные.

Что такое транзакция и что означает ACID?

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

Классический пример — перевод денег: списание с одного счёта и зачисление на другой должны либо произойти вместе, либо не произойти. В JDBC транзакцией управляют через connection.setAutoCommit(false), commit() и rollback(). Вопрос на продолжение: уровни изоляции — READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE — и какие аномалии каждый допускает: грязное чтение, неповторяющееся чтение, фантомы.

Spring и Spring Boot

По Spring джуна спрашивают основы: как контейнер создаёт объекты и связывает их, как написать контроллер и что делает @Transactional. Хорошо, если на каждый ответ у вас есть пример из собственного проекта. Первоисточник — справочная документация Spring Framework. Примеры кода в этом блоке минимальные и локально не запускались.

Что такое IoC и Dependency Injection?

Inversion of Control (инверсия управления) — принцип, по которому объекты создаёт и связывает не ваш код, а контейнер фреймворка. Dependency Injection (внедрение зависимостей) — способ это сделать: класс не создаёт свои зависимости через new, а получает их готовыми извне.

В Spring зависимости внедряют тремя способами: через конструктор, через сеттер и прямо в поле. Рекомендуется конструктор: зависимости можно объявить final, объект нельзя создать в «полусобранном» виде, а в тесте достаточно передать заглушку в new. Если конструктор у класса один, аннотация @Autowired над ним не нужна. Что хотят услышать: зачем это вообще — слабая связанность и тестируемость, а не «так принято в Spring».

Что такое бин и какие у него области видимости?

Бин — объект, который создаёт и которым управляет контейнер Spring. Бином класс становится, если помечен @Component или одной из её специализаций — @Service, @Repository, @Controller, @RestController, — либо если его возвращает метод с @Bean в классе @Configuration.

Область видимости по умолчанию — singleton: один экземпляр на контейнер. Другие области: prototype — новый объект при каждом запросе бина; request, session, application и websocket — для веб-приложений. Что хотят услышать: singleton-бин делят все потоки, обрабатывающие запросы, поэтому хранить в его полях изменяемое состояние (например, данные текущего пользователя) нельзя. Между стереотипами разница в основном смысловая; @Repository дополнительно включает преобразование исключений доступа к данным в иерархию Spring.

Как написать REST-контроллер в Spring?

Класс помечают @RestController, методы — аннотациями @GetMapping, @PostMapping, @PutMapping, @DeleteMapping. Возвращаемый объект Spring сам превращает в JSON, а параметры запроса передаёт в аргументы метода.

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;

    public BookController(BookService bookService) { // constructor injection
        this.bookService = bookService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> findById(@PathVariable Long id) {
        return bookService.findById(id)              // Optional<Book>
                .map(ResponseEntity::ok)             // 200 + JSON body
                .orElse(ResponseEntity.notFound().build()); // 404
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Book create(@RequestBody Book book) {
        return bookService.save(book);
    }
}

@RestController — это @Controller плюс @ResponseBody. @PathVariable берёт значение из пути, @RequestParam — из строки запроса, @RequestBody — из тела. Подробности — в разделе об аннотированных контроллерах, а о самом подходе — в глоссарии: REST API. Что хотят услышать: контроллер тонкий — принимает запрос и отдаёт ответ, бизнес-логика живёт в сервисе; коды ответа осмысленные: 200, 201, 400, 404.

Как работает @Transactional?

Spring оборачивает бин в прокси: при вызове метода с @Transactional прокси открывает транзакцию, вызывает ваш метод и после него фиксирует изменения. Если метод выбросил исключение, транзакция откатывается.

@Service
public class TransferService {

    private final AccountRepository accounts;

    public TransferService(AccountRepository accounts) {
        this.accounts = accounts;
    }

    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accounts.findById(fromId).orElseThrow();
        Account to = accounts.findById(toId).orElseThrow();
        from.withdraw(amount); // throws a RuntimeException if there is not enough money
        to.deposit(amount);
    } // normal return -> commit, RuntimeException -> rollback of both changes
}

Три детали, которые отличают сильный ответ, все из документации:

  • по умолчанию откат происходит при RuntimeException и Error, а при checked-исключении транзакция фиксируется; это меняют параметром rollbackFor;
  • вызов транзакционного метода из другого метода того же класса (this.transfer(...)) идёт мимо прокси, и транзакция не открывается;
  • по той же причине аннотация не работает на private-методах.

Вопрос на продолжение: режимы распространения — REQUIRED (по умолчанию: присоединиться к текущей транзакции или открыть новую) и REQUIRES_NEW (всегда новая).

Чем Spring Boot отличается от Spring Framework?

Spring Framework — сам фреймворк: контейнер, веб-слой, работа с данными. Spring Boot — надстройка, которая убирает ручную настройку: автоконфигурация по тому, какие библиотеки подключены, стартеры с согласованными версиями зависимостей и встроенный веб-сервер, так что приложение запускается как обычный jar.

@SpringBootApplication объединяет три аннотации: @SpringBootConfiguration, @EnableAutoConfiguration и @ComponentScan. Настройки лежат в application.properties или application.yml. Что хотят услышать: Boot не заменяет Spring и не содержит «магии» — он подключает те же бины, которые раньше описывали вручную, и любую автоконфигурацию можно переопределить своим бином.

Практические задачи

На практической части обычно дают короткую задачу на строки или коллекции и SQL-запрос — проверяют не знание алгоритмов, а то, как вы рассуждаете и выбираете структуру данных. Ниже четыре типовые задачи с решениями; все запущены, вывод указан в комментариях или после кода. Больше задач для тренировки — в бесплатном курсе по Java.

Задача 1. Посчитать, сколько раз каждое слово встречается в тексте

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

import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;

public class WordCount {
    static Map<String, Long> count(String text) {
        return Arrays.stream(text.toLowerCase().split("\\W+"))
                .filter(word -> !word.isEmpty())
                .collect(Collectors.groupingBy(Function.identity(), TreeMap::new, Collectors.counting()));
    }

    public static void main(String[] args) {
        System.out.println(count("To be, or not to be: that is the question"));
    }
}
{be=2, is=1, not=1, or=1, question=1, that=1, the=1, to=2}

Без стримов то же делается циклом и counts.merge(word, 1, Integer::sum). TreeMap взят, чтобы результат был отсортирован по словам.

Задача 2. Найти первый неповторяющийся символ строки

Ключевая мысль — нужна структура, которая и считает, и помнит порядок появления: LinkedHashMap. Решение за два прохода, сложность O(n).

import java.util.LinkedHashMap;
import java.util.Map;

public class FirstUnique {
    static Character firstUnique(String text) {
        Map<Character, Integer> counts = new LinkedHashMap<>(); // keeps insertion order
        for (char c : text.toCharArray()) {
            counts.merge(c, 1, Integer::sum);
        }
        for (Map.Entry<Character, Integer> entry : counts.entrySet()) {
            if (entry.getValue() == 1) return entry.getKey();
        }
        return null;
    }

    public static void main(String[] args) {
        System.out.println(firstUnique("swiss"));  // w
        System.out.println(firstUnique("aabb"));   // null
    }
}

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

Классическая задача на стек: открывающую скобку кладём в стек, для закрывающей проверяем, что на вершине лежит парная. В Java стеком служит ArrayDeque, а не устаревший класс Stack.

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;

public class Brackets {
    private static final Map<Character, Character> PAIRS = Map.of(')', '(', ']', '[', '}', '{');

    static boolean isBalanced(String text) {
        Deque<Character> stack = new ArrayDeque<>();
        for (char c : text.toCharArray()) {
            if (PAIRS.containsValue(c)) {
                stack.push(c);
            } else if (PAIRS.containsKey(c)) {
                if (stack.isEmpty() || stack.pop() != PAIRS.get(c)) return false;
            }
        }
        return stack.isEmpty();
    }

    public static void main(String[] args) {
        System.out.println(isBalanced("{[()]}")); // true
        System.out.println(isBalanced("([)]"));   // false
        System.out.println(isBalanced("(("));     // false
    }
}

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

Задача 4. SQL: сотрудники по отделам и вторая по величине зарплата

Таблицы — departments(id, name) и employees(id, name, salary, department_id); данные те же, что в вопросе про JOIN, зарплаты — 3000, 2200, 1800 и 1500.

-- 1. Number of employees and the top salary per department
SELECT d.name AS department, COUNT(e.id) AS employees, MAX(e.salary) AS max_salary
FROM departments d
LEFT JOIN employees e ON e.department_id = d.id
GROUP BY d.id, d.name
HAVING COUNT(e.id) >= 1
ORDER BY max_salary DESC;

-- 2. The second highest salary in the company
SELECT MAX(salary) AS second_salary
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
department  employees  max_salary
----------  ---------  ----------
Backend     2          3000
QA          1          1800

second_salary
-------------
2200

В первом запросе считается COUNT(e.id), а не COUNT(*): иначе отдел без сотрудников получил бы единицу. HAVING фильтрует уже сгруппированные строки, WHERE — исходные. Второй запрос можно решить и через ORDER BY salary DESC LIMIT 1 OFFSET 1, но тогда нужно отдельно подумать об одинаковых зарплатах.

Типичные ошибки кандидатов

По моим наблюдениям, на техническом интервью кандидатов чаще подводит не незнание редкой темы, а то, как они отвечают. Это опыт автора как преподавателя, а не статистика:

  • Заученные определения без примера. «Полиморфизм — это способность объекта принимать множество форм» ничего не говорит о понимании. К каждому определению держите пример кода.
  • Молчание во время решения задачи. Интервьюер оценивает ход мысли; тишина выглядит как «не знаю». Проговаривайте, что делаете и почему.
  • Попытка угадать вместо «не знаю». Уверенно сказанная неправда хуже честного «с этим не работал, но предполагаю, что…».
  • Сравнение строк и обёрток через ==. Ошибка в коде на live coding, которая сразу выдаёт пробел в основах.
  • Незнание собственного проекта. Если в резюме указан Spring Boot, будьте готовы объяснить любую аннотацию и любой класс в репозитории.
  • Технологии в резюме «для количества». По каждой строке резюме могут задать вопрос. Не уверены — уберите.
  • Отсутствие вопросов к компании. Выглядит как отсутствие интереса к работе.
  • Игнорирование условия тестового задания. Сделано больше, чем просили, но пропущено обязательное требование — обидная причина отказа.

Что спросить у интервьюера?

Вопросы кандидата — часть собеседования: по ним видно, думаете ли вы о реальной работе, и это ваш способ понять, подходит ли вам команда. Достаточно трёх-четырёх из списка:

  • Какая версия Java и какой стек на проекте: Spring Boot, база данных, система сборки, CI?
  • Какими будут мои задачи в первые один-два месяца?
  • Как устроена адаптация: будет ли ментор и кто проверяет код новичка?
  • Как проходит код-ревью и есть ли в команде принятые стандарты кода?
  • Как на проекте с тестами: кто их пишет и что считается достаточным покрытием?
  • Какой размер команды и кто в неё входит?
  • По каким критериям оценивают результат после испытательного срока?
  • Как в команде относятся к использованию AI-ассистентов в работе?
  • Какие следующие этапы и когда ждать обратную связь?

План подготовки на две недели

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

ДеньТемаЧто сделать
1–2Java Core и ООПОтветить вслух на вопросы блока, написать по примеру на передачу параметров, перегрузку и переопределение
3String, equals/hashCode, неизменяемостьНаписать класс с equals() и hashCode() вручную, затем заменить его на record
4–5КоллекцииОбъяснить устройство HashMap на листе бумаги; решить три задачи на Map и Set
6Исключения и genericsНаписать свой класс исключения, пример с try-with-resources и обобщённый метод с wildcard-ами
7Лямбды и Stream APIПереписать пять циклов из своего проекта на стримы и обратно
8–9Многопоточность, память, GCВоспроизвести гонку на счётчике и исправить её тремя способами; нарисовать стек и кучу для небольшой программы
10SQL, JDBC, JPAНаписать десять запросов с JOIN и GROUP BY к своей базе; найти в проекте место, где возможна проблема N+1
11–12Spring и собственный проектПройти по проекту и объяснить каждую аннотацию; подготовить рассказ о проекте на три минуты
13Практические задачиРешить задачи из этой статьи без подсказок, с таймером и вслух
14Пробное собеседованиеПопросить знакомого разработчика или одногруппника провести интервью; подготовить вопросы к компании

Если базы пока нет, начинать стоит не с вопросов, а с языка: зачем его учить и что он даёт, разобрано в статье почему стоит учить Java. Историю своего проекта удобно вести в Git с первого дня — поможет Git для начинающих.

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

Сколько времени нужно на подготовку к собеседованию Java Junior?

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

Будет ли live coding на собеседовании Java-разработчика?

Скорее всего да, в той или иной форме: небольшая задача на строки или коллекции, разбор чужого кода с вопросом «что выведет программа» или SQL-запрос. Формат зависит от компании, поэтому уточните его у рекрутера заранее. Готовиться стоит в любом случае: решайте задачи вслух и без автодополнения.

Нужно ли знать Spring для позиции Junior Java Developer?

Для большинства вакансий в серверной разработке — да, на базовом уровне: что такое внедрение зависимостей, бин, REST-контроллер и @Transactional и как устроен ваш собственный проект на Spring Boot. Глубоких знаний внутреннего устройства фреймворка от джуна обычно не ждут, но Java Core без Spring для первой работы чаще всего недостаточно.

Какую версию Java учить для собеседования?

Любую актуальную LTS-версию: 17, 21 или 25. Основа языка у них общая, а на собеседовании полезно знать, что появилось после Java 8: var, records, текстовые блоки, switch-выражения, sealed-классы, виртуальные потоки. Про лямбды и Stream API из Java 8 спрашивают по-прежнему, потому что на них написан почти любой рабочий код.

Нужен ли английский на собеседовании Java-разработчика?

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

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

На самом собеседовании — только если интервьюер прямо разрешил: скрытое использование подсказок обычно расценивают как обман. В тестовом задании правила задаёт компания, поэтому спросите заранее. В любом случае вы должны уметь объяснить каждую строку решения: код, который автор не понимает, на защите вредит, а не помогает.

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

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

Дают ли тестовое задание на позицию Java Junior и как его выполнять?

Дают не везде, это зависит от компании. Если дали — выполните ровно то, что написано в условии, добавьте README с инструкцией запуска, несколько тестов и осмысленную историю коммитов. Не усложняйте: простое работающее решение, которое вы можете объяснить, лучше недоделанной архитектуры на вырост.

Нужно ли знать алгоритмы и структуры данных для Junior Java?

На базовом уровне — да: понимать сложность операций в ArrayList, LinkedList, HashMap и TreeMap, уметь написать цикл с Map или Set, развернуть строку, найти дубликаты. Сложные задачи на графы и динамическое программирование на позицию Junior встречаются реже и зависят от компании.

Источники

Подготовиться к собеседованию на курсе Java →

Фото автора: Александр Цимбалюк

Об авторе

Тренер Prog Academy по Java и Python

Преподаёт программирование более 12 лет, автор офлайн- и онлайн-курсов Prog Academy по Java и Python. В блоге пишет о Java, Python и анализе данных на Python.

Контакт

Записаться на консультацию

Укажите актуальный номер, мы позвоним в любую страну :)