Java - Pytania Rekrutacyjne dla Backend Developera [2026]
Przygotowujesz się do rozmowy na stanowisko Java Backend Developer? Java pozostaje jednym z najpopularniejszych języków w enterprise i fintech. Ten przewodnik zawiera 150 pytań rekrutacyjnych z odpowiedziami - od podstaw języka przez OOP, kolekcje, wielowątkowość po Java 8+ i optymalizację wydajności.
Spis treści
- Podstawy języka Java
- Programowanie obiektowe (OOP)
- Kolekcje (Collections Framework)
- Wielowątkowość i współbieżność
- Java 8+ i nowoczesne funkcje
- Zarządzanie pamięcią i Garbage Collection
- Obsługa wyjątków
- Wzorce projektowe
- Zobacz też
Podstawy języka Java
Jakie są główne różnice między JDK, JRE a JVM?
Odpowiedź w 30 sekund: JVM (Java Virtual Machine) wykonuje bytecode Java. JRE (Java Runtime Environment) to JVM + biblioteki standardowe - wystarczy do uruchamiania aplikacji. JDK (Java Development Kit) to JRE + kompilator, debugger i narzędzia deweloperskie - potrzebny do tworzenia aplikacji.
Odpowiedź w 2 minuty:
Najlepiej zrozumieć strukturę poprzez diagram pokazujący hierarchię komponentów. Poniższy schemat ilustruje, jak JDK zawiera JRE, a JRE zawiera JVM.
┌─────────────────────────────────────────────────────┐
│ JDK │
│ ┌───────────────────────────────────────────────┐ │
│ │ javac (kompilator) │ │
│ │ jdb (debugger) │ │
│ │ jar, javadoc, jshell... │ │
│ └───────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────┐ │
│ │ JRE │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Biblioteki standardowe (rt.jar) │ │ │
│ │ │ java.lang, java.util, java.io... │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ JVM │ │ │
│ │ │ Class Loader → Bytecode Verifier │ │ │
│ │ │ → Interpreter → JIT Compiler │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
| Komponent | Zawiera | Użycie |
|---|---|---|
| JVM | Interpreter, JIT, GC | Wykonuje bytecode |
| JRE | JVM + biblioteki | Uruchamia aplikacje |
| JDK | JRE + narzędzia dev | Tworzy aplikacje |
# Sprawdzenie wersji
java -version # JRE/JVM
javac -version # JDK (kompilator)
Dlaczego Java jest uznawana za język niezależny od platformy?
Odpowiedź w 30 sekund: Java kompiluje się do bytecode'u (pliki .class), który jest niezależny od platformy. Ten bytecode jest następnie wykonywany przez JVM, która jest specyficzna dla każdego systemu operacyjnego. Stąd hasło "Write Once, Run Anywhere" - kod Java działa wszędzie, gdzie jest JVM.
Odpowiedź w 2 minuty:
Niezależność od platformy wynika z dwuetapowego procesu kompilacji. Poniższy diagram pokazuje, jak kod Java kompiluje się do bytecode'u, który następnie jest interpretowany przez JVM specyficzną dla każdego systemu.
┌────────────────┐
│ Kod Java │
│ (.java) │
└───────┬────────┘
│ javac (kompilacja)
▼
┌────────────────┐
│ Bytecode │ ← Niezależny od platformy
│ (.class) │
└───────┬────────┘
│
┌────┴────┬─────────┐
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│JVM │ │JVM │ │JVM │
│Windows│ │Linux │ │macOS │
└──────┘ └──────┘ └──────┘
W przeciwieństwie do C/C++, które kompilują się bezpośrednio do kodu maszynowego (specyficznego dla procesora), Java wprowadza warstwę abstrakcji:
// Ten sam kod działa na Windows, Linux, macOS
public class Hello {
public static void main(String[] args) {
System.out.println("Hello from any platform!");
}
}
Czym różni się operator == od metody equals()?
Odpowiedź w 30 sekund:
Operator == porównuje referencje - czy dwie zmienne wskazują na ten sam obiekt w pamięci. Metoda equals() porównuje wartości obiektów według zdefiniowanej logiki. Dla String: == może zwrócić false dla identycznych tekstów z różnych miejsc pamięci, equals() zwróci true.
Odpowiedź w 2 minuty:
Najlepiej zobrazować to na praktycznym przykładzie z String. Ten kod demonstruje różnicę między porównywaniem referencji a wartości.
// Przykład z String
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = s1;
s1 == s2 // false - różne obiekty w pamięci
s1 == s3 // true - ta sama referencja
s1.equals(s2) // true - ta sama wartość
// Uwaga na String pool!
String a = "hello"; // z puli
String b = "hello"; // ta sama referencja z puli
a == b // true! (optymalizacja JVM)
Pamięć heap:
┌─────────────────────────────────────────┐
│ [0x100] String "hello" ← s1, s3 │
│ [0x200] String "hello" ← s2 │
│ [0x300] String "hello" ← String pool │
│ ↑ a, b │
└─────────────────────────────────────────┘
Zasada: Zawsze używaj equals() do porównywania obiektów (szczególnie String), == tylko dla typów prymitywnych i sprawdzania null.
Jakie jest zastosowanie słowa kluczowego final w Javie?
Odpowiedź w 30 sekund:
final ma trzy zastosowania: dla zmiennych - wartość nie może być zmieniona po inicjalizacji; dla metod - metoda nie może być nadpisana w klasie potomnej; dla klas - klasa nie może być dziedziczona. Jest to mechanizm zapewnienia niezmienności i bezpieczeństwa kodu.
Odpowiedź w 2 minuty:
Słowo kluczowe final ma różne zastosowania w zależności od kontekstu. Poniższe przykłady pokazują wszystkie cztery główne przypadki użycia.
// 1. Final zmienna - stała wartość
final int MAX_SIZE = 100;
// MAX_SIZE = 200; // Błąd kompilacji!
// 2. Final referencja - nie zmienisz referencji, ale możesz obiekt
final List<String> list = new ArrayList<>();
list.add("OK"); // Można modyfikować zawartość
// list = new ArrayList<>(); // Błąd! Nie można zmienić referencji
// 3. Final metoda - nie można nadpisać
class Parent {
final void importantMethod() {
// Implementacja gwarantowana
}
}
// 4. Final klasa - nie można dziedziczyć
final class ImmutableClass {
// String, Integer, etc. są final
}
// class Child extends ImmutableClass {} // Błąd!
| Kontekst | Znaczenie | Przykład |
|---|---|---|
| Zmienna | Nie można zmienić wartości | final int x = 5; |
| Referencja | Nie można zmienić referencji | final List<> l = ... |
| Metoda | Nie można nadpisać | final void m() |
| Klasa | Nie można dziedziczyć | final class C |
Programowanie obiektowe (OOP)
Jakie są cztery główne filary OOP i jak są zaimplementowane w Javie?
Odpowiedź w 30 sekund:
- Enkapsulacja - ukrywanie danych za metodami (private + gettery/settery)
- Dziedziczenie - rozszerzanie klas (extends)
- Polimorfizm - wiele form tej samej metody (przesłanianie, przeciążanie)
- Abstrakcja - definiowanie interfejsów bez implementacji (abstract, interface)
Odpowiedź w 2 minuty:
Każdy filar OOP można zobrazować praktycznym przykładem w kodzie Java. Poniżej przedstawiamy wszystkie cztery filary z konkretnymi implementacjami.
// 1. ENKAPSULACJA - ukrywanie implementacji
class BankAccount {
private double balance; // ukryte
public void deposit(double amount) {
if (amount > 0) balance += amount; // kontrola dostępu
}
public double getBalance() {
return balance; // kontrolowany odczyt
}
}
// 2. DZIEDZICZENIE - ponowne użycie kodu
class SavingsAccount extends BankAccount {
private double interestRate;
public void addInterest() {
deposit(getBalance() * interestRate);
}
}
// 3. POLIMORFIZM - wiele form
class Animal {
void makeSound() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void makeSound() { System.out.println("Woof!"); } // przesłanianie
}
Animal animal = new Dog();
animal.makeSound(); // "Woof!" - polimorfizm w akcji
// 4. ABSTRAKCJA - kontrakt bez implementacji
interface Drawable {
void draw(); // co, nie jak
}
abstract class Shape implements Drawable {
abstract double area(); // wymuszenie implementacji
}
Czym różnią się klasy abstrakcyjne od interfejsów?
Odpowiedź w 30 sekund: Klasa abstrakcyjna może mieć konstruktor, pola instancji i metody z implementacją - używaj gdy klasy mają wspólną logikę. Interfejs definiuje tylko kontrakt (od Java 8 może mieć default methods) - używaj dla definiowania zdolności. Klasa może implementować wiele interfejsów, ale dziedziczyć tylko jedną klasę.
Odpowiedź w 2 minuty:
Różnice najlepiej widać w przykładzie porównującym obie konstrukcje. Kod poniżej pokazuje, kiedy stosować klasę abstrakcyjną, a kiedy interfejs.
// KLASA ABSTRAKCYJNA - "jest rodzajem" (is-a)
abstract class Vehicle {
protected String brand; // może mieć pola
Vehicle(String brand) { // może mieć konstruktor
this.brand = brand;
}
void start() { // może mieć implementację
System.out.println("Starting " + brand);
}
abstract void move(); // wymusza implementację
}
// INTERFEJS - "potrafi" (can-do)
interface Flyable {
void fly(); // domyślnie public abstract
default void land() { // Java 8+ - domyślna implementacja
System.out.println("Landing...");
}
}
// Jedna klasa, wiele interfejsów
class Airplane extends Vehicle implements Flyable, Serializable {
Airplane(String brand) { super(brand); }
@Override void move() { fly(); }
@Override public void fly() { System.out.println("Flying!"); }
}
| Cecha | Klasa abstrakcyjna | Interfejs |
|---|---|---|
| Dziedziczenie | Jedno (extends) | Wiele (implements) |
| Konstruktor | Tak | Nie |
| Pola instancji | Tak | Tylko static final |
| Metody z ciałem | Tak | Od Java 8 (default) |
| Modyfikatory metod | Dowolne | public (domyślnie) |
| Kiedy używać | Wspólna logika | Definiowanie zdolności |
Czym różni się kompozycja od dziedziczenia?
Odpowiedź w 30 sekund: Dziedziczenie ("is-a") to relacja rodzic-dziecko, gdzie klasa potomna rozszerza rodzica. Kompozycja ("has-a") to relacja zawierania, gdzie klasa posiada instancję innej klasy. Kompozycja jest preferowana, bo daje luźniejsze powiązanie, łatwiejsze testowanie i większą elastyczność.
Odpowiedź w 2 minuty:
Różnicę najlepiej pokazać na przykładzie implementacji Stack - jednej przez dziedziczenie, drugiej przez kompozycję. Zobaczysz, dlaczego kompozycja daje lepszą enkapsulację.
// ❌ DZIEDZICZENIE - silne powiązanie
class Stack extends ArrayList<Integer> {
public void push(int item) { add(item); }
public int pop() { return remove(size() - 1); }
// Problem: Stack dziedziczy WSZYSTKO z ArrayList
// np. add(index, element) - łamie semantykę stosu!
}
// ✅ KOMPOZYCJA - luźne powiązanie
class Stack {
private final List<Integer> items = new ArrayList<>(); // has-a
public void push(int item) { items.add(item); }
public int pop() { return items.remove(items.size() - 1); }
// Pełna kontrola nad API - tylko push/pop są publiczne
}
Zasada: "Favor composition over inheritance" (Gang of Four)
| Aspekt | Dziedziczenie | Kompozycja |
|---|---|---|
| Relacja | is-a (jest) | has-a (ma) |
| Powiązanie | Silne | Luźne |
| Elastyczność | Niska | Wysoka |
| Testowanie | Trudniejsze | Łatwiejsze (mock) |
| Zmiana w runtime | Nie | Tak (Strategy) |
Kolekcje (Collections Framework)
Jaka jest różnica między List, Set i Map?
Odpowiedź w 30 sekund: List - uporządkowana kolekcja z duplikatami, dostęp po indeksie. Set - kolekcja unikalnych elementów, bez duplikatów. Map - pary klucz-wartość, klucze unikalne. List gdy ważna kolejność, Set dla unikalności, Map dla asocjacji klucz→wartość.
Odpowiedź w 2 minuty:
Różnice między typami kolekcji najlepiej pokazać na prostych przykładach użycia. Poniższy kod demonstruje podstawowe operacje dla każdego typu.
// LIST - uporządkowana, duplikaty OK
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("A"); // OK - duplikat dozwolony
list.get(0); // "A" - dostęp po indeksie
// SET - unikalne elementy
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // Ignorowane - już istnieje
set.size(); // 2
// MAP - klucz → wartość
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);
map.put("Bob", 30);
map.get("Alice"); // 25
┌─────────────────────────────────────────────────────────┐
│ Collection │
├─────────────────────┬───────────────────────────────────┤
│ List │ Set │
│ [A, B, A, C] │ {A, B, C} │
│ - ArrayList │ - HashSet │
│ - LinkedList │ - TreeSet │
│ - Vector │ - LinkedHashSet │
└─────────────────────┴───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Map │
│ {A→1, B→2, C→3} │
│ - HashMap, TreeMap, LinkedHashMap │
└─────────────────────────────────────────────────────────┘
Jakie właściwości ma ArrayList w porównaniu z LinkedList?
Odpowiedź w 30 sekund: ArrayList używa tablicy dynamicznej - szybki dostęp O(1) po indeksie, wolne wstawianie O(n) w środek (przesuwanie elementów). LinkedList to lista dwukierunkowa - wolny dostęp O(n), szybkie wstawianie O(1) na końcach. ArrayList lepszy dla odczytu, LinkedList gdy częste modyfikacje na końcach.
Odpowiedź w 2 minuty:
Charakterystykę obu list najlepiej zobrazować porównując te same operacje. Kod poniżej pokazuje złożoność czasową typowych operacji dla obu implementacji.
// ArrayList - tablica pod spodem
ArrayList<String> array = new ArrayList<>();
array.get(500); // O(1) - bezpośredni dostęp
array.add("X"); // O(1) amortyzowane (resize co jakiś czas)
array.add(0, "X"); // O(n) - przesunięcie wszystkich
// LinkedList - węzły połączone
LinkedList<String> linked = new LinkedList<>();
linked.get(500); // O(n) - trzeba przejść 500 węzłów
linked.addFirst("X"); // O(1) - zmiana wskaźników
linked.addLast("X"); // O(1)
ArrayList:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │ │ │ capacity > size
└───┴───┴───┴───┴───┴───┴───┴───┘
↑ index 0 = O(1) access
LinkedList:
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ A │◄─►│ B │◄─►│ C │◄─►│ D │
└───┘ └───┘ └───┘ └───┘
↑ head ↑ tail
| Operacja | ArrayList | LinkedList |
|---|---|---|
| get(index) | O(1) | O(n) |
| add(end) | O(1)* | O(1) |
| add(start) | O(n) | O(1) |
| add(middle) | O(n) | O(n)** |
| remove(index) | O(n) | O(n) |
| Memory | Kompaktowa | Więcej (wskaźniki) |
*amortyzowane, **O(1) jak mamy iterator
Jak HashMap przechowuje pary klucz-wartość i co to jest kolizja?
Odpowiedź w 30 sekund: HashMap używa tablicy "bucketów". Klucz jest haszowany do indeksu tablicy, gdzie trafia para klucz-wartość. Kolizja występuje, gdy różne klucze mają ten sam hash (indeks). Java rozwiązuje to przez linked list lub drzewo (od Java 8) w danym buckecie.
Odpowiedź w 2 minuty:
Mechanizm HashMap opiera się na hashowaniu klucza do indeksu tablicy. Poniższy przykład pokazuje krok po kroku, jak działa dodawanie i pobieranie wartości.
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);
// 1. hashCode("Alice") → np. 123456
// 2. index = hash % capacity → np. 4
// 3. bucket[4] = Entry("Alice", 25)
map.put("Bob", 30); // index = 7
map.get("Alice"); // hash → index 4 → znajdź Entry
HashMap structure:
┌─────────────────────────────────────────┐
│ buckets[] (tablica) │
├─────┬───────────────────────────────────┤
│ 0 │ null │
│ 1 │ null │
│ 2 │ Entry("Carol", 35) │
│ 3 │ null │
│ 4 │ Entry("Alice", 25) → Entry("Eve", 28) ← KOLIZJA!
│ 5 │ null │
│ 6 │ null │
│ 7 │ Entry("Bob", 30) │
└─────┴───────────────────────────────────┘
Od Java 8: gdy bucket > 8 elementów → TreeNode (O(log n))
Dlaczego equals() i hashCode() muszą być spójne?
// Jeśli a.equals(b) → a.hashCode() == b.hashCode()
// Inaczej: put(a, v1) i get(b) nie znajdą wartości!
Czym różni się ConcurrentHashMap od HashMap?
Odpowiedź w 30 sekund: HashMap nie jest thread-safe - współbieżny dostęp może uszkodzić dane. ConcurrentHashMap jest thread-safe dzięki segmentowej synchronizacji (od Java 8: lock na poziomie bucketa). Pozwala na współbieżny odczyt bez blokowania i ograniczone blokowanie przy zapisie.
Odpowiedź w 2 minuty:
Porównanie trzech podejść do map w środowisku wielowątkowym pokazuje ewolucję rozwiązań. Kod demonstruje, dlaczego ConcurrentHashMap jest najlepszym wyborem dla współbieżności.
// ❌ HashMap - nie thread-safe
Map<String, Integer> hashMap = new HashMap<>();
// Wiele wątków pisząc jednocześnie = corrupted data!
// ❌ Collections.synchronizedMap - blokuje całą mapę
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// Jeden wątek pisze = wszystkie czekają
// ✅ ConcurrentHashMap - granularna synchronizacja
ConcurrentHashMap<String, Integer> concMap = new ConcurrentHashMap<>();
// Blokuje tylko bucket, nie całą mapę
concMap.put("key", 1); // Lock na bucket
concMap.get("key"); // Bez locka (volatile read)
concMap.compute("key", (k, v) -> v + 1); // Atomic update
| Cecha | HashMap | ConcurrentHashMap |
|---|---|---|
| Thread-safe | Nie | Tak |
| Null keys/values | Tak | Nie |
| Blokowanie | - | Per bucket |
| Odczyt | - | Lock-free |
| Iteracja | Fail-fast | Weakly consistent |
| Wydajność | Najlepsza (single) | Dobra (multi) |
Wielowątkowość i współbieżność
Jak utworzyć wątek w Javie?
Odpowiedź w 30 sekund:
Dwa główne sposoby: 1) Rozszerzenie klasy Thread i nadpisanie run(), 2) Implementacja interfejsu Runnable i przekazanie do Thread. Preferowany jest Runnable - pozwala na dziedziczenie z innej klasy i lepszą separację logiki od mechanizmu wątków.
Odpowiedź w 2 minuty:
W Javie mamy cztery główne sposoby tworzenia wątków. Kod poniżej pokazuje wszystkie metody - od podstawowych po nowoczesne z ExecutorService.
// Sposób 1: extends Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + getName());
}
}
new MyThread().start();
// Sposób 2: implements Runnable (preferowany)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running");
}
}
new Thread(new MyRunnable()).start();
// Sposób 3: Lambda (Java 8+)
new Thread(() -> System.out.println("Lambda running")).start();
// Sposób 4: ExecutorService (produkcyjny kod)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("Executor running"));
executor.shutdown();
| Sposób | Zalety | Wady |
|---|---|---|
| extends Thread | Prosty | Blokuje dziedziczenie |
| Runnable | Elastyczny | Trochę więcej kodu |
| Lambda | Zwięzły | Tylko dla prostych przypadków |
| ExecutorService | Pula wątków, kontrola | Wymaga shutdown |
Jak działa słowo kluczowe synchronized?
Odpowiedź w 30 sekund:
synchronized zapewnia, że tylko jeden wątek naraz wykonuje dany blok kodu. Można synchronizować metodę (lock na this lub klasie dla static) lub blok kodu (lock na dowolnym obiekcie). Zapewnia wzajemne wykluczanie i widoczność zmian między wątkami.
Odpowiedź w 2 minuty:
Słowo synchronized można użyć na różne sposoby, aby chronić współdzielony stan. Poniższe przykłady pokazują synchronizację metody, bloku kodu i metody statycznej.
class Counter {
private int count = 0;
// Synchronized metoda - lock na this
public synchronized void increment() {
count++; // Bezpieczne
}
// Synchronized blok - bardziej granularny
public void incrementBlock() {
synchronized(this) {
count++;
}
}
// Static synchronized - lock na klasie
private static int globalCount = 0;
public static synchronized void incrementGlobal() {
globalCount++;
}
}
Wątek 1 Wątek 2
│ │
▼ ▼
acquire lock ───────┐ try acquire lock
│ │ │
▼ │ ▼
count++ │ BLOCKED
│ │ │
▼ │ │
release lock ◄──────┘ │
▼
acquire lock
│
▼
count++
Wady synchronized:
- Blokuje cały wątek
- Możliwe deadlocki
- Nie można przerwać oczekiwania
- Lepsze alternatywy:
ReentrantLock,AtomicInteger,ConcurrentHashMap
Jaka jest różnica między volatile a zmiennymi atomic?
Odpowiedź w 30 sekund: volatile zapewnia widoczność zmian między wątkami (każdy odczyt z głównej pamięci), ale nie atomowość operacji. Atomic (np. AtomicInteger) zapewnia zarówno widoczność jak i atomowe operacje (np. incrementAndGet) bez synchronizacji, używając instrukcji CAS procesora.
Odpowiedź w 2 minuty:
Różnica między volatile a atomic staje się jasna, gdy spojrzymy na operację inkrementacji. Kod pokazuje, dlaczego volatile nie wystarczy dla operacji złożonych.
// ❌ volatile - widoczność, ale nie atomowość
class VolatileCounter {
private volatile int count = 0;
public void increment() {
count++; // NIE atomowe! (read-modify-write)
// count = count + 1 → 3 operacje
}
}
// ✅ AtomicInteger - atomowe operacje
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Atomowe CAS
}
public void conditionalUpdate() {
count.compareAndSet(5, 10); // Atomic: jeśli 5 → ustaw 10
}
}
volatile:
┌─────────┐ write ┌─────────────┐ read ┌─────────┐
│ Thread 1│ ──────────► │ Main Memory │ ◄────────── │ Thread 2│
└─────────┘ └─────────────┘ └─────────┘
Gwarancja widoczności
Atomic (CAS - Compare And Swap):
1. Odczytaj wartość
2. Oblicz nową wartość
3. CAS: jeśli stara == oczekiwana → zapisz nową (atomowo!)
4. Jeśli nie → powtórz (spin)
| Cecha | volatile | Atomic |
|---|---|---|
| Widoczność | Tak | Tak |
| Atomowość | Nie | Tak |
| Operacje | Proste read/write | increment, CAS, etc. |
| Overhead | Niski | Niski (hardware) |
| Użycie | Flagi, pojedyncze wartości | Liczniki, współdzielony stan |
Java 8+ i nowoczesne funkcje
Czym są wyrażenia Lambda i jak działają?
Odpowiedź w 30 sekund:
Lambda to skrócony zapis anonimowych funkcji: (parametry) -> wyrażenie. Pod spodem kompilator tworzy instancję interfejsu funkcyjnego. Lambdy umożliwiają programowanie funkcyjne w Javie - przekazywanie zachowań jako argumentów, operacje na kolekcjach przez Stream API.
Odpowiedź w 2 minuty:
Lambda to syntaktyczny cukier, który znacznie upraszcza kod. Porównanie starego i nowego sposobu pokazuje, jak wielką zmianą była Java 8.
// Przed Java 8 - anonimowa klasa
Runnable oldWay = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Java 8+ - lambda
Runnable newWay = () -> System.out.println("Hello");
// Przykłady składni
Comparator<String> comp = (a, b) -> a.compareTo(b);
Function<String, Integer> len = s -> s.length();
Consumer<String> print = System.out::println; // method reference
// Użycie w Stream API
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(n -> n.length() > 3) // lambda jako predykat
.map(String::toUpperCase) // method reference
.forEach(System.out::println); // lambda jako consumer
Interfejsy funkcyjne (jeden abstract method):
| Interfejs | Metoda | Sygnatura |
|---|---|---|
Function<T,R> |
apply | T → R |
Consumer<T> |
accept | T → void |
Supplier<T> |
get | () → T |
Predicate<T> |
test | T → boolean |
Runnable |
run | () → void |
Jak różnią się operacje pośrednie od końcowych w Stream API?
Odpowiedź w 30 sekund: Operacje pośrednie (filter, map, sorted) są leniwe - nie wykonują się od razu, tylko budują pipeline. Zwracają nowy Stream. Operacje końcowe (collect, forEach, count) wyzwalają przetwarzanie całego pipeline'u i zwracają wynik lub void. Stream można użyć tylko raz!
Odpowiedź w 2 minuty:
Kluczowa różnica polega na leniowym wykonywaniu operacji pośrednich. Poniższy kod demonstruje, że operacje pośrednie nie wykonują się do momentu wywołania operacji końcowej.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// Pipeline - nic się nie wykonuje do terminal operation!
Stream<String> pipeline = names.stream()
.filter(n -> {
System.out.println("Filtering: " + n); // Nie wypisze się!
return n.length() > 3;
})
.map(String::toUpperCase); // Nadal leniwe
// Terminal operation - teraz się wykonuje
List<String> result = pipeline.collect(Collectors.toList());
// Wypisze: Filtering: Alice, Filtering: Bob, ...
| Operacje pośrednie | Operacje końcowe |
|---|---|
filter() |
collect() |
map() |
forEach() |
flatMap() |
reduce() |
sorted() |
count() |
distinct() |
findFirst() |
limit() |
anyMatch() |
skip() |
toArray() |
// Praktyczny przykład
long count = products.stream()
.filter(p -> p.getPrice() > 100) // pośrednia
.map(Product::getName) // pośrednia
.distinct() // pośrednia
.count(); // końcowa - wyzwala pipeline
// Stream jest jednorazowy!
Stream<String> stream = names.stream();
stream.count();
stream.count(); // IllegalStateException!
Do czego służy klasa Optional?
Odpowiedź w 30 sekund:
Optional<T> to kontener, który może zawierać wartość lub być pusty. Służy do jawnego sygnalizowania, że metoda może nie zwrócić wartości - zamiast zwracać null. Zmusza programistę do obsługi braku wartości, eliminując NullPointerException.
Odpowiedź w 2 minuty:
Optional eliminuje kaskady sprawdzeń null i sprawia, że kod jest bardziej czytelny. Porównanie starego i nowego podejścia pokazuje siłę fluent API.
// ❌ Przed Optional - null sprawdzanie
public String getCity(User user) {
if (user != null) {
Address address = user.getAddress();
if (address != null) {
return address.getCity();
}
}
return "Unknown";
}
// ✅ Z Optional - fluent API
public String getCity(User user) {
return Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");
}
// Tworzenie Optional
Optional<String> present = Optional.of("value"); // rzuci NPE dla null
Optional<String> nullable = Optional.ofNullable(str); // bezpieczne
Optional<String> empty = Optional.empty();
// Pobieranie wartości
optional.get(); // rzuci NoSuchElementException jeśli pusty!
optional.orElse("default"); // bezpieczne z domyślną wartością
optional.orElseGet(() -> compute()); // leniwa kalkulacja domyślnej
optional.orElseThrow(() -> new CustomException());
// Sprawdzanie i transformacja
optional.isPresent(); // boolean
optional.ifPresent(v -> process(v)); // wykonaj jeśli jest
optional.map(String::toUpperCase); // transformuj jeśli jest
optional.filter(s -> s.length() > 3);// filtruj
Kiedy NIE używać Optional:
- Jako pola klasy (overhead)
- Jako parametru metody (lepszy overloading)
- Z kolekcjami (zwróć pustą kolekcję)
Zarządzanie pamięcią i Garbage Collection
Jaka jest struktura obszarów pamięci w JVM?
Odpowiedź w 30 sekund: JVM dzieli pamięć na: Heap (obiekty, GC), Stack (wywołania metod, zmienne lokalne), Metaspace (metadane klas), Code Cache (skompilowany kod JIT). Heap dzieli się na Young Generation (Eden + Survivor) i Old Generation.
Odpowiedź w 2 minuty:
Pamięć JVM jest podzielona na kilka głównych obszarów, każdy z osobną funkcją. Poniższy diagram przedstawia strukturę pamięci z podziałem na heap, metaspace, stack i code cache.
┌─────────────────────────────────────────────────────────┐
│ JVM Memory │
├─────────────────────────────────────────────────────────┤
│ HEAP (współdzielony, GC) │
│ ┌────────────────────────┬────────────────────────┐ │
│ │ Young Generation │ Old Generation │ │
│ │ ┌──────┬───────────┐ │ (Tenured) │ │
│ │ │ Eden │ Survivor │ │ │ │
│ │ │ │ S0 │ S1 │ │ Długożyjące obiekty │ │
│ │ └──────┴─────┴─────┘ │ │ │
│ │ Nowe obiekty tutaj │ │ │
│ └────────────────────────┴────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ METASPACE (poza heap) │
│ - Metadane klas, nazwy metod, constant pool │
│ - Zastąpił PermGen w Java 8 │
├─────────────────────────────────────────────────────────┤
│ STACK (per thread) │
│ - Ramki wywołań metod │
│ - Zmienne lokalne, referencje │
│ - Typy prymitywne │
├─────────────────────────────────────────────────────────┤
│ CODE CACHE │
│ - Skompilowany kod JIT │
└─────────────────────────────────────────────────────────┘
// Konfiguracja heap
java -Xms512m -Xmx2g MyApp // min 512MB, max 2GB
// Monitoring
jstat -gc <pid> // statystyki GC
jmap -heap <pid> // szczegóły heap
Na czym polega działanie Garbage Collection?
Odpowiedź w 30 sekund: GC automatycznie usuwa obiekty bez referencji. Proces: 1) Mark - oznacz osiągalne obiekty (od GC roots), 2) Sweep - usuń nieoznaczone. Young GC (Minor) - szybkie, częste, dla Young Gen. Old GC (Major/Full) - wolniejsze, dla całego heap. Nowoczesne GC (G1, ZGC) minimalizują pauzy.
Odpowiedź w 2 minuty:
Garbage Collection opiera się na algorytmie Mark & Sweep i generacyjnym modelu pamięci. Poniższy opis pokazuje, jak GC identyfikuje obiekty do usunięcia i jak działa proces generacyjny.
GC Roots (punkty startowe):
- Zmienne lokalne w stack'ach wątków
- Statyczne pola klas
- JNI references
- Aktywne wątki
Algorytm Mark & Sweep:
┌─────────────────────────────────────────┐
│ 1. MARK - przejdź graf od GC Roots │
│ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │───►│ B │───►│ C │ ✓ reachable │
│ └───┘ └───┘ └───┘ │
│ ┌───┐ │
│ │ D │ ✗ unreachable │
│ └───┘ │
│ 2. SWEEP - usuń nieoznaczone (D) │
└─────────────────────────────────────────┘
Generational GC:
┌─────────────────────────────────────────────────────┐
│ 1. Nowy obiekt → Eden │
│ 2. Eden pełny → Minor GC │
│ - Żywe obiekty → Survivor (S0/S1) │
│ 3. Po kilku cyklach → Old Generation │
│ 4. Old pełny → Major/Full GC (Stop-The-World!) │
└─────────────────────────────────────────────────────┘
| GC | Charakterystyka | Użycie |
|---|---|---|
| Serial | Single-threaded, STW | Małe aplikacje |
| Parallel | Multi-threaded, throughput | Batch processing |
| G1 | Balans pauzy/throughput | Domyślny Java 9+ |
| ZGC | Ultra-low latency (<1ms) | Duże heapy, real-time |
Obsługa wyjątków
Czym różnią się wyjątki checked od unchecked?
Odpowiedź w 30 sekund: Checked (np. IOException) - kompilator wymusza obsługę (try-catch lub throws), oznaczają sytuacje, z których program może się odzyskać. Unchecked (RuntimeException, np. NullPointerException) - nie wymagają deklaracji, oznaczają błędy programistyczne. Error - poważne problemy JVM (OutOfMemoryError).
Odpowiedź w 2 minuty:
Hierarchia wyjątków w Javie definiuje, które wymagają obsługi, a które nie. Poniższy diagram przedstawia strukturę klas wyjątków i ich kategoryzację.
Throwable
│
┌────────────┴────────────┐
│ │
Error Exception
(nie łapać!) │
- OutOfMemoryError ┌──────────┴──────────┐
- StackOverflowError │ │
RuntimeException Checked
(unchecked) - IOException
- NullPointerException - SQLException
- IllegalArgumentException
- ArrayIndexOutOfBounds
// CHECKED - musisz obsłużyć
public void readFile() throws IOException { // lub try-catch
FileReader reader = new FileReader("file.txt");
}
// UNCHECKED - nie wymaga deklaracji
public void process(String s) {
s.length(); // Może rzucić NullPointerException
// ale nie musisz deklarować
}
// Tworzenie własnych wyjątków
class BusinessException extends Exception { // checked
BusinessException(String msg) { super(msg); }
}
class ValidationException extends RuntimeException { // unchecked
ValidationException(String msg) { super(msg); }
}
| Typ | Przykłady | Obsługa |
|---|---|---|
| Checked | IOException, SQLException | Wymagana |
| Unchecked | NPE, IllegalArgumentException | Opcjonalna |
| Error | OOM, StackOverflow | Nie łapać |
Co to jest try-with-resources?
Odpowiedź w 30 sekund:
Try-with-resources (Java 7+) automatycznie zamyka zasoby implementujące AutoCloseable po zakończeniu bloku try. Eliminuje wyciek zasobów i boilerplate finally. Zasoby są zamykane w odwrotnej kolejności do otwarcia, nawet gdy wystąpi wyjątek.
Odpowiedź w 2 minuty:
Try-with-resources dramatycznie upraszcza zarządzanie zasobami. Porównanie starego i nowego podejścia pokazuje, ile boilerplate kodu można wyeliminować.
// ❌ Przed Java 7 - brzydkie, podatne na błędy
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
return reader.readLine();
} finally {
if (reader != null) {
try {
reader.close(); // close() też może rzucić!
} catch (IOException e) {
// co teraz?
}
}
}
// ✅ Try-with-resources - czyste i bezpieczne
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
return reader.readLine();
} // reader.close() wywołane automatycznie
// Wiele zasobów (zamykane w odwrotnej kolejności)
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
// użyj rs
} // zamknięcie: rs → stmt → conn
// Od Java 9 - istniejące zmienne
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
try (reader) { // effectively final
return reader.readLine();
}
AutoCloseable interface:
public interface AutoCloseable {
void close() throws Exception;
}
Wzorce projektowe
Jak zaimplementować Singleton thread-safe w Javie?
Odpowiedź w 30 sekund: Najlepsza implementacja to enum singleton (Joshua Bloch) - thread-safe, serialization-safe, zwięzły. Alternatywy: double-checked locking z volatile, static holder class. Unikaj lazy initialization holder dla prostych przypadków - enum wystarczy.
Odpowiedź w 2 minuty:
Istnieje kilka sposobów implementacji Singletona thread-safe. Kod poniżej pokazuje trzy podejścia - od najlepszego (enum) po klasyczne (double-checked locking).
// ✅ NAJLEPSZY: Enum Singleton
public enum DatabaseConnection {
INSTANCE;
public void connect() {
// połącz
}
}
// Użycie: DatabaseConnection.INSTANCE.connect();
// ✅ Static Holder - lazy, thread-safe
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // ładowane przy pierwszym wywołaniu
}
}
// ⚠️ Double-Checked Locking (poprawna wersja)
public class Singleton {
private static volatile Singleton instance; // MUSI być volatile!
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
// ❌ ZŁAMANE: bez volatile/synchronized
// ❌ Podatne na reflection attack (oprócz enum)
| Metoda | Thread-safe | Lazy | Serialization-safe |
|---|---|---|---|
| Enum | ✅ | Nie | ✅ |
| Static Holder | ✅ | ✅ | Wymaga readResolve |
| Double-Checked | ✅ | ✅ | Wymaga readResolve |
Czym różni się Factory Method od Abstract Factory?
Odpowiedź w 30 sekund: Factory Method - jedna metoda tworząca jeden typ produktu, podklasy decydują co utworzyć. Abstract Factory - fabryka fabryk, tworzy rodziny powiązanych produktów. Factory Method dla jednego produktu z wariacjami, Abstract Factory dla zestawów kompatybilnych obiektów.
Odpowiedź w 2 minuty:
Różnica polega na zakresie odpowiedzialności wzorca. Factory Method tworzy warianty jednego produktu, podczas gdy Abstract Factory tworzy rodziny kompatybilnych produktów.
// FACTORY METHOD - jeden produkt, wiele wariantów
abstract class Document {
public abstract Page createPage(); // Factory Method
public void addPage() {
Page page = createPage(); // subclass decyduje
pages.add(page);
}
}
class PDFDocument extends Document {
@Override
public Page createPage() {
return new PDFPage(); // konkretny produkt
}
}
class WordDocument extends Document {
@Override
public Page createPage() {
return new WordPage();
}
}
Dla kontrastu, Abstract Factory tworzy całe zestawy powiązanych obiektów. Przykład poniżej pokazuje fabrykę komponentów GUI dla różnych systemów operacyjnych.
// ABSTRACT FACTORY - rodziny produktów
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
class WindowsFactory implements GUIFactory {
public Button createButton() { return new WindowsButton(); }
public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
class MacFactory implements GUIFactory {
public Button createButton() { return new MacButton(); }
public Checkbox createCheckbox() { return new MacCheckbox(); }
}
// Użycie - gwarancja kompatybilności komponentów
GUIFactory factory = new WindowsFactory();
Button btn = factory.createButton(); // WindowsButton
Checkbox chk = factory.createCheckbox(); // WindowsCheckbox
| Aspekt | Factory Method | Abstract Factory |
|---|---|---|
| Tworzy | Jeden produkt | Rodzinę produktów |
| Mechanizm | Dziedziczenie | Kompozycja |
| Elastyczność | Typ produktu | Cała rodzina |
| Złożoność | Niska | Wyższa |
Zobacz też
- Kompletny Przewodnik - Rozmowa Java Backend Developer - pełny przewodnik przygotowania do rozmowy Java
- Spring Boot Pytania Rekrutacyjne - framework Spring Boot
- SQL Pytania Rekrutacyjne - bazy danych relacyjne
- Wzorce i Architektura Backend - wzorce projektowe i architektura
Chcesz przećwiczyć te pytania? Sprawdź nasze fiszki Java z 150 pytaniami rekrutacyjnymi - idealne do nauki przed rozmową kwalifikacyjną.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
