Готуєтеся до співбесіди на позицію Java-розробника?
Знайдіть мою книгу Guide To Clear Java Developer Interview тут Gumroad(PDF формат) та Amazon (Kindle eBook).
Guide To Clear Spring-Boot Microservice Interview тут Gumroadd (PDF формат) та Amazon(Kindle eBook).
Guide To Clear Front-End Developer Interview тут Gumroad (PDF формат) та Amazon (Kindle eBook).
Завантажте зразок копії тут:
Guide To Clear Java Developer Interview[Free Sample Copy]
Guide To Clear Spring-Boot Microservice Interview[Free Sample Copy]
Guide To Clear Front-End Developer Interview [SampleCopy]_
Якщо вам потрібне персональне консультування, ось 1:1 лінк — https://topmate.io/ajayrathod11_
На цьому технічному етапі співбесіди можна помітити чітку закономірність у тому, як ставилися питання:
- Основи Java: Спочатку почали з основ Java Stream API.
- Spring Framework: Далі переключилися на основні концепції Spring.
- Кодингові завдання: Потім перевірили здатність кандидата до програмування, ймовірно, включаючи SQL-запити.
- Frontend: Якщо це було зазначено у вашому резюме, то ставили питання по фреймворках, таких як Angular або React.
- Spring та мікросервіси: Нарешті, вони детально розглянули основні концепції Spring та мікросервісів.
Для успішного проходження таких співбесід важливо освоїти:
- Основи Java, зокрема її нові можливості
- Spring Framework
- Spring Boot та Мікросервіси
Ці області є критично важливими для успіху. Тепер давайте розглянемо розділ питань і відповідей, щоб докладніше обговорити ці питання.
Java 8 Stream API
Поясніть, що таке Java 8 Streams і чим вони відрізняються від традиційних колекцій.
Відповідь: Java 8 Streams — це нове абстрагування для обробки послідовностей елементів у функціональному стилі. На відміну від колекцій, потоки не зберігають елементи; вони більше нагадують конвеєр операцій над джерелом даних. Вони підтримують операції, такі як map, filter і reduce, які можна з’єднувати для обробки даних у "лінивий" (lazy) та декларативний спосіб.
Які типи операцій можна виконувати над потоком?
Відповідь: Існує два типи операцій над потоками:
- Проміжні операції: Ці операції повертають потік і можуть бути з’єднані, наприклад, filter(), map(), sorted().
- Термінальні операції: Вони призводять до отримання результату або побічного ефекту, завершуючи обробку потоку, такі як collect(), forEach(), reduce().
Як ви використаєте Stream API для фільтрації всіх парних чисел з списку цілих чисел?
Відповідь:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());
Можете пояснити різницю між map та flatMap в Stream API?
Відповідь: map перетворює кожен елемент на інший об’єкт, зберігаючи відношення один до одного.
flatMap використовується, коли один елемент може призвести до кількох елементів або коли працюєте з колекціями в потоку, ефективно "сплющуючи" структуру в один потік.
- map: Stream.of(“a”, “bb”, “ccc”).map(s -> s.length()) дає результат [1, 2, 3].
- flatMap: Stream.of(“a”, “bb”, “ccc”).flatMap(s -> s.chars().boxed()) дає потік окремих кодів символів.
Як відсортувати потік об'єктів за конкретною ознакою?
Відповідь:
List people = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25)); List sortedByAge = people.stream().sorted(Comparator.comparing(Person::getAge)).collect(Collectors.toList());
Що робить метод collect в Stream API?
Відповідь: collect — це термінальна операція, яка збирає елементи потоку в результатуючий контейнер, такий як List, Set або Map, використовуючи колектори. Наприклад:
List result = stream.collect(Collectors.toList());
Поясніть метод reduce на прикладі.
Відповідь: reduce виконує зменшення елементів потоку, використовуючи бінарну операцію для їх поєднання. Приклад:
Optional sum = Stream.of(1, 2, 3, 4).reduce(Integer::sum);
Це підсумовує всі числа в потоці, повертаючи Optional, оскільки потік може бути порожнім.
Яка мета Optional в Java 8 і як його використовують з потоком?
Відповідь: Optional використовується для представлення значення, яке може бути відсутнім, зменшуючи потребу в перевірках на null. У поєднанні з потоком він часто повертається методами, такими як findFirst(), reduce() або min(), щоб вказати, що результат може бути відсутнім:
Optional max = Stream.of(1, 2, 3).max(Integer::compareTo);
Функціональні інтерфейси за замовчуванням:
Що таке функціональний інтерфейс в Java 8?
Відповідь: Функціональний інтерфейс — це інтерфейс, який має тільки один абстрактний метод. Це дозволяє йому бути реалізованим за допомогою виразів лямбда або посилань на методи.
Назвіть і поясніть використання деяких основних функціональних інтерфейсів в Java 8.
Відповідь:
- Predicate: Використовується для операцій фільтрації, повертає boolean.
- Consumer: Приймає один аргумент і виконує дію, не повертаючи результату, використовується в forEach.
- Function: Перетворює вхідний тип T в вихідний тип R, використовується в map.
- Supplier: Подає результат типу T без потреби вхідного аргумента, використовується для лінійної оцінки.
- UnaryOperator та BinaryOperator: Спеціалізації Function для операцій, де вхідні та вихідні типи однакові.
Як ви б використали Predicate для фільтрації потоку?
List nonEmptyStrings = Arrays.asList("", "a", "b", "").stream() .filter(Predicate.not(String::isEmpty)) .collect(Collectors.toList());
Поясніть, як використовувати Consumer з потоком.
List myList = Arrays.asList("A", "B", "C"); myList.stream().forEach(System.out::println); // System.out::println — це Consumer
В чому різниця між Function та BiFunction?
Відповідь: Function приймає один аргумент і повертає результат, тоді як BiFunction приймає два аргументи для створення результату.
Приклад:
- Function може конвертувати Integer в його рядкове представлення.
- BiFunction може конкатенувати два цілих числа в рядок.
Як можна використати Supplier в операціях з потоком?
Відповідь:
Stream.generate(() -> new Random().nextInt(100)).limit(10).forEach(System.out::println);
Це генерує потік з 10 випадкових цілих чисел.
Наведіть приклад використання UnaryOperator з потоком.
Відповідь:
List strings = Arrays.asList("hello", "world"); List upperCase = strings.stream().map(String::toUpperCase).collect(Collectors.toList());
Тут, String::toUpperCase — це UnaryOperator.
Spring core
Питання 1: Яка мета анотації @Qualifier в Spring і як вона використовується разом з @Autowired?
Відповідь:
Анотація @Qualifier використовується в Spring для розв'язування неоднозначності, коли в контексті програми є кілька бінів одного типу. Коли ви використовуєте @Autowired для інжекції залежностей і в контексті існує кілька бінів одного типу, Spring не може визначити, який бін інжектувати. Тут на допомогу приходить @Qualifier:
- Використання: @Qualifier використовують разом з @Autowired, щоб вказати, який бін слід інжектувати, коли є кілька кандидатів. Ви анотуюєте точку інжекції за допомогою @Qualifier та вказуєте ім'я чи кваліфікатор біну, який хочете інжектувати.
// В конфігураційному чи компонентному класі
@Bean("specialDataSource")
public DataSource dataSource() {
return new DataSource();
}
// У класі, де хочете виконати інжекцію
@Autowired
@Qualifier("specialDataSource")
private DataSource dataSource;
Вказавши “specialDataSource” через @Qualifier, ви кажете Spring інжектувати саме бін з ім'ям “specialDataSource”, а не будь-який інший бін DataSource, що може існувати.
Поясніть, як працює анотація @Transactional в Spring. Які основні атрибути потрібно враховувати?
Відповідь:
Анотація @Transactional в Spring дозволяє здійснювати декларативне управління транзакціями, що означає, що ви можете працювати з транзакціями без явного кодування їх у вашій бізнес-логіці. Вона застосовується до класів або методів для визначення області транзакції:
- Як це працює: Коли ви анотуєте метод або клас анотацією @Transactional, Spring обгортає виклик методу в транзакцію. Якщо в методі виникає виняток, транзакція буде скасована, в іншому випадку вона буде підтверджена в кінці виконання методу.
Основні атрибути:
- propagation: Визначає, як має поводитись транзакція, коли один контекст транзакції викликає інший. Розповсюджені значення: REQUIRED (за умовчанням), REQUIRES_NEW (нова транзакція), NESTED (вкладена транзакція всередині поточної).
- isolation: Визначає рівень ізоляції транзакції для запобігання проблемам, таким як брудні читання, неповторювані читання і фантомні читання. Опції включають READCOMMITTED, READUNCOMMITTED, REPEATABLE_READ, SERIALIZABLE.
- rollbackFor: Визначає типи винятків, які мають призводити до скасування транзакції. За умовчанням тільки виключення типу Runtime викликають скасування; перевірені виключення не призводять до цього.
- readOnly: Якщо встановлено в true, це вказує, що транзакція є лише для читання, що може оптимізувати продуктивність для деяких транзакційних ресурсів.
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class, readOnly = false)
public void saveData(MyData data) {
// Бізнес-логіка тут
}
У цьому прикладі ми забезпечуємо транзакцію з конкретними правилами щодо того, як вона має поширюватися, її рівня ізоляції, а також що вона буде скасована для будь-якого винятку, а не тільки для винятків типу runtime.
Що відбудеться, якщо в методі ланцюга викликів буде кілька анотацій @Transactional?
Відповідь:
Коли в ланцюзі викликів методів присутні кілька анотацій @Transactional:
- Propagation: Поведінка залежатиме від атрибута propagation кожної анотації @Transactional.
Якщо метод, позначений анотацією @Transactional(propagation = Propagation.REQUIRES_NEW), викликається зсередини іншого транзакційного методу, для цього методу буде створена нова фізична транзакція, незалежна від зовнішньої транзакції. - Вкладеність: Якщо методи позначені @Transactional, але без REQUIRES_NEW, вони зазвичай приєднуються до існуючої транзакції, якщо не вказано інше. Однак при використанні NESTED, Spring використовує контрольні точки (savepoints) в межах однієї фізичної транзакції, що дозволяє здійснювати часткові відкатки.
- Результат: Якщо виникає виключення, поведінка транзакції (підтвердження або відкат) залежить від конфігурації найзовнішнішої транзакції, якщо внутрішні транзакції не налаштовані з REQUIRES_NEW або NESTED із контрольними точками.
Це налаштування може призвести до складних меж транзакцій, і розуміння того, як транзакції поширюються та взаємодіють, є важливим для управління консистентністю даних у додатках.
Запитання з Frontend (React):
Різниця між середовищами розробки та продакшн:
Запитання: Які основні відмінності між середовищами розробки та продакшн в React додатках?
Відповідь:
- Продуктивність: Продукційні збірки оптимізовані для продуктивності; активи мінімізовані, а код зазвичай збирається та стискається.
- Обробка помилок: У продакшн-середовищі важливими є межі помилок (error boundaries) для плавного відновлення після помилок; в середовищі розробки помилки більш детальні для налагодження.
- Змінні середовища: Використання process.env.NODE_ENV для умовної логіки, наприклад, для відключення PropTypes у продакшн.
- Source Maps: У середовищі розробки є source maps для зручності налагодження; у продакшн середовищі це може бути відсутнє через міркування безпеки.
Оператори строгого рівності (== vs ===):
Запитання: Поясніть різницю між == та === в JavaScript і коли їх використовувати в контексті React?
Відповідь:
- == (Легка рівність): Виконується приведення типів, якщо типи різні, порівнюються лише значення.
- === (Строга рівність): Порівнюються як значення, так і типи без приведення типів.
У React завжди слід використовувати === для порівняння стану або умовного рендерингу, щоб уникнути непередбачуваної поведінки через приведення типів. Наприклад, при перевірці рівності властивостей або стану.
Рендеринг у React:
Запитання: У чому різниця між методом render() та ReactDOM.render() в React?
Відповідь:
- render(): Метод всередині класового компонента, який повертає те, що має бути відображено в DOM. Це частина життєвого циклу компонента.
- ReactDOM.render(): Функція, яка використовується для рендерингу елемента React у DOM у вказаному контейнері.
Це використовується для ініціалізації процесу рендерингу вашого додатка або його частини.
Обробка різних props в React:
Запитання: Як передавати різні типи props в компонент React і як їх обробляти?
Відповідь:
- Прості Props: Як числа, рядки, булеві значення, передаються безпосередньо.
- Об'єктні Props: Об'єкти або масиви передаються за посиланням.
- Функціональні Props: Корисні для зворотних викликів або для передачі обробників подій.
Керувати ними можна, визначивши propTypes для перевірки типів у режимі розробки та деструктуруючи props у компоненті для чистішого коду.
const MyComponent = ({ name, age, onClick }) => { return
{name}, {age}
; };
Отримання даних з API в React:
Запитання: Як отримати дані з API в компоненті React, враховуючи найкращі практики для управління станом?
Відповідь:
Використовуйте useEffect для побічних ефектів, як-от виклики API:
import React, { useState, useEffect } from 'react';
function FetchData() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('your-api-endpoint')
.then(response => response.json())
.then(data => setData(data))
.catch(error => console.error('Error:', error));
}, []); // Порожній масив забезпечує, що ефект виконуватиметься тільки один раз при монтуванні
return (
{data ? JSON.stringify(data) : 'Loading...'}
);
}
- Обробляйте стани завантаження, помилки та використовуйте useCallback для мемоізації, якщо функцію fetch потрібно передавати як props для оптимізації повторних рендерів.
Запитання 1: Реалізація кастомного пулу потоків
Запитання: Напишіть базову реалізацію пулу потоків на Java. Ваш пул потоків має бути здатний керувати фіксованою кількістю потоків, ставити завдання в чергу та виконувати їх. Включіть методи для подачі завдань, зупинки пулу та обробки виконання завдань.
Відповідь:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class CustomThreadPool {
private final int nThreads;
private final PoolWorker[] threads;
private final BlockingQueue taskQueue;
public CustomThreadPool(int nThreads) {
this.nThreads = nThreads;
taskQueue = new LinkedBlockingQueue<>();
threads = new PoolWorker[nThreads];
for (int i = 0; i < nThreads; i++) {
threads[i] = new PoolWorker();
threads[i].start();
}
}
public void execute(Runnable task) throws InterruptedException {
taskQueue.put(task);
}
public void shutdown() throws InterruptedException {
for (PoolWorker worker : threads) {
worker.stopWorker();
}
for (PoolWorker worker : threads) {
worker.join();
}
}
private class PoolWorker extends Thread {
private volatile boolean running = true;
public void stopWorker() {
running = false;
}
@Override
public void run() {
while (running) {
try {
Runnable task = taskQueue.take();
task.run();
} catch (InterruptedException e) {
// Відновити статус переривання
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) throws InterruptedException {
CustomThreadPool pool = new CustomThreadPool(2);
for (int i = 0; i < 5; i++) {
int taskId = i;
pool.execute(() -> {
System.out.println("Task " + taskId + " by " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
}
}
Пояснення: Цей приклад демонструє простий пул потоків, де завдання керуються в черзі, і фіксована кількість потоків виконує ці завдання. Реалізація включає механізм зупинки для плавного завершення всіх потоків.
Запитання 2: Реалізація кешу за принципом Least Recently Used (LRU)
Запитання: Реалізуйте кеш LRU з часовою складністю O(1) для операцій get та put.
Кеш має фіксовану ємність, і коли вона перевищена, видаляється найменш нещодавно використовуваний елемент.
Відповідь:
import java.util.HashMap;
import java.util.Map;
class LRUCache {
private class Node {
int key, value;
Node prev, next;
Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Map cache;
private int capacity;
private Node head, tail;
public LRUCache(int capacity) {
this.capacity = capacity;
cache = new HashMap<>();
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (!cache.containsKey(key)) return -1;
Node node = cache.get(key);
removeNode(node);
addToHead(node);
return node.value;
}
public void put(int key, int value) {
if (cache.containsKey(key)) {
removeNode(cache.get(key));
}
if (cache.size() >= capacity) {
removeNode(tail.prev);
}
Node node = new Node(key, value);
addToHead(node);
cache.put(key, node);
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void addToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
public static void main(String[] args) {
LRUCache cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1)); // повертає 1
cache.put(3, 3); // видаляє ключ 2
System.out.println(cache.get(2)); // повертає -1 (не знайдено)
cache.put(4, 4); // видаляє ключ 1
System.out.println(cache.get(1)); // повертає -1 (не знайдено)
System.out.println(cache.get(3)); // повертає 3
System.out.println(cache.get(4)); // повертає 4
}
}
Пояснення: Ця реалізація використовує двозв'язний список для збереження порядку та HashMap для доступу до вузлів за O(1).
Коли елемент доступний або додається, він переміщується на початок (голову) списку, що гарантує, що найменш нещодавно використаний елемент завжди буде в кінці списку, готовий до видалення, якщо кеш заповнено.
Spring та Мікросервіси
Запитання 1: Поясніть роль Spring Boot в архітектурі мікросервісів
Запитання: Що таке Spring Boot і як він сприяє розробці мікросервісів?
Відповідь:
Spring Boot є розширенням фреймворку Spring, яке спрощує процес налаштування, конфігурації та запуску як автономних, так і готових до виробництва додатків на Spring з мінімальними налаштуваннями.
- Автоконфігурація: Spring Boot автоматично конфігурує бібліотеки Spring та сторонні бібліотеки, коли це можливо, зменшуючи кількість шаблонного коду для загальних випадків використання, таких як налаштування баз даних, веб-серверів тощо.
- Опініоновані налаштування за замовчуванням: Він надає "стартер" залежності, що забезпечують налаштування за замовчуванням для різних функціональностей, що полегшує початок роботи з узгодженим набором технологій для побудови мікросервісів.
- Вбудовані сервери: Spring Boot підтримує запуск веб-додатків із вбудованими серверами, такими як Tomcat, Jetty або Undertow, що є важливим для самостійного розгортання мікросервісів.
- Підтримка мікросервісів:
- Відкриття сервісів: Інтеграція з інструментами для реєстрації та відкриття сервісів, такими як Eureka або Consul.
- Розподілені конфігурації: Централізоване управління конфігурацією за допомогою Spring Cloud Config.
- Запобіжник (Circuit Breaker): Реалізація патернів стійкості, таких як запобіжник, з Hystrix або Resilience4j.
- API Gateway: Працює з інструментами, такими як Spring Cloud Gateway, для керування маршрутизацією та балансуванням навантаження.
- Функції для готовності до виробництва: Надає перевірки стану, метрики та зовнішні конфігурації, що є важливими для мікросервісів у виробничому середовищі.
Запитання 2: Обговоріть важливість API Gateway в архітектурі мікросервісів
Запитання: Чому API Gateways важливі в архітектурі мікросервісів, і як Spring Cloud Gateway вирішує ці задачі?
Відповідь:
API Gateways в архітектурі мікросервісів виконують кілька важливих функцій:
- Єдиний точка входу: Вони виступають єдиною точкою входу для всіх запитів від клієнтів, спрощуючи взаємодію з клієнтами та приховуючи складність ландшафту сервісів.
- Маршрутизація запитів: Маршрутизують запити до відповідних бекенд-сервісів на основі різних критеріїв, таких як шлях, заголовки тощо.
- Балансування навантаження: Розподіляють запити між екземплярами сервісів для управління навантаженням і забезпечення високої доступності.
- Безпека: Реалізують аутентифікацію, обмеження на кількість запитів і можуть бути кордоном безпеки для сервісів.
- Перехресні проблеми: Обробляють такі питання, як логування, моніторинг і збір метрик в одному місці, а не в кожному сервісі окремо.
Spring Cloud Gateway, частина Spring Cloud, покращує екосистему мікросервісів, забезпечуючи:
- Конфігурація маршрутів: Підтримує визначення маршрутів через зовнішню конфігурацію, що робить їх динамічними і керованими без змін у коді.
- Предикати та фільтри: Пропонує великий набір вбудованих предикатів для маршрутизації та фільтрів для модифікації запитів/відповідей, що дозволяє реалізувати складну логіку маршрутизації та трансформацію.
- Інтеграція з екосистемою Spring: Легко інтегрується з іншими компонентами Spring Cloud для відкриття сервісів, управління конфігурацією та стійкості.
- Реактивне програмування: Побудовано на Project Reactor, підтримує реактивні потоки, що робить його ефективним для обробки високої конкуренції та ситуацій з надмірним навантаженням, які є типовими для мікросервісів.
Запитання 3: Як працює відкриття сервісів у мікросервісах з Spring Cloud?
Запитання: Описати, як працює відкриття сервісів у мікросервісному налаштуванні за допомогою Spring Cloud, і які переваги воно надає?
Відповідь:
Відкриття сервісів у Spring Cloud включає:
- Реєстрація: Коли екземпляр сервісу запускається, він реєструється в реєстрі сервісів (наприклад, Eureka або Consul).
- Реєстрація: Це включає деталі, як-от мережеве місцезнаходження, стан здоров'я тощо.
- Пошук: Клієнти (або інші сервіси) можуть запитувати цей реєстр, щоб знайти доступні екземпляри сервісів, з якими їм потрібно взаємодіяти.
- Динамічні оновлення: Реєстр постійно оновлюється, коли сервіси додаються або видаляються, що гарантує, що клієнти завжди можуть знайти активні сервіси.
Переваги:
- Розв'язання залежностей: Сервіси не повинні знати місцезнаходження один одного; вони взаємодіють через логічні імена сервісів, що зменшує зв'язність.
- Масштабованість: Спрощує горизонтальне масштабування, дозволяючи додавати або видаляти нові екземпляри без змін конфігурації клієнтів.
- Устойчивість: Якщо екземпляр сервісу виходить з ладу, клієнти можуть автоматично знайти здоровий екземпляр, покращуючи стійкість до помилок.
- Балансування навантаження: Зазвичай інтегрується з балансуванням навантаження на стороні клієнта, дозволяючи запити розподілятися між кількома екземплярами сервісу.
Spring Cloud спрощує це за допомогою:
- Інтеграція з Eureka або Consul: Підтримка реєстрів сервісів з коробки.
- DiscoveryClient: Абстракція, яку сервіси використовують для взаємодії з реєстром, абстрагуючи підлягаючий механізм пошуку.
- Ribbon (для старіших версій) або LoadBalancerClient: Для балансування навантаження на стороні клієнта, що забезпечує розподіл запитів між екземплярами.
Ця конфігурація є основою для досягнення автономії, масштабованості та стійкості, які обіцяють мікросервіси.
Це все, хлопці,
Дякую за читання
- 👏 Будь ласка, аплодуйте за історію та підписуйтеся на мене 👉
- 📰 Читайте більше контенту на моєму Medium (21 історія про інтерв'ю для Java-розробників)
Знайдіть мої книги тут:
- Посібник по проходженню інтерв'ю для Java-розробників тут Amazon (Книга Kindle) та Gumroad(формат PDF).
- Посібник по проходженню інтерв'ю для Spring-Boot Microservice тут Gumroadd (PDF формат) та Amazon(Kindle eBook).
- 🔔 Слідуйте за мною: LinkedIn | Twitter | Youtube
Перекладено з: Recent Senior Java Developer Interview Experience