Implementációk
ArrayList, LinkedList, HashMap, TreeMap, HashSet és LinkedHashMap
Mélymerülés az ArrayList, LinkedList, HashMap, TreeMap, HashSet és LinkedHashMap implementációkba — mikor melyiket használd, teljesítményjellemzők és kritikus trade-offok.
1. Definíció
Mi ez?
A JCF konkrét implementációkat biztosít az interfészeihez. Mindegyik meghatározott kompromisszumokat köt memória, hozzáférési sebesség, rendezés és szálbiztonság között. A megfelelő implementáció kiválasztása az egyik legjelentősebb teljesítménybeli döntés Java-ban.
Miért fontos?
Ha LinkedList-et használsz ott, ahol ArrayList elegendő lenne, 10×+ lassulást okozhatsz véletlenszerű hozzáférésnél. Ha HashMap helyett TreeMap-et választasz, minden műveletnél O(log n) pluszköltséget viselsz, de elveszted a rendezettséget. Ezek a döntések nagy léptékben összeadódnak.
Hol helyezkednek el?
List → ArrayList, LinkedList
Set → HashSet, LinkedHashSet, TreeSet
Map → HashMap, LinkedHashMap, TreeMap
2. Alapfogalmak
2.1 `ArrayList`
Átméretezhető tömb (Object[]) által tárolva. Alapértelmezett kezdeti kapacitás: 10.
| Művelet | Komplexitás | Megjegyzés |
|---|---|---|
get(index) |
O(1) | Közvetlen tömbhozzáférés |
add(element) végre |
O(1) amortizált | Átméretezés = új tömb ×1.5 |
add(index, element) |
O(n) | Elemek jobbra tolása |
remove(index) |
O(n) | Elemek balra tolása |
contains(object) |
O(n) | Lineáris keresés |
2.2 `LinkedList`
Kétszeresen láncolt lista — minden csomópont prev/next referenciákat tartalmaz. Megvalósítja a List és a Deque interfészt is.
| Művelet | Komplexitás | Megjegyzés |
|---|---|---|
get(index) |
O(n) | Bejárás fejtől/faroktól |
addFirst() / addLast() |
O(1) | Mutatófrissítés |
removeFirst() / removeLast() |
O(1) | Mutatófrissítés |
| Memória elemenként | ~48 bájt | Node header + adat + 2 referencia |
2.3 `HashMap`
Hash tábla vödörként funkcionáló tömbbel. Alapértelmezett kapacitás: 16, load factor: 0.75.
| Művelet | Komplexitás | Megjegyzés |
|---|---|---|
get(key) |
O(1) átl. | O(log n) legrosszabb (Java 8+ fává alakított vödör) |
put(key, value) |
O(1) átl. | Kiválthat átméretezést |
containsKey(key) |
O(1) átl. |
Java 8+: ≥8 bejegyzést tartalmazó vödrök linked list-ről Red-Black Tree-re váltanak (O(log n) legrosszabb eset).
2.4 `LinkedHashMap`
A HashMap-et bővíti egy kétszeresen láncolt listával az összes bejegyzésen keresztül:
- Beszúrási sorrend (alapértelmezett)
- Hozzáférési sorrend (
new LinkedHashMap<>(cap, 0.75f, true)) → LRU cache
Ugyanolyan O(1) mint a HashMap, kis konstans overhead-del.
2.5 `TreeMap`
Red-Black Tree — önkiegyensúlyozó BST. Minden művelet O(log n). Kulcsok természetes sorrendben vagy megadott Comparator szerint rendezve. null kulcsok nem engedélyezettek.
| Művelet | Komplexitás |
|---|---|
get / put / remove |
O(log n) |
firstKey() / lastKey() |
O(log n) |
headMap() / subMap() |
O(log n) |
2.6 `HashSet`
HashMap<E, PRESENT> által tárolva (PRESENT = statikus dummy Object). Minden művelet a belső map-re delegál — azonos teljesítmény.
2.7 `ArrayDeque`
Az ArrayDeque általában a legjobb általános célú választás queue és stack jellegű használatra.
| Művelet | Komplexitás | Megjegyzés |
|---|---|---|
addFirst / addLast |
O(1) amortizált | Körkörös tömb van alatta |
removeFirst / removeLast |
O(1) | Nagyon hatékony a két végén |
push / pop |
O(1) | Modern stack-helyettesítő a Stack helyett |
Sokszor veri a LinkedList-et, mert tömörebben tárol és jobban kihasználja a cache lokalitást.
2.8 Terhelés alapján válassz, ne megszokásból
Az implementációválasztást az uralkodó műveletekhez kell igazítani:
- sok random olvasás →
ArrayList - sor / verem jellegű kétvéges műveletek →
ArrayDeque - egyszerű kulcs alapú lookup →
HashMap - stabil iterálási sorrend →
LinkedHashMap - rendezett navigáció →
TreeMap
2.9 A specializált implementáció sokszor még jobb választás
Az igazán jó választás néha nem a “szokásos” implementációk egyike. Az EnumMap és az EnumSet nagyon kompakt és gyors, ha a kulcsok vagy értékek enumok. Az IdentityHashMap referenciaazonosság alapján hasonlít, nem equals() szerint, ezért csak speciális identitás-alapú esetekben helyes. A ConcurrentHashMap pedig megosztott, módosítható map-eknél jobb alapértelmezés konkurens kódban, mert sokkal jobban skálázódik, mint egy durva zárolással körbetekert HashMap.
Ez interjún is jó jelzés: először a megfelelő absztrakciót válaszd ki, utána a helyes standard implementációt, végül nézd meg, hogy egy specializált implementáció még jobban illeszkedik-e a domainhez.
3. Gyakorlati használat
ArrayList vs LinkedList
| Forgatókönyv | Választás | Miért |
|---|---|---|
| Olvasás-intenzív, véletlenszerű hozzáférés | ArrayList |
O(1) get |
| Közepéről gyakori hozzáadás/törlés | ArrayList |
Cache-barát a O(n) tolás ellenére is |
| Queue/stack felhasználás | ArrayDeque |
Jobb mint LinkedList mindkét végre |
| List + Deque egyszerre szükséges | LinkedList |
Ritka; általában ArrayDeque jobb |
Ellenintuitív: Az
ArrayListtipikus méreteknél még közepén való beillesztésnél is veri aLinkedList-et a CPU cache lokalitás miatt.
HashMap vs LinkedHashMap vs TreeMap
| Igény | Választás |
|---|---|
| Leggyorsabb get/put, sorrend nem kell | HashMap |
| Beszúrási sorrend megőrzése | LinkedHashMap |
| LRU cache | LinkedHashMap(cap, 0.75f, true) + removeEldestEntry |
| Rendezett kulcsok, tartomány lekérdezések | TreeMap |
Gyakorlati választási ellenőrzőlista
Mielőtt konkrét implementációt választasz, kérdezd meg:
- A sorrend része-e a helyes működésnek?
- Kell-e tartomány alapú navigáció?
- Számít-e a memória overhead?
- Az olvasás, beszúrás vagy törlés a domináns művelet?
- Szekvenciális, kulcs-érték vagy kétvégű adatszerkezetről van szó?
4. Kód példák
Alap példa — ArrayList vs LinkedList
List<Integer> arrayList = new ArrayList<>(100_000);
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 100_000; i++) {
arrayList.add(i);
linkedList.add(i);
}
// O(1)
int a = arrayList.get(50_000);
// O(n) — ~50k csomópont bejárása
int b = linkedList.get(50_000);
// LinkedList csak a végeken ragyog
Deque<Integer> deque = (Deque<Integer>) linkedList;
deque.addFirst(999); // O(1)
Haladó példa — LRU Cache LinkedHashMap-mel
public class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LruCache(int maxSize) {
super(maxSize, 0.75f, true); // accessOrder = true
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
// Használat
LruCache<Integer, String> cache = new LruCache<>(3);
cache.put(1, "egy"); cache.put(2, "kettő"); cache.put(3, "három");
cache.get(1); // 1-es kulcs hozzáférés → "legújabb" végre kerül
cache.put(4, "négy"); // kizárja a 2-es kulcsot (legrégebben használt)
System.out.println(cache.keySet()); // [3, 1, 4]
Haladó példa — TreeMap tartomány lekérdezések
TreeMap<String, Integer> freq = new TreeMap<>();
List.of("banán", "alma", "cseresznye", "avokádó", "áfonya")
.forEach(w -> freq.merge(w, 1, Integer::sum));
System.out.println(freq.firstKey()); // áfonya
System.out.println(freq.subMap("b", "c").keySet()); // [banán]
System.out.println(freq.floorKey("barack")); // banán
Tipikus hiba — Módosítható HashMap kulcs
// ❌ Módosítható kulcs megtöri a HashMap-et
class ModosithatoKulcs {
int ertek;
public int hashCode() { return ertek; }
public boolean equals(Object o) { return o instanceof ModosithatoKulcs m && m.ertek == ertek; }
}
Map<ModosithatoKulcs, String> map = new HashMap<>();
ModosithatoKulcs kulcs = new ModosithatoKulcs();
kulcs.ertek = 42;
map.put(kulcs, "hello");
kulcs.ertek = 99; // hashCode megváltozott!
System.out.println(map.get(kulcs)); // null — bejegyzés elveszett a rossz vödörben
// ✅ Mindig immutábilis kulcsokat használj: String, Integer, record, UUID
Tipikus hiba — HashMap méretének előre meg nem adása
// ❌ 16-tól indul, sok rehash 10k bejegyzésnél
Map<String, Integer> map = new HashMap<>();
// ✅ Előre méretezés: kapacitás = várható_bejegyzések / 0.75 + 1
Map<String, Integer> map = new HashMap<>(13_334); // ~10k / 0.75
// Java 19+:
Map<String, Integer> map = HashMap.newHashMap(10_000);
5. Trade-offok
ArrayList vs LinkedList
| Szempont | ArrayList | LinkedList |
|---|---|---|
⚡ Véletlenszerű get(i) |
O(1) | O(n) |
| ⚡ Hozzáadás véghez | O(1) amortizált | O(1) |
| ⚡ Beillesztés közepére | O(n) tolás | O(n) bejárás |
| ⚡ Törlés az elejéről | O(n) tolás | O(1) |
| 💾 Memória/elem | ~4 bájt (referencia) | ~48 bájt (csomópont) |
| 🔧 Cache lokalitás | Kiváló | Gyenge |
| 🔄 Megvalósítja még | RandomAccess |
Deque |
HashMap vs LinkedHashMap vs TreeMap
| Szempont | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| ⚡ get/put | O(1) átl. | O(1) átl. | O(log n) |
| 💾 Memória | Alap | +2 ref/bejegyzés | +2 ref + szín bit |
| 🔧 Sorrend | ❌ Nincs | ✅ Beillesztési/hozzáférési | ✅ Rendezett |
| 🔧 Tartomány lekérdezés | ❌ | ❌ | ✅ |
| 🔧 null kulcsok | ✅ 1 engedélyezett | ✅ 1 engedélyezett | ❌ NPE |
6. Gyakori hibák
1. Map iterálása keySet + get-tel
// ❌ Kétszeres lookup bejegyzésenként
for (String key : map.keySet()) {
Integer value = map.get(key); // felesleges lookup
}
// ✅ Használj entrySet-et vagy forEach-t
for (Map.Entry<String, Integer> e : map.entrySet()) {
feldolgoz(e.getKey(), e.getValue());
}
map.forEach((k, v) -> feldolgoz(k, v)); // Java 8+
2. TreeMap használata HashMap helyett
// ❌ O(log n) egyszerű lookupnál
Map<String, Felhasznalo> users = new TreeMap<>();
// ✅ O(1) átl. — TreeMap-et csak rendezéskor használj
Map<String, Felhasznalo> users = new HashMap<>();
3. HashSet equals + hashCode nélkül
// ❌ Hiányzó equals/hashCode — duplikátumok a HashSet-ben
class Pont { int x, y; }
Set<Pont> set = new HashSet<>();
set.add(new Pont()); // x=1, y=2
set.add(new Pont()); // x=1, y=2 — de NEM duplikátum! Különböző objektumok
System.out.println(set.size()); // 2 — hiba!
// ✅ Adj hozzá @Override equals() és hashCode() metódusokat
record Pont(int x, int y) {} // record-ok automatikusan generálják ezeket
7. Mélymerülés
Miért veri az ArrayList a LinkedList-et beillesztésnél
A CPU cache sorok 64 bájtosak. Az ArrayList összefüggő tömbje lehetővé teszi a CPU prefetch-et — amikor az i indexhez férsz hozzá, a CPU előre betölti az i+1, i+2 stb. elemeket. A LinkedList csomópontjai szétszórva vannak a heap-en → minden csomópont-hozzáférés potenciális cache miss. Ez az ArrayList-et 3–10× gyorsabbá teszi a gyakorlatban tipikus méreteknél.
HashMap treeification (Java 8+)
Rosszul elosztott hashCode implementációk O(n) legrosszabb esetet okozhatnak HashMap-nél. A Java 8 bevezette a treeification mechanizmust: ha egy vödörben ≥8 bejegyzés van ÉS a tábla ≥64 vödörrel rendelkezik, a vödör Red-Black Tree-vé alakul → O(log n) legrosszabb eset, megelőzve a hash flooding DoS támadásokat.
EnumMap és EnumSet — a feledett drágakövek
Enum kulcsoknál mindig ezeket a specializált változatokat használd:
// Tömb-alapú, nincs hashing, O(1) ordinális index alapján
Map<DayOfWeek, List<Feladat>> menetrend = new EnumMap<>(DayOfWeek.class);
Set<DayOfWeek> munkanapok = EnumSet.range(MONDAY, FRIDAY);
Determinisztikus teszt kimenet
A HashMap iterálási sorrendje nem determinisztikus és JVM futtatások között változhat. Map tartalmát ellenőrző tesztekhez:
// ✅ Kiszámítható sorrend tesztekben
Map<String, Integer> rendezett = new TreeMap<>(hashMap);
assertEquals("{a=1, b=2}", rendezett.toString());
Miért váltsa le az `ArrayDeque` a `Stack`-et?
A Stack a Vector-ból örököl, ami legacy szinkronizációt és kényelmetlen örökséget hoz magával. Modern LIFO használatra szinte mindig az ArrayDeque a jobb alapértelmezés.
Az előre méretezés valódi optimalizáció
Nagy mennyiségű beszúrásnál az ArrayList és HashMap ismételt növekedési lépései felesleges allokációt, másolást és rehashingot okoznak. Reális kezdőkapacitással ez jelentősen csökkenthető.
8. Interjúkérdések
Miért jobb alapértelmezett választásnak általában az `ArrayList`, mint a `LinkedList`?
Mert a legtöbb üzleti terhelés jobban profitál a tömör memóriaképből, a gyors indexelt olvasásból és a cache lokalitásból, mint a láncolt lista elméleti előnyeiből.
Mikor jobb a `LinkedHashMap`, mint a `HashMap`?
Amikor a determinisztikus iterálási sorrend fontos, például stabil kimenetnél, teszteknél, felhasználói listáknál vagy LRU cache építésénél.
Mi az a treeification a `HashMap`-ben?
Ez a Java 8+ védelmi és teljesítményi mechanizmusa, amikor az erősen ütköző bucketek linked list helyett kiegyensúlyozott fává alakulnak.
Mikor választanál mégis `TreeMap`-et a `HashMap` helyett?
Ha a rendezett kulcsok, a navigációs metódusok vagy a tartomány-lekérdezések valódi követelmények.
9. Szószedet
| Fogalom | Definíció |
|---|---|
| Load factor | Bejegyzések és vödrök aránya (alapérték 0.75); átméretezést vált ki ha túllépik |
| Rehashing | Új, nagyobb vödörtömb létrehozása és az összes bejegyzés újra-beillesztése — O(n) |
| Treeification | Java 8+ HashMap vödör átalakítása Red-Black Tree-vé ≥8 bejegyzésnél |
| Cache lokalitás | Mennyire fér bele az adat a CPU cache sorokba; összefüggő tömböknek kiváló a lokalitása |
| Amortizált O(1) | Átlagosan O(1) sok műveleten keresztül, alkalmanként O(n) tüske ellenére (pl. átméretezés) |
| LRU cache | Least Recently Used — a legrégebben hozzáfért bejegyzést távolítja el amikor megtelt |
10. Gyorsreferencia
- 📋
ArrayList— alapértelmezettList. O(1) get. Előre méretezés:new ArrayList<>(n) - 🔗
LinkedList— kerüld List-ként. CsakDeque-ként használd. ~48 bájt/csomópont overhead - 🗺️
HashMap— alapértelmezettMap. O(1) átl. ≥8 bejegyzésnél vödörré alakul - 🔗
LinkedHashMap— HashMap + beillesztési/hozzáférési sorrend. LRU cacheremoveEldestEntry-vel - 🌲
TreeMap— O(log n), rendezett kulcsok. Tartomány lekérdezéshez,firstKey(),headMap() - 🔑
HashSet= belül HashMap. Helyesequals/hashCodeszükséges - ⚠️ Módosítható HashMap/HashSet kulcs = elveszett bejegyzések — mindig immutábilis kulcsokat használj
- 📏 HashMap előre méretezés:
new HashMap<>((int)(n / 0.75) + 1)a rehashing elkerüléséhez - 🚀
EnumMap/EnumSetenum kulcsoknál — tömb-alapú, nincs hashing szükséges - 🔄 Map iterálása
entrySet()-tel vagyforEach()-vel, sohakeySet()+get(key)-jel
🎮 Játékok
10 kérdés