Java
Głębokie, aktualne semantyczne podstawy Javy 21 LTS: wyjątki, kontrakt equals/hashCode, rekordy, sealed classes, generyki, Stream API i wątki wirtualne.
- 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
★ egzaminThrowable 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.
// 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;
} - 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.
static void readFirstLine(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
}
} 2 · equals(), hashCode() i pułapki autoboxingu
★ egzaminObject.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
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);
}
} 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 3 · String: niezmienność i string pool
★ egzaminString 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
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 StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i).append(',');
}
String result = sb.toString(); 4 · Rekordy (records): nowoczesny nośnik danych
★ egzaminRekord (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.
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] 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
}
} - 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
5 · Hierarchie typów: interfejsy, klasy abstrakcyjne, sealed classes i pattern matching
★ egzaminInterfejsy 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
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);
}
} 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 {} 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;
};
} 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";
};
} - 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)
6 · Generyki, type erasure i var
★ egzaminGeneryki 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.
static <T extends Comparable<T>> T max(List<T> list) {
return list.stream()
.max(Comparator.naturalOrder())
.orElseThrow(NoSuchElementException::new);
} static void copy(List<? extends Number> source, List<? super Number> destination) {
for (Number n : source) {
destination.add(n);
}
} 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
} 7 · Stream API: leniwość, operacje terminalne, kolektory
★ egzaminStream 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.
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 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
List<Integer> firstFive = Stream.iterate(1, n -> n + 1)
.limit(5)
.toList(); // [1, 2, 3, 4, 5], JDK 16+ 8 · Wątki wirtualne (Project Loom) vs platformowe
★ egzaminWą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)
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ń 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();
}
} - 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
Sprawdź się - testowanie to nauka
24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.