Питання на співбесіді Java-розробника: 40 питань з відповідями для Junior
Коротка відповідь: на співбесіді Junior Java Developer перевіряють три речі — чи розумієте ви Java Core (ООП, рядки, колекції, винятки), чи можете пояснити, що робить ваш код, і чи знаєте базовий робочий стек: SQL, Spring Boot, Git. У статті 40 питань із короткими відповідями, згрупованих за сімома темами, — у тому вигляді, в якому їх ставить інтерв'юер, з кодом, який можна запустити.
Я викладаю програмування понад 12 років, і розбір питань та задач зі співбесід входить до програми курсу Java. З мого досвіду, кандидати частіше спотикаються не на рідкісних темах, а на базових: людина щодня користується HashMap, але не може пояснити, навіщо перевизначати hashCode(). Тому нижче — не «всі питання з Java», а ті, на яких перевіряється розуміння. До кожної відповіді я додав, що саме хоче почути інтерв'юер.
Відповіді правильні для актуальних 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 і final | Junior |
| String, equals/hashCode | Пул рядків, порівняння об'єктів, контракт equals() і hashCode(), незмінні класи та records | Junior |
| Колекції | Вибір між ArrayList, LinkedList, HashMap, TreeMap, HashSet; складність операцій | Junior |
| Будова HashMap | Бакети, колізії, розширення таблиці, змінювані ключі | Junior / Middle |
| Винятки та generics | Checked і unchecked, try-with-resources, стирання типів, wildcard-и | Junior; wildcard-и — Middle |
| Лямбди та Stream API | Функціональні інтерфейси, проміжні й термінальні операції, лінивість | Junior |
| Багатопотоковість | Створення потоків, synchronized, volatile, deadlock, ExecutorService, віртуальні потоки | Junior — основи; Middle — глибше |
| JVM і пам'ять | Стек і купа, збирання сміття, витоки пам'яті | Junior — основи; Middle — налаштування та діагностика |
| SQL, JDBC, JPA | JOIN, групування, транзакції, PreparedStatement, стани сутностей, проблема N+1 | Junior; N+1 — Middle |
| Spring і Spring Boot | IoC і DI, біни та області видимості, REST-контролер, @Transactional | Junior — основи; 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), бо до елемента треба дійти за посиланнями.
| Операція | ArrayList | LinkedList |
|---|---|---|
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 — один елемент на кілька й склеює їх у спільний стрим (список замовлень → список усіх товарів).
Ті самі теми — з викладачем і перевіркою коду
Курс 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–2 | Java Core і ООП | Відповісти вголос на питання блоку, написати по прикладу на передавання параметрів, перевантаження та перевизначення |
| 3 | String, equals/hashCode, незмінність | Написати клас з equals() і hashCode() вручну, потім замінити його на record |
| 4–5 | Колекції | Пояснити будову HashMap на аркуші паперу; розв'язати три задачі на Map і Set |
| 6 | Винятки та generics | Написати власний клас винятку, приклад із try-with-resources та узагальнений метод із wildcard-ами |
| 7 | Лямбди та Stream API | Переписати п'ять циклів зі свого проєкту на стрими й назад |
| 8–9 | Багатопотоковість, пам'ять, GC | Відтворити гонку на лічильнику й виправити її трьома способами; намалювати стек і купу для невеликої програми |
| 10 | SQL, JDBC, JPA | Написати десять запитів із JOIN і GROUP BY до своєї бази; знайти в проєкті місце, де можлива проблема N+1 |
| 11–12 | Spring і власний проєкт | Пройтися проєктом і пояснити кожну анотацію; підготувати розповідь про проєкт на три хвилини |
| 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 трапляються рідше й залежать від компанії.
Джерела
- The Java Language Specification, Java SE 25 Edition — класи, перетворення типів (5.1.7), винятки (11), потоки та блокування (17)
- The Java Virtual Machine Specification: Run-Time Data Areas
- Java SE 25 API: Object, String, Record
- Java SE 25 API: HashMap, ArrayList, ConcurrentModificationException
- Java SE 25 API: package java.util.stream
- Java SE 25 API: package java.util.concurrent
- Java SE 25 API: PreparedStatement
- OpenJDK: JEP 444 — Virtual Threads
- Oracle: HotSpot Virtual Machine Garbage Collection Tuning Guide
- Oracle Java SE Support Roadmap — які версії є LTS
- dev.java: Learn Java — навчальні розділи про винятки та generics
- Jakarta Persistence Specification
- Spring Framework Reference: The IoC Container, Bean Scopes
- Spring Framework Reference: Annotated Controllers
- Spring Framework Reference: Using @Transactional
- Spring Boot Reference Documentation
