Java - Pytania Rekrutacyjne dla Backend Developera [2026]

Sławomir Plamowski 25 min czytania
backend gc java kolekcje oop stream-api wielowątkowość

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

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:

  1. Enkapsulacja - ukrywanie danych za metodami (private + gettery/settery)
  2. Dziedziczenie - rozszerzanie klas (extends)
  3. Polimorfizm - wiele form tej samej metody (przesłanianie, przeciążanie)
  4. 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ż


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.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.