Przejdź do treści
← wszystkie przedmioty

Java

Głębokie, aktualne semantyczne podstawy Javy 21 LTS: wyjątki, kontrakt equals/hashCode, rekordy, sealed classes, generyki, Stream API i wątki wirtualne.

TEMATY EGZAMINACYJNE W TYM PRZEDMIOCIE
  • Kontrakt equals()/hashCode(): łamanie go psuje HashMap i HashSet
  • Checked vs unchecked exceptions oraz try-with-resources z suppressed exceptions
  • Rekordy (records) jako zamiennik boilerplate'owych DTO
  • Sealed classes/interfaces + pattern matching dla switch: wyczerpujące dopasowanie
  • Stream API: leniwość operacji pośrednich, terminal ops, kolektory
  • Wątki wirtualne (Project Loom) vs platformowe, w tym pinning

1 · Wyjątki: checked vs unchecked i try-with-resources

★ egzamin

Throwable dzieli się na Error (błędy poziomu JVM, nie do przechwytywania w normalnym kodzie) i Exception. Exception dzieli się dalej: podklasy RuntimeException to wyjątki niekontrolowane (unchecked), reszta to wyjątki kontrolowane (checked), wymuszane przez kompilator.

Hierarchia
Throwable → Error | Exception. Exception → RuntimeException (unchecked) | pozostałe klasy (checked, np. IOException, SQLException).
Termin kluczowy
Checked exception - Wyjątek niebędący podklasą RuntimeException; kompilator wymaga jego obsłużenia (catch) lub zadeklarowania (throws) w sygnaturze metody.
Termin kluczowy
Unchecked exception - Podklasa RuntimeException (lub Error); kompilator nie wymaga catch ani throws, np. NullPointerException, IllegalArgumentException, IllegalStateException.
Checked wymusza throws, unchecked nie java
// Checked: wywołujący MUSI obsłużyć lub zadeklarować
void readConfig(String path) throws IOException {
    Files.readString(Path.of(path));
}

// Unchecked: wywołujący MOŻE obsłużyć, kompilator nie wymusza
int divide(int a, int b) {
    if (b == 0) throw new IllegalArgumentException("Divisor cannot be zero");
    return a / b;
}
Pułapka
Łapanie wyjątku i ignorowanie go pustym catch(Exception e) {} ukrywa błąd i utrudnia debugowanie: loguj albo propaguj dalej, nigdy nie połykaj w ciszy.
  • Unchecked dla błędów programistycznych (złe argumenty, zły stan): klient i tak nie mógł się przed nimi zabezpieczyć w runtime.
  • Checked tylko gdy wywołujący realnie może i powinien się odzyskać (np. brak pliku, retry po timeout sieciowym).
  • Nadużywanie checked exceptions w warstwie domenowej prowadzi do zaśmieconych sygnatur i przechwytywania na wszelki wypadek.
Zasób AutoCloseable zamykany automatycznie java
static void readFirstLine(Path path) throws IOException {
    try (BufferedReader reader = Files.newBufferedReader(path)) {
        System.out.println(reader.readLine());
    }
}
Kolejność zamykania
Przy wielu zasobach w jednym try(...) zamykane są w kolejności odwrotnej do deklaracji: ostatni zadeklarowany zamyka się pierwszy.
Suppressed exceptions
Jeśli wyjątek wystąpi zarówno w bloku try, jak i w close(), wyjątek z try jest wyjątkiem głównym, a wyjątek z close() trafia do niego jako suppressed (Throwable.getSuppressed()).
Mnemonik
Checked to kompilator pilnujący jak nauczyciel; unchecked to błąd programisty, którego kompilator nie wyłapie za ciebie.

2 · equals(), hashCode() i pułapki autoboxingu

★ egzamin

Object.equals() domyślnie porównuje referencje (==), a Object.hashCode() zwraca wartość zależną od JVM (zwykle powiązaną z adresem w pamięci). Nadpisanie jednej metody bez drugiej to najczęstszy sposób na złamanie kolekcji haszujących.

  • Zwrotność: x.equals(x) zawsze true
  • Symetria: x.equals(y) wtedy i tylko wtedy, gdy y.equals(x)
  • Przechodniość: x.equals(y) i y.equals(z) implikuje x.equals(z)
  • Spójność: powtarzane wywołania dają ten sam wynik, jeśli pola się nie zmieniły
  • x.equals(null) zawsze zwraca false
Kontrakt hashCode()
Obiekty równe wg equals() MUSZĄ mieć ten sam hashCode(). Obiekty nierówne MOGĄ mieć ten sam hashCode() (kolizja dozwolona, ale pogarsza wydajność HashMap).
Poprawna para equals/hashCode z Objects.hash java
final class Point {
    private final int x, y;

    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        return o instanceof Point p && x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}
Pułapka
Nadpisanie equals() bez hashCode() sprawia, że obiekt znika z HashSet/HashMap: dwa logicznie równe obiekty trafiają do różnych kubełków, bo mają różny hashCode().
Termin kluczowy
Autoboxing - Automatyczna konwersja typu prymitywnego na jego wrapper (np. int na Integer) i odwrotnie (unboxing), wykonywana niejawnie przez kompilator.
Cache Integer
Integer.valueOf(int) cache'uje instancje dla wartości od -128 do 127 (JLS 5.1.7); autoboxing w tym zakresie zwraca tę samą instancję, poza nim tworzy nowy obiekt.
Pułapka == na typach opakowujących java
Integer a = 127, b = 127;
System.out.println(a == b); // true, cache -128..127

Integer c = 128, d = 128;
System.out.println(c == d); // false, poza cache, różne obiekty

System.out.println(c.equals(d)); // true, poprawne porównanie wartości
Pułapka
Kod testowany na małych liczbach (np. w pętli 0..10) może działać z ==, bo trafia w cache, a w produkcji na większych wartościach cichnie w losowe błędy logiczne. Zawsze .equals() do porównania wartości, == tylko do celowego porównania referencji.
NullPointerException przy unboxingu
Integer x = null; int y = x; rzuca NullPointerException w momencie unboxingu, nie w momencie przypisania null.
Mnemonik
equals mówi CO jest równe, hashCode mówi GDZIE tego szukać w tablicy haszującej: muszą się zgadzać.

3 · String: niezmienność i string pool

★ egzamin

String w Javie jest niezmienny (immutable): raz utworzonego obiektu String nie da się zmodyfikować. Każda operacja pozornie modyfikująca (np. concat, replace, toUpperCase) zwraca nowy obiekt.

  • Bezpieczeństwo wątkowe bez synchronizacji: niezmienny obiekt można bezpiecznie współdzielić między wątkami
  • Bezpieczne cache'owanie hashCode() (pole prywatne liczone raz, potem zwracane bez przeliczania)
  • Bezpieczeństwo w classloaderach, ścieżkach plików, połączeniach sieciowych: wartość nie może się zmienić pod spodem po walidacji
  • Współdzielenie literałów w string pool bez ryzyka, że jeden fragment kodu zmodyfikuje string używany gdzie indziej
Termin kluczowy
String pool - Pula literałów String w pamięci JVM (część sterty od Javy 7), w której kompilator automatycznie interniuje literały: identyczne literały wskazują na ten sam obiekt.
Literały dzielą jedną instancję z puli, new String() nie java
String a = "Tenzan";
String b = "Tenzan";
System.out.println(a == b); // true, ten sam obiekt z string pool

String c = new String("Tenzan");
System.out.println(a == c);          // false, nowy obiekt na stercie
System.out.println(a == c.intern()); // true, intern() zwraca referencję z puli
Pułapka
String c = new String("Tenzan") celowo omija pulę i tworzy nowy obiekt na stercie; porównywanie go przez == z literałem da false, mimo identycznej wartości. To najczęstsza pułapka na rozmowach rekrutacyjnych.
intern()
String.intern() zwraca referencję z puli dla danej wartości, dodając ją do puli, jeśli jeszcze tam nie ma. Po intern() porównanie == z literałem o tej samej treści zwraca true.
Pułapka
Konkatenacja stringów operatorem + wewnątrz pętli tworzy nowy obiekt String przy każdej iteracji (bo String jest niezmienny); kompilator optymalizuje pojedyncze wyrażenie +, ale nie serię dołączeń w pętli.
StringBuilder zamiast + w pętli: jeden mutowalny bufor java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i).append(',');
}
String result = sb.toString();
Compact Strings (Java 9+)
Od Javy 9 (JEP 254) String przechowuje dane w byte[] z kodowaniem Latin-1, gdy to możliwe, zamiast char[] UTF-16; oszczędza to zwykle około połowę pamięci dla typowych tekstów ASCII/Latin.
Mnemonik
String się nie zmienia, tylko referencja wskazuje gdzie indziej: +, replace, substring zawsze zwracają nowy obiekt.

4 · Rekordy (records): nowoczesny nośnik danych

★ egzamin

Rekord (Java 16+, JEP 395) to zwięzła, niezmienna klasa danych: jedna deklaracja generuje konstruktor kanoniczny, prywatne pola final, publiczne akcesory, equals(), hashCode() i toString(), eliminując boilerplate ręcznie pisanych DTO.

Jedna linijka zamiast klasy z gettersami, equals, hashCode, toString java
public record Employee(String name, String email, double salary) {}

Employee e = new Employee("Anna Kowalska", "anna@firma.pl", 9500.0);
System.out.println(e.name()); // "Anna Kowalska", brak prefiksu "get"
System.out.println(e);        // Employee[name=Anna Kowalska, email=anna@firma.pl, salary=9500.0]
Co generuje kompilator
Konstruktor kanoniczny (parametry = komponenty rekordu), prywatne pola final o tych samych nazwach, publiczne metody akcesorów o nazwie pola bez prefiksu get (np. name(), nie getName()), equals()/hashCode() porównujące wszystkie komponenty, toString() w formacie NazwaRekordu[pole=wartość, ...].
Termin kluczowy
Compact constructor - Konstruktor bez jawnej listy parametrów (używa niejawnie parametrów o nazwach komponentów), pozwala walidować lub normalizować dane wejściowe przed niejawnym przypisaniem do pól.
Walidacja bez powtarzania listy parametrów java
public record Range(int min, int max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException("min > max");
        }
        // brak this.min = min: przypisanie pól jest niejawne
    }
}
Pułapka
W konstruktorze kompaktowym nie wolno pisać this.min = min; jawnie: przypisanie do pól jest niejawne i następuje automatycznie po jego zakończeniu. Wolno tylko modyfikować same parametry (np. min = Math.abs(min);).
  • Rekord jest niejawnie final: nie można go rozszerzyć
  • Rekord nie może extends innej klasy (niejawnie dziedziczy po java.lang.Record), ale może implements dowolną liczbę interfejsów
  • Nie można deklarować dodatkowych pól instancyjnych poza komponentami rekordu (statyczne pola/metody są dozwolone)
  • Można nadpisać wygenerowane equals()/hashCode()/toString() lub dodać własne metody i konstruktory pomocnicze
Pułapka
Rekordy nie nadają się do encji z tożsamością i mutowalnym stanem (np. encje JPA z wymaganym bezargumentowym konstruktorem i setterami): to narzędzie do przenoszenia niemutowalnych danych, nie do modelowania stanu zmieniającego się w czasie.
Integracja z pattern matching
Od Javy 21 rekordy można dekonstruować we wzorcach (record patterns) w instanceof i switch, wyciągając komponenty bezpośrednio bez wywoływania akcesorów.
Mnemonik
Rekord to dane plus tożsamość wartości (equals po zawartości), klasa to zachowanie plus tożsamość referencyjna.

5 · Hierarchie typów: interfejsy, klasy abstrakcyjne, sealed classes i pattern matching

★ egzamin

Interfejsy i klasy abstrakcyjne to dwa narzędzia do modelowania hierarchii typów; sealed classes/interfaces (Java 17+) dodają kontrolę nad tym, kto może rozszerzać typ, a pattern matching dla switch (Java 21) pozwala wyczerpująco go skonsumować bez branchu default.

Interfejs

  • Wielodziedziczenie: klasa może implementować wiele interfejsów
  • Metody: abstract, default (Java 8+), static, private (Java 9+); brak stanu instancyjnego poza stałymi
  • Kontrakt zdolności: co obiekt POTRAFI zrobić

Klasa abstrakcyjna

  • Jednodziedziczenie: tylko jedna klasa bazowa
  • Może mieć konstruktory, pola instancyjne, metody z dowolną widocznością (private/protected/public)
  • Hierarchia 'jest': wspólna implementacja i stan dla podklas
Domyślna implementacja (Java 8+) i prywatna metoda pomocnicza (Java 9+) w interfejsie java
public interface PaymentProcessor {
    void charge(double amount);

    default void chargeWithLog(double amount) {
        log(amount);
        charge(amount);
    }

    private void log(double amount) {
        System.out.printf("Charging %.2f%n", amount);
    }
}
Termin kluczowy
sealed - Modyfikator (Java 17+) ograniczający zbiór klas/interfejsów mogących bezpośrednio rozszerzać dany typ do jawnie wymienionych w klauzuli permits.
Zamknięta hierarchia: tylko te trzy typy mogą implementować Shape java
public sealed interface Shape permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
Wymóg dla podtypów
Każda klasa wymieniona w permits musi być zadeklarowana jako final, sealed (dalej ogranicza) albo non-sealed (jawnie otwiera hierarchię z powrotem); brak jednego z tych słów to błąd kompilacji.
Switch bez default: kompilator sprawdza wyczerpanie wszystkich permitted typów java
static double area(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square s -> s.side() * s.side();
        case Triangle t -> t.base() * t.height() / 2;
    };
}
Dekonstrukcja rekordu we wzorcu plus warunek when java
record Point(int x, int y) {}

static String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y) when x == y -> "Punkt na przekątnej: " + x;
        case Point(int x, int y) -> "Punkt (" + x + ", " + y + ")";
        default -> "Nieznany obiekt";
    };
}
Pułapka
Przed sealed classes jedyną ochroną przed nieobsłużonym podtypem w switch/if-else był ręcznie pisany branch default rzucający wyjątek w runtime; sealed plus pattern matching przenosi ten błąd z runtime na czas kompilacji.
  • Interfejs: gdy potrzebujesz kontraktu implementowanego przez niepowiązane typy (np. Comparable, Runnable)
  • Klasa abstrakcyjna: gdy podklasy dzielą realną implementację i stan
  • Sealed: gdy znasz z góry pełny, zamknięty zbiór wariantów (np. reprezentacja AST, wynik operacji: Success/Failure)
Mnemonik
sealed mówi 'to są WSZYSTKIE możliwe podtypy', switch z pattern matching mówi 'obsłużyłem je WSZYSTKIE, kompilator to potwierdza'.

6 · Generyki, type erasure i var

★ egzamin

Generyki dają bezpieczeństwo typów w czasie kompilacji bez rzutowań, ale JVM ich nie widzi w runtime: mechanizm type erasure usuwa parametry typu z bytecode'u, zostawiając tylko ich ograniczenia (bounds) albo Object.

Co znika w runtime
List<String> i List<Integer> mają dokładnie tę samą klasę w runtime: List.class. Nie da się zrobić instanceof List<String>, tylko instanceof List<?> (wildcard nieograniczony).
Bounded type parameter: T musi implementować Comparable<T> java
static <T extends Comparable<T>> T max(List<T> list) {
    return list.stream()
        .max(Comparator.naturalOrder())
        .orElseThrow(NoSuchElementException::new);
}
Termin kluczowy
PECS (Producer Extends, Consumer Super) - Reguła doboru wildcardów: użyj ? extends T dla źródła, z którego tylko czytasz (producenta), i ? super T dla celu, do którego tylko zapisujesz (konsumenta).
Producer extends, consumer super java
static void copy(List<? extends Number> source, List<? super Number> destination) {
    for (Number n : source) {
        destination.add(n);
    }
}
Pułapka
new T[10] nie kompiluje się przy generycznym T: erasure nie zna rzeczywistego typu elementu w runtime, więc JVM nie potrafi utworzyć poprawnie otypowanej tablicy. Obejście: Array.newInstance(...) z jawnym Class<T> i rzutowaniem, albo użycie List<T> zamiast tablicy.
Bridge methods
Kompilator generuje niewidoczne metody mostkujące (bridge methods), gdy nadpisanie metody generycznej po erasure miałoby inną sygnaturę niż w typie bazowym; dzięki temu polimorfizm działa mimo erasure.
Termin kluczowy
var - Lokalne wnioskowanie typu (Java 10+, JEP 286): kompilator wyprowadza konkretny typ statyczny zmiennej lokalnej z wyrażenia inicjalizującego. To nie jest typowanie dynamiczne; typ jest ustalany raz, w czasie kompilacji.
var tam, gdzie typ jest oczywisty z prawej strony java
var users = new ArrayList<String>();  // typ: ArrayList<String>
for (var user : users) {              // typ: String
    System.out.println(user.toUpperCase());
}

try (var conn = dataSource.getConnection()) {
    // typ: Connection
}
Pułapka
var wymaga inicjalizatora (var x; nie kompiluje się), nie działa dla pól klasy ani zwykłych parametrów metod (dozwolone od Javy 11 tylko dla parametrów lambdy), i nie powinno być nadużywane tam, gdzie ukrywa typ niejasny z kontekstu (np. var result = process();).
Mnemonik
Generyki plus var to dwa różne poziomy: generyki to bezpieczeństwo typu w API, var to wygoda zapisu bez zmiany rzeczywistego typu.

7 · Stream API: leniwość, operacje terminalne, kolektory

★ egzamin

Stream API opisuje przetwarzanie danych deklaratywnie: źródło, operacje pośrednie (intermediate), operacja terminalna (terminal). Operacje pośrednie są leniwe: nic się nie wykonuje, dopóki nie pojawi się operacja terminalna.

Leniwość
map(), filter(), sorted(), distinct(), peek() budują tylko opis pipeline'u. Faktyczne przejście po elementach następuje dopiero przy wywołaniu operacji terminalnej, i to element po elemencie (nie faza po fazie).
Bez operacji terminalnej pipeline się nie wykonuje java
List<String> names = List.of("Ala", "Bartek", "Celina");

names.stream()
    .filter(n -> {
        System.out.println("filter: " + n);
        return n.length() > 3;
    })
    .map(String::toUpperCase);
// brak wypisania na konsolę - bez operacji terminalnej pipeline się nie wykonuje
Termin kluczowy
Operacja terminalna - Operacja kończąca pipeline i faktycznie konsumująca strumień, zwracająca wynik lub efekt uboczny (collect, forEach, reduce, count, anyMatch, findFirst...). Po jej wywołaniu strumień jest zamknięty.
Pułapka
Ponowne użycie tego samego obiektu Stream po wywołaniu operacji terminalnej rzuca IllegalStateException ('stream has already been operated upon or closed'). Strumień to jednorazowy potok, nie kolekcja wielokrotnego użytku.
Grupowanie po długości nazwy java
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));
  • toList() / toSet() / toUnmodifiableList(): zbiór wynikowy
  • toMap(keyFn, valueFn): mapa klucz-wartość
  • joining(delimiter): łączenie Stringów
  • groupingBy(classifier): grupowanie po kluczu
  • partitioningBy(predicate): podział na true/false
  • counting(), summarizingInt(): agregacje liczbowe
Short-circuiting
Operacje takie jak limit(), findFirst(), anyMatch() mogą zakończyć przetwarzanie wcześniej, nie odwiedzając całego źródła; dzięki temu da się bezpiecznie operować na nieskończonych strumieniach (Stream.iterate, Stream.generate).
Stream.iterate + limit: bezpieczne cięcie nieskończonego strumienia java
List<Integer> firstFive = Stream.iterate(1, n -> n + 1)
    .limit(5)
    .toList(); // [1, 2, 3, 4, 5], JDK 16+
toList() (Java 16+)
Stream.toList() zwraca niemodyfikowalną listę podobną w duchu do collect(Collectors.toUnmodifiableList()), ale nie jest z nią tożsamy: toList() dopuszcza elementy null, natomiast toUnmodifiableList() rzuca NullPointerException dla null; JDK nie gwarantuje konkretnego typu implementacji ani identyczności zachowania.
Mnemonik
Operacje pośrednie tylko planują; operacja terminalna naciska 'start'.

8 · Wątki wirtualne (Project Loom) vs platformowe

★ egzamin

Wątki wirtualne (Java 21, JEP 444) to lekkie wątki zarządzane przez JVM, mapowane wiele-do-kilku na wątki platformowe (wątki nośne, carrier threads). Celem jest tania współbieżność dla kodu blokującego na I/O, bez pisania kodu asynchronicznego.

Wątek platformowy

  • Odwzorowanie 1:1 na wątek systemu operacyjnego
  • Kosztowny: stos rzędu megabajtów, tworzenie i przełączanie kontekstu drogie
  • Praktyczny limit: tysiące wątków na proces
  • Dobry do obliczeń intensywnie korzystających z CPU

Wątek wirtualny

  • Zarządzany przez JVM, tymczasowo montowany na wątku nośnym
  • Tani: lekki stos rosnący na stercie, tworzenie niemal darmowe
  • Praktyczny limit: setki tysięcy lub miliony instancji
  • Dobry do kodu blokującego na I/O (sieć, baza danych, pliki)
Executors.newVirtualThreadPerTaskExecutor(): jeden wątek wirtualny na zadanie java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return null;
        });
    }
} // executor.close() czeka na zakończenie wszystkich zadań
Odmontowanie na blokadzie
Gdy wątek wirtualny blokuje się na operacji I/O, JVM odczepia go od wątku nośnego, zwalniając ten wątek nośny do obsługi innych wątków wirtualnych. Po zakończeniu operacji wątek wirtualny wraca na dowolny wolny wątek nośny.
Pułapka
Pinning: wątek wirtualny wykonujący blokującą operację wewnątrz bloku synchronized (albo natywnej metody JNI) NIE odczepia się od wątku nośnego, tylko go blokuje (przypina). Przy dużej liczbie wątków wirtualnych to zjada pulę wątków nośnych i niweczy korzyść skalowania.
ReentrantLock zamiast synchronized w kodzie z blokującym I/O na wątkach wirtualnych java
private final ReentrantLock lock = new ReentrantLock();

void transfer() {
    lock.lock();
    try {
        // blokująca operacja I/O nie przypina wątku nośnego
    } finally {
        lock.unlock();
    }
}
Diagnostyka pinningu
Flaga -Djdk.tracePinnedThreads=full (albo short) loguje miejsca w kodzie, gdzie wątek wirtualny zostaje przypięty do wątku nośnego podczas blokowania.
Pułapka
Wątki wirtualne nie przyspieszają obliczeń intensywnie korzystających z CPU (nie ma ich więcej niż rdzeni procesora, które faktycznie mogą coś liczyć); do równoległych obliczeń nadal właściwy jest ForkJoinPool albo równoległe strumienie, nie wątki wirtualne.
  • Twórz jeden wątek wirtualny na zadanie (task), nie pul ich jak platformowych: są tanie, pooling tylko dodaje złożoność bez korzyści
  • Unikaj synchronized w ścieżkach z blokującym I/O na wątkach wirtualnych, użyj java.util.concurrent.locks
  • Nie trzymaj dużych ThreadLocal na wątkach wirtualnych masowo: koszt pamięci mnoży się przez liczbę instancji
Mnemonik
Platformowy wątek to drogi pracownik na etacie; wirtualny to tani podwykonawca na zlecenie, którego JVM zatrudnia i zwalnia w mikrosekundach.
Fiszki - aktywne przypominanie

Sprawdź się - testowanie to nauka

24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.

Rozpocznij quiz
Zbudowane przez Tenzan Logic