KözéphaladóOlvasási idő: ~17 perc

Alapok

Generikus osztályok, generikus metódusok és korlátozott típusok

Generikus osztályok, generikus metódusok, korlátozott típusparaméterek és az újrahasznosítható, típusbiztos API-k mögötti tervezési szemlélet.

1. Definíció

Mi ez?

A Java generics lehetővé teszi, hogy algoritmusokat és adatstruktúrákat konkrét osztályok helyett típusparaméterekkel írj le.

Ahelyett, hogy külön konténert írnál String-hez, Integer-hez és User-hez, egyetlen generikus API-t készítesz, és a hívó köti be a konkrét típust.

A compiler ezután ellenőrzi, hogy a kiválasztott típus mindenhol következetesen legyen használva.

Ezért tud egy List<String> fordítási időben elutasítani egy Integer-t anélkül, hogy neked külön futásidejű ellenőrzést kellene írnod.

Miért létezik?

A generics előtti Java kollekciók Object referenciákat tároltak.

Ez rugalmas volt, de nem biztonságos.

A fejlesztőknek kézzel kellett visszacastolniuk az értékeket.

Ezek a castok zajosak voltak, könnyű volt elfelejteni őket, és a hibák gyakran csak futásidőben derültek ki.

A generics három visszatérő problémára adott választ:

  • csökkentse a nem biztonságos castolást
  • a hibákat futásidőről fordítási időre tolja át
  • újrahasznosítható könyvtárakat tegyen lehetővé olvashatóságvesztés nélkül

Hova illeszkedik?

A generics a nyelvtervezés, az API-tervezés és a mindennapi kódolási stílus metszetében helyezkedik el.

Alapvető a következőkhöz:

  • kollekciók, például List<T>, Map<K, V>, Optional<T>
  • utility API-k, például Collections.sort, Comparator<T>, Stream<T>
  • framework absztrakciók, például repository-k, converterek, serializer-ek és event handler-ek

Interjún a generics nem csak szintaxis.

Annak a jele, hogy érted a típusbiztonságot, a library design-t és azt, hogyan egyensúlyoz a Java a visszafelé kompatibilitás és a fejlesztői kényelem között.

2. Alapfogalmak

2.1 Generikus osztályok

2.1.1 Kulcsszavak és szerződések, amelyeket itt ki kell mondani

Ez a téma nem csak szintaxis. Az alábbi szavak írják le a Java generics alap-szerződéseit, ezért pontosan kell használni őket:

  • type parameter — az API készítője által deklarált helykitöltő típus, például a T a Box<T> alakban.
  • type argument — a hívó által megadott konkrét típus, például a String a Box<String> alakban.
  • generic class — olyan osztály, amelynek szerződése egy vagy több típusparamétertől függ.
  • generic method — olyan metódus, amely saját típusparamétert deklarál az osztálytól függetlenül.
  • bound — a megengedett típusokra tett korlát, például <T extends Number>.
  • multiple bounds — több követelmény kombinálása, például <T extends Number & Comparable<T>>.
  • raw type — generikus típus legacy, típusargumentum nélküli használata, ami gyengíti a típusbiztonságot.
  • unchecked warning — jelzés arról, hogy a compiler már nem tud teljes típushelyességet bizonyítani.
  • diamond operator — a <> rövidítés, amellyel a compiler képes kikövetkeztetni a konstruktor típusargumentumait.

Interjún erősebb azt mondani, hogy „ez az API egy felső korlátos típusparamétert vezet be”, mint azt, hogy „itt generics van”.

Egy generikus osztály osztályszinten vezet be egy vagy több típusparamétert.

public final class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }

    public void set(T value) {
        this.value = value;
    }
}

A T egy helykitöltő.

A hívó választja ki a konkrét típust az osztály használatakor.

Box<String> messageBox = new Box<>("hello");
Box<Integer> answerBox = new Box<>(42);

A fontos tervezési gondolat az, hogy az osztály logikája ugyanaz marad, miközben a compiler minden felhasználási helyen követi a típushelyességet.

2.2 Generikus metódusok

Egy generikus metódus a saját típusparaméterét közvetlenül a metóduson deklarálja.

Nem szükséges hozzá, hogy a környező osztály is generikus legyen.

public final class MoreCollections {
    public static <T> T first(List<T> items) {
        if (items.isEmpty()) {
            throw new IllegalArgumentException("items must not be empty");
        }
        return items.get(0);
    }
}

Az <T> itt a metódushoz tartozik, nem az osztályhoz.

Ez interjún gyakran előkerülő különbség.

2.3 Korlátozott típusparaméterek

Néha a „bármilyen típus” túl megengedő.

Újrahasznosíthatóság kell, de csak egy bizonyos típuscsaládon belül.

Ilyenkor felső korlátot adsz meg.

public static <T extends Number> double sum(List<T> numbers) {
    double total = 0.0;
    for (T number : numbers) {
        total += number.doubleValue();
    }
    return total;
}

A korlát azt mondja ki:

  • a hívók használhatnak Integer, Long, Double és más Number leszármazottakat
  • a metódus belsejében biztonságosan hívhatók a Number metódusai T-n

Ez API-szerződés.

Egyszerre kommunikál képességet és korlátozást.

2.4 Többszörös korlátok

A Java egy típusparaméteren több korlátot is támogat.

public static <T extends Number & Comparable<T>> T max(T left, T right) {
    return left.compareTo(right) >= 0 ? left : right;
}

Az első korlát lehet osztály.

Az utána következőknek interface-eknek kell lenniük.

Ez akkor fontos, ha egy algoritmusnak ugyanattól a típustól több képességre is szüksége van.

2.5 Raw type-ok

A raw type egy generikus típus legacy, nem paraméterezett használata.

List raw = new ArrayList();
raw.add("hello");
raw.add(42);

A raw type-ok főleg a generics előtti Java-val való visszafelé kompatibilitás miatt léteznek.

Kikapcsolják a generics által nyújtott fordítási garanciák nagy részét.

Csak akkor használd őket, ha tényleg legacy API-val kell összedolgoznod.

Modern kódban a raw type inkább szag.

2.6 Típusinferencia

A compiler gyakran a kontextusból is ki tudja következtetni a generikus típusokat.

Map<String, Integer> scores = new HashMap<>();
var names = List.of("Ada", "Linus", "Grace");

Ez csökkenti a boilerplate-et.

De az inferencia csak kényelmi funkció.

Nem szabad, hogy rontsa az olvashatóságot.

Ha a kikövetkeztetett típus nem egyértelmű, az explicit generikus paraméter sokszor jobb döntés.

2.7 Gyakori típusparaméter-nevek

Paraméter Tipikus jelentés
T tetszőleges típus
E elem egy kollekcióban
K kulcs
V érték
R visszatérési típus
N numerikus jellegű típus domain-specifikus API-kban

Ezek konvenciók, nem törvények.

A jó elnevezés javítja a megérthetőséget, különösen bonyolultabb generikus szignatúráknál.

2.8 Mit garantál és mit nem a generics?

A generics a paraméterezett használatokra fordítási idejű típusellenőrzést garantál.

A generics nem őrzi meg csodával határos módon az összes futásidejű típusinformációt.

Ez a határ később a type erasure témában válik igazán fontossá.

Az alapoknál ezt jegyezd meg:

  • biztonságosabb API-k
  • kevesebb explicit cast
  • erősebb típus-szintű szándékkifejezés

3. Gyakorlati használat

Konténerek és wrapper-ek tervezése

A generikus osztályok ideálisak akkor, ha a viselkedés sok típuson ugyanaz.

Példák:

  • Result<T> service eredményekhez
  • Page<T> lapozáshoz
  • Pair<L, R> kisebb utility absztrakciókhoz
  • CacheEntry<K, V> infrastruktúra kódhoz

Az ökölszabály egyszerű.

Ha a kód csak azért változna, mert a tárolt típus változik, akkor jó eséllyel generics kell.

Utility metódusok tervezése

Generikus metódus jobb választás, ha maga az osztály nem kell, hogy generikus legyen.

Példák:

  • első elem kiválasztása
  • két érték felcserélése egy listában
  • lista map-pé alakítása mapper függvények segítségével

Így a generikus hatókör a lehető legkisebb marad.

A kisebb scope általában egyszerűbb API-t jelent.

Service-layer absztrakciók építése

Alkalmazáskódban a generics sokszor inkább újrahasznosítható infrastruktúrában jelenik meg, nem a business logikában.

Példák:

  • Repository<T, ID>
  • Mapper<S, T>
  • Validator<T>
  • EventHandler<T>

Ez azért hasznos, mert a framework logika újrahasznosítható marad, miközben a domain típus változik.

Generikus absztrakció vagy konkrét API?

Ne tegyél mindent automatikusan generikussá.

Ezt egy jó interjúválasz kifejezetten megemlíti.

Konkrét típust válassz, ha:

  • az API erősen domain-specifikus, és valójában csak egy típusnak van értelme
  • a generikus absztrakció elrejtené a hasznos üzleti jelentést
  • a generikus szignatúra nehezebben olvasható lenne, mint a konkrét verzió

Generikus absztrakciót válassz, ha:

  • ugyanaz a logika több típuson ismétlődik
  • a viselkedés képességektől függ, nem egyetlen konkrét osztálytól
  • a generikus paraméter erősebb szándékot kommunikál a hívó felé

Korlátozott generics a valóságban

A bounded type parameter akkor gyakori, amikor az algoritmusnak közös képességre van szüksége.

Példák:

  • az értékek összehasonlíthatók legyenek
  • az értékek numerikusak legyenek
  • az értékek implementáljanak valamilyen framework interface-t

A korlát mindig valós követelményt fejezzen ki.

Ha csak azért írsz extends-et, mert “így haladónak tűnik”, a design valószínűleg rossz irányba megy.

API-tervezési ellenőrzőlista

Generikus API tervezésekor kérdezd meg:

  1. Mi változik: a tárolt típus, a viselkedés vagy mindkettő?
  2. Valódi típusbiztonságot nyer a hívó a paraméterezésből?
  3. Nem lenne érthetőbb egy konkrét típus?
  4. A korlátok valódi képességigényt fejeznek ki?
  5. A szignatúra később is könnyen olvasható marad?

Interjús megfogalmazás

Ha azt kérdezik, “mikor használnál generics-et?”, egy erős válasz így hangzik:

Akkor használok generics-et, amikor a viselkedés stabil, de a részt vevő típus változik, és azt szeretném, hogy a compiler kényszerítse ki a szerződést Object + castolás helyett.

Ez erősebb válasz annál, mint hogy “újrahasznosíthatóság miatt”.

4. Kód példák

1. példa — Generikus wrapper típus

public final class Result<T> {
    private final T value;
    private final String error;

    private Result(T value, String error) {
        this.value = value;
        this.error = error;
    }

    public static <T> Result<T> success(T value) {
        return new Result<>(value, null);
    }

    public static <T> Result<T> failure(String error) {
        return new Result<>(null, error);
    }

    public boolean isSuccess() {
        return error == null;
    }

    public T getValue() {
        return value;
    }
}

Ez a minta sok kódbázisban előfordul.

A generikus paraméter a sikeres eredmény payload típusát fejezi ki.

2. példa — Generikus metódus

public final class Lists {
    public static <T> T last(List<T> items) {
        if (items.isEmpty()) {
            throw new IllegalArgumentException("items must not be empty");
        }
        return items.get(items.size() - 1);
    }
}

Ez a klasszikus eset, amikor maga az osztály nem kell, hogy generikus legyen.

3. példa — Felső korlát

public final class MathUtil {
    public static <T extends Number> double average(List<T> numbers) {
        if (numbers.isEmpty()) {
            throw new IllegalArgumentException("numbers must not be empty");
        }

        double total = 0;
        for (T number : numbers) {
            total += number.doubleValue();
        }
        return total / numbers.size();
    }
}

A korlát dokumentálja, miért működik az algoritmus.

Szüksége van a Number API-ra.

4. példa — Többszörös korlát

public final class Comparisons {
    public static <T extends Number & Comparable<T>> T larger(T left, T right) {
        return left.compareTo(right) >= 0 ? left : right;
    }
}

Ez akkor hasznos, amikor ugyanannak a típusnak több szerződést is teljesítenie kell.

5. példa — Raw type csapda

List raw = new ArrayList();
raw.add("text");
raw.add(123);

List<String> names = raw;
String first = names.get(1); // runtime ClassCastException

A compiler figyelmeztetni fog.

A figyelmeztetés fontos.

Az unchecked műveletek sokszor késleltetett production bugokra utalnak.

5. Trade-offok

Szempont Előny Költség / kockázat
Típusbiztonság A hibák fordításkor kiderülnek Raw type-okkal és unchecked castokkal megkerülhető
Újrahasznosítás Egy API sok típuson használható Túlabsztrahálva rontja az olvashatóságot
Dokumentáció A típusparaméterek explicitté teszik a szándékot A bonyolult szignatúrák elriaszthatják a kevésbé tapasztalt fejlesztőket
Karbantartás Kevesebb duplikált implementáció A generikus hibaüzenetek eleinte nehezebben értelmezhetők
API-stabilitás Erősebb szerződés a hívók felé A típusparaméter későbbi módosítása törő változás lehet

A kulcsgondolat az, hogy a generics nem ingyenes absztrakció.

Értékes absztrakció.

Ez a különbség fontos.

Ha egy generikus szignatúra túl okoskodóvá válik, többé nem segít.

6. Gyakori hibák

1. Raw type használata modern kódban

A raw type a generics fő előnyét veszi el.

Általában legacy interopot vagy figyelmetlen kódot jelez.

2. Az egész osztály generikussá tétele, amikor csak egy metódusnak kellene

Ez feleslegesen terjeszti szét a komplexitást.

Tartsd a generikus hatókört a lehető legkisebben.

3. Korlátok hozzáadása valós képességigény nélkül

<T extends Serializable> csak akkor indokolt, ha a szerializálhatóság tényleg követelmény.

Különben csak zaj.

4. A `List` összekeverése a “bármi listájával”

A List<Object> nem ugyanaz, mint a List<String> vagy a List<Integer>.

A Java generics invariáns.

Ez a félreértés később wildcard visszaélésekhez vezet.

5. Compiler warningok figyelmen kívül hagyása

Az unchecked warningok gyakran arra mutatnak rá, hogy a típusrendszer már nem tud megvédeni.

Ez nem ártalmatlan.

6. Olvashatatlan szignatúrák tervezése

Egy <T, U, V, X, Y> jellegű API kontextus nélkül lehet technikailag helyes, mégis rossz design.

Senior szinten a karbantarthatóság fontosabb, mint az okoskodás.

7. Mélymerülés

Miért változtatta meg a generics a Java library design-t?

Amikor megjelent a generics, a standard library sokkal több szándékot tudott kifejezni.

A List<E> világosabb, mint egy Object-eket tároló gyűjtemény.

A Comparator<T> pontosan megmondja, milyen típust hasonlít.

Az Optional<T> egy konkrét típusú, esetleg hiányzó értéket kommunikál.

Ezért érződnek a modern Java API-k biztonságosabbnak a Java 1.4-es korszakhoz képest.

Fordítási szerződés mint architekturális eszköz

A generics-et gyakran szintaktikai témának tekintik.

Valójában architekturális eszköz is.

Amikor Mapper<S, T>-t vagy Repository<T, ID>-t tervezel, újrahasznosítható szerződést írsz le, amelyet sok domain modul biztonságosan implementálhat.

Ez egyszerre csökkenti a copy-paste-et és dokumentálja a szándékot.

Kapcsolat a wildcardokkal

Az alapok erre a kérdésre válaszolnak:

„Hogyan paraméterezek egy típust vagy metódust?”

A wildcardok a következő kérdésre:

„Hogyan viselkednek egymáshoz képest a rokon paraméterezett típusok?”

Ezért kell előbb az alapokat érteni.

Wildcardokról csak akkor lehet jól beszélni, ha az invariancia és a paraméterezés már tiszta.

Kapcsolat a type erasure-rel

Kezdőként a generics könnyen futásidejű feature-nek tűnik.

Pedig az erejének nagy része fordítási idejű.

Ezért vannak bizonyos tiltások és korlátok:

  • nem írhatsz egyszerűen new T()-t
  • nem hozhatsz létre new List<String>[10] tömböt
  • nem kérdezheted meg megbízhatóan, hogy if (value instanceof List<String>)

Ezek nem véletlenszerű nyelvi furcsaságok.

A type erasure és a visszafelé kompatibilitás következményei.

Senior interjús nézőpont

A mélyebb jelzés nem az, hogy “ismerem a <T> szintaxisát”.

Hanem az, hogy:

  • tudom, mikor javít a generics az API-n
  • tudom, mikor válik zajjá
  • tudom, hogyan hat a compiler garanciája a design minőségére

Ez választja el a mechanikus tudást a mérnöki ítélőképességtől.

8. Interjúkérdések

Milyen problémát oldott meg a generics Java-ban?

A generics sok típushibát futásidőről fordítási időre tolt át, és csökkentette a kézi castolás igényét, különösen kollekcióknál és újrahasznosítható API-knál.

Mi a különbség a generikus osztály és a generikus metódus között?

A generikus osztály a típusparamétert az objektumhoz vagy a típusdeklarációhoz köti.

A generikus metódus olyan típusparamétert vezet be, amely csak az adott metódushívásra él.

Mikor adnál felső korlátot, például ``?

Amikor az algoritmusnak olyan képességre van szüksége, amelyet a Number garantál, például a doubleValue() használatára, és fordításkor szeretnéd kizárni a nem kapcsolódó típusokat.

Miért nem ajánlottak a raw type-ok?

Mert megkerülik a generikus típusellenőrzést, és visszahozzák az unchecked castokat meg a futásidejű ClassCastException kockázatát.

Mi a generics legnagyobb tervezési kockázata?

A legnagyobb kockázat a túltervezés.

Egy API lehet technikailag rugalmas, miközben kognitívan drága.

Az olvasható szerződések fontosabbak, mint a típusrendszer fitogtatása.

9. Szószedet

Fogalom Jelentés
típusparaméter Helykitöltő, például T, E, K vagy V
generikus osztály Típusparaméterekkel deklarált osztály, például Box<T>
generikus metódus Saját típusparaméterrel rendelkező metódus, például <T> T first(...)
felső korlát Olyan megkötés, mint a <T extends Number>
raw type Generikus típus legacy, paraméterek nélküli használata
unchecked warning Olyan compiler warning, ahol a típusbiztonság nem igazolható teljesen
típusinferencia A compiler kontextusból kikövetkezteti a generikus argumentumokat
invariáns A List<String> nem lesz a List<Object> leszármazottja

10. Cheatsheet

  • A generics fordítási idejű típusbiztonságot ad és csökkenti a castolást.
  • Generikus osztályt akkor használj, ha maga az absztrakció paraméterezett.
  • Generikus metódust akkor használj, ha csak egy műveletnek kell típusparaméter.
  • Korlátot csak akkor adj meg, ha az algoritmus tényleg igényel valamilyen képességet.
  • Kerüld a raw type-okat, kivéve ha legacy interoppal dolgozol.
  • A típusinferencia kényelmes, de a tisztaság fontosabb.
  • A List<Object> nem jelenti azt, hogy “bármilyen lista”.
  • Az unchecked warning valós tervezési visszajelzés.
  • A jó generikus API újrahasznosítható és olvasható.
  • Interjún ne csak az előnyt, hanem a trade-offot is magyarázd el.

🎮 Játékok

10 kérdés

© 2026 Mester Ádám