Typy generyczne w Javie: var, Optional, sealed

Zaktualizowano 22 października 2026

Typy generyczne (generyki) w Javie znasz być może z ArrayList<String> (zobacz listy i mapy w Javie): lista wie, że trafiają do niej tylko teksty. W tym artykule sam napiszesz takie generyczne klasy i metody. Do tego dochodzą narzędzia z nowszych wersji Javy, które sprawiają, że kod jest krótszy i bezpieczniejszy: var, dopasowywanie wzorców (pattern matching), Optional i sealed.

var i instanceof

Dzięki var pomijasz typ zmiennej lokalnej. Java wyprowadza go z wartości po prawej stronie =: var names = new ArrayList<String>(); ma typ ArrayList<String>. Typ jest potem tak samo ustalony jak zwykle, tylko nie jest zapisany. Przy var typ w nawiasach ostrych piszesz po prawej stronie. var działa też w pętli for-each, ale nie dla pól i parametrów.

Zmienna typu Object może zawierać wszystko. instanceof sprawdza, co jest w środku, i od razu daje ci pasującą zmienną: w if (value instanceof String s) zmienna s jest w bloku if typu String, bez rzutowania.

import java.util.ArrayList;import java.util.List;
public class Main {    public static String describe(Object value) {        if (value instanceof String s) {            return "Text with " + s.length() + " characters";        } else if (value instanceof Integer i) {            return "Integer, doubled " + (i * 2);        }        return "Something else";    }
    public static void main(String[] args) {        var names = new ArrayList<String>();        names.add("Ada");        names.add("Linus");        for (var name : names) {            System.out.println(name.toUpperCase());        }        var count = names.size();        System.out.println("Count: " + count);
        List<Object> things = List.of("hello", 21, 3.5);        for (var thing : things) {            System.out.println(describe(thing));        }    }}

Klasy i metody generyczne

class Box<T> ma parametr typu T. Wewnątrz klasy używasz T jak zwykłego typu. Dopiero przy tworzeniu obiektu ustala się, czym jest T: w Box<String> to String, w Box<Integer> to Integer. Dzięki temu piszesz pudełko raz, a get() i tak zwraca właściwy typ. Rekordy też mogą mieć parametry typu, nawet kilka: record Pair<A, B>(A first, B second).

Także pojedyncza metoda może mieć parametr typu. Stoi on przed typem zwracanym:

public static <T extends Comparable<T>> T smallest(List<T> list)

T extends Comparable<T> oznacza: T musi dać się porównywać. Tylko wtedy możesz w metodzie wywołać compareTo. Metoda działa z liczbami, tekstami i każdą własną klasą, która implementuje Comparable.

import java.util.List;
class Box<T> {    private T content;
    public void put(T content) {        this.content = content;    }
    public T get() {        return content;    }}
record Pair<A, B>(A first, B second) {}
public class Main {    public static <T extends Comparable<T>> T smallest(List<T> list) {        T best = list.get(0);        for (T element : list) {            if (element.compareTo(best) < 0) {                best = element;            }        }        return best;    }
    public static void main(String[] args) {        Box<String> word = new Box<>();        word.put("Java");        String w = word.get();        System.out.println(w.length());
        Box<Integer> number = new Box<>();        number.put(7);        int n = number.get();        System.out.println(n + 1);
        Pair<String, Integer> entry = new Pair<>("Ada", 36);        System.out.println(entry);
        System.out.println(smallest(List.of(5, 2, 9)));        System.out.println(smallest(List.of("pear", "apple", "fig")));    }}

Optional

Co ma zwrócić wyszukiwanie, które niczego nie znalazło? O sprawdzeniu null łatwo zapomnieć. Optional<T> pokazuje już w typie, że wartości może nie być. Zawiera albo wartość (Optional.of(x)), albo jest pusty (Optional.empty()).

  • isPresent() sprawdza, czy wartość istnieje
  • orElse(fallback) zwraca wartość albo wartość zastępczą
  • map(...) przekształca zawartość, jeśli jakaś jest; pusty zostaje pusty (podobnie jak w strumieniach w Javie)
import java.util.List;import java.util.Optional;
public class Main {    public static Optional<String> firstStartingWith(List<String> words, char letter) {        for (String w : words) {            if (w.charAt(0) == letter) {                return Optional.of(w);            }        }        return Optional.empty();    }
    public static void main(String[] args) {        List<String> fruits = List.of("apple", "banana", "cherry");        Optional<String> b = firstStartingWith(fruits, 'b');        Optional<String> z = firstStartingWith(fruits, 'z');
        System.out.println(b.isPresent());        System.out.println(b.orElse("none"));        System.out.println(z.orElse("none"));        System.out.println(b.map(String::length).orElse(0));        System.out.println(z.map(String::length).orElse(0));    }}

sealed i switch ze wzorcami

sealed interface Payment permits Cash, Card, Voucher ustala: istnieją dokładnie te trzy rodzaje Payment i żadne inne. Wtedy switch może decydować bezpośrednio według rodzaju:

  • case Voucher v -> sprawdza rodzaj i daje ci obiekt jako v
  • case Cash(double amount) -> od razu rozpakowuje rekord (wzorzec rekordu, record pattern)
  • when dodaje warunek: case Cash(double amount) when amount >= 100 ->

Ponieważ kompilator zna wszystkie dozwolone rodzaje, switch nie potrzebuje default. Jeśli brakuje jakiegoś przypadku, kompilator to zgłosi. Tak jak przy else if, wygrywa pierwszy pasujący przypadek. Dlatego bardziej szczegółowy przypadek z when musi stać pierwszy.

sealed interface Payment permits Cash, Card, Voucher {}
record Cash(double amount) implements Payment {}
record Card(double amount, String bank) implements Payment {}
record Voucher(String code) implements Payment {}
public class Main {    public static String describe(Payment p) {        return switch (p) {            case Cash(double amount) when amount >= 100 -> "Large cash payment: " + amount;            case Cash(double amount) -> "Cash: " + amount;            case Card(double amount, String bank) -> "Card from " + bank + ": " + amount;            case Voucher v -> "Voucher " + v.code();        };    }
    public static void main(String[] args) {        System.out.println(describe(new Cash(20)));        System.out.println(describe(new Cash(150)));        System.out.println(describe(new Card(35.5, "Sparkasse")));        System.out.println(describe(new Voucher("SUMMER10")));    }}

Typowe błędy

  • var bez wartości

    Kompilator odrzuca var count; albo var x = null; z komunikatem cannot infer type, bo po prawej stronie nie ma niczego, z czego mógłby odczytać typ.

  • Box<int>

    Również we własnych klasach generycznych dozwolone są tylko klasy: Box<Integer>, a nie Box<int>. Tak samo jest w przypadku List<Integer>.

  • compareTo bez ograniczenia

    Jeśli przed metodą stoi tylko <T>, Java nic nie wie o T. element.compareTo(best) zgłasza wtedy cannot find symbol. Dopiero <T extends Comparable<T>> pozwala na to wywołanie.

  • Ogólny przypadek przed szczegółowym

    Jeśli case Cash c -> stoi przed case Cash(double amount) when amount >= 100 ->, drugi przypadek nigdy nie zostanie osiągnięty. Kompilator zgłasza this case label is dominated by a preceding case label.

Zadania z tego tematu

Przećwicz to, czego się nauczyłeś, od razu w przeglądarce. Testy natychmiast sprawdzają twoje rozwiązanie.

Powiązane artykuły