Java Senior Interview kérdések
1 – Mi a különbség a volatile és a synchronized között a Java Memory Model szempontjából?
Atomicitás (atomicity): egy művelet akkor atomi, ha más szálak számára oszthatatlanul, megszakíthatatlanul hajtódik végre – vagyis nem látható köztes, részben végrehajtott állapot.
volatile: - Láthatóságot (visibility – minden szál a legfrissebb, main memory-ben lévő értéket látja) és sorrendiségi garanciát (ordering – bizonyos utasítás-átütemezések tiltása a memory barrier-ek miatt) biztosít egy változóra. - Nem teszi atommá az összetett (compound) műveleteket (több lépésből álló logikai egység, pl. ellenőrzés + módosítás: if (x == 0) x++). - A volatile írás és olvasás happens-before kapcsolatot hoz létre (a Java Memory Modelben definiált memória-láthatósági garancia), de nem biztosít kölcsönös kizárást.
synchronized: - Kölcsönös kizárást (mutual exclusion – egyszerre csak egy szál lehet bent a kritikus szekcióban) biztosít. - Monitor: Minden Java objektumhoz tartozó belső szinkronizációs mechanizmus a JVM-ben. Amikor egy szál belép egy synchronized blokkba, megszerzi az adott objektum monitorját (lock-ot), és más szálak addig nem léphetnek be, amíg az első szál ki nem lép és fel nem szabadítja. - A synchronized blokk belépésekor (monitor enter) és kilépésekor (monitor exit) happens-before kapcsolat jön létre, ami garantálja, hogy a kritikus szekción belül végzett összes írás látható lesz más szálak számára. - Nem az egyes JVM utasításokat teszi atommá, hanem a teljes kritikus szekció végrehajtását teszi más szálak felé atomi hatásúvá.
Érdekességek: - Thread Confinement: Olyan technika, ahol az állapotot nem több szál között védjük lockkal, hanem egyetlen szálhoz kötjük (pl. SingleThreadExecutor, ThreadLocal, request-scoped bean vagy actor-szerű modell), így nincs szükség explicit szinkronizációra. - ReentrantLock: A synchronized-hez hasonló kölcsönös kizárást biztosít, de rugalmasabb vezérlést ad (pl. tryLock(), időzített lock kérés, megszakítható várakozás, opcionális fairness). Az unlock manuális, ezért hibalehetőség.
2 – Mi történik a háttérben, amikor @Transactional-t használsz Spring Boot 3-ban?
Amikor egy metódust @Transactional annotációval látunk el, a Spring nem magát a metódust módosítja, hanem egy proxy objektumot hoz létre köré.
Mi az a proxy? Egy köztes objektum, amely a valódi bean előtt ül, és interceptálja (elfogja) a metódushívásokat. A Spring ezt AOP (Aspect-Oriented Programming – aspektus-orientált programozás) segítségével valósítja meg.
Mi az az AOP? Célja, hogy keresztmetszeti (cross-cutting) funkcionalitásokat – pl. tranzakciókezelés, logging, security – leválasszon az üzleti logikáról.
A hívás folyamata: 1. A kliens nem a valódi bean-t hívja, hanem a proxy-t. 2. A proxy meghívja a TransactionManager-t, hogy indítson tranzakciót. 3. A proxy meghívja az eredeti metódust. 4. Ha a metódus sikeresen lefut → commit történik. 5. Ha RuntimeException vagy Error dobódik → rollback történik. 6. Checked exception esetén alapértelmezetten commit történik, hacsak nem adunk meg rollbackFor-t.
PlatformTransactionManager: A Spring tranzakciókezelő absztrakciója. - DataSourceTransactionManager → tiszta JDBC - JpaTransactionManager → JPA/Hibernate
Hogyan kötődik a tranzakció a szálhoz? A Spring a TransactionSynchronizationManager segítségével a tranzakcióhoz tartozó erőforrásokat (pl. JDBC Connection, EntityManager) ThreadLocal-ban tárolja. Ezért a tranzakció thread-bound, tehát más szálra nem terjed át automatikusan.
ThreadLocal: Olyan mechanizmus, amely szálanként külön példányt tart fenn egy változóból.
Fontos működési részletek: - Self-invocation esetén (osztályon belüli metódushívás) nincs proxy → nincs tranzakció. - A commit a metódus végén történik. - JPA esetén a flush (az entitások SQL-re fordítása) commit előtt is megtörténhet. - Az isolation és propagation paraméterek a TransactionManageren keresztül kerülnek a JDBC driver felé. - Spring Boot auto-konfiguráció automatikusan létrehozza a megfelelő TransactionManager beant.
3 – Mi a különbség a HashMap és a ConcurrentHashMap között?
HashMap: - Nem thread-safe. - Több szál egyidejű módosítása race condition-höz (versenyhelyzet) vezethet. - Inkonzisztens állapot is kialakulhat.
ConcurrentHashMap: - Thread-safe. - Nem használ globális lockot. - Finom granularitású lockolást és CAS (Compare-And-Swap – hardveres atomi összehasonlító-cserélő művelet) műveleteket használ. - Nagy collision esetén a bucket belső struktúrája kiegyensúlyozott fává (red-black tree) alakul.
Olvasás: Jellemzően lock nélkül történik, de a memória-láthatóság garantált.
Iterátor: - HashMap → fail-fast (ConcurrentModificationException) - ConcurrentHashMap → weakly consistent (nem dob hibát, de nem snapshot)
Null kezelés: - HashMap → enged null kulcsot és értéket - ConcurrentHashMap → nem enged null-t (mert nem lehetne megkülönböztetni a „nincs érték" és a „null érték" esetet concurrency mellett)
4 – Mit csinál pontosan a @SpringBootApplication annotáció Spring Boot 3-ban?
A @SpringBootApplication meta-annotáció, amely magában foglalja: - @SpringBootConfiguration – speciális @Configuration, amely az alkalmazás fő konfigurációs osztályát jelöli. - @ComponentScan – a főosztály csomagjától lefelé beolvassa a komponenseket. - @EnableAutoConfiguration – elindítja az auto-konfigurációs mechanizmust.
Spring Boot 3-ban az auto-konfigurációs osztályok a META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports fájlban vannak felsorolva.
Ezek az osztályok feltételekhez kötötten töltődnek be (@ConditionalOnClass, @ConditionalOnMissingBean, stb.).
Az auto-konfiguráció mindig a felhasználó saját konfigurációja után értékelődik ki, és nem írja felül az explicit definiált beaneket.
Az @AutoConfiguration nem component scan által töltődik be, hanem az imports fájlból – ezért lehet akár külső starter JAR-ban is.
Ez a „convention over configuration" elvet valósítja meg.
5 – Mi a különbség a == és az equals() között?
== operátor: - Primitív típusoknál → értékegyenlőség. - Objektumoknál → referencia-azonosság (ugyanarra a memóriacímre mutat-e).
equals(): - Logikai egyenlőséget vizsgál. - Alapértelmezésben referencia-összehasonlítás. - Gyakran felülírt (String, Integer, LocalDate, record stb.).
String és a String Pool: A String immutable (változtathatatlan), ezért a JVM optimalizálásként String Pool-t használ. A String Pool a heap egy speciális része, ahol azonos literálból csak egy példány létezik.
String a = "hello";
String b = "hello";
// a == b → true (ugyanarra a pool-beli példányra mutatnak)
String a = new String("hello");
String b = new String("hello");
// a == b → false (két külön objektum)
intern(): Megnézi, hogy létezik-e az adott tartalmú String a pool-ban. Ha igen → visszaadja a pool-beli referenciát. Ha nem → beteszi és visszaadja.
Objektumokat (különösen Stringet) mindig
equals()-szel hasonlítunk, mert az a tartalmat vizsgálja.
6 – Mi az a tranzakciós izolációs szint, és mit jelent a READ_COMMITTED vs REPEATABLE_READ?
Mi az izoláció? A tranzakció izoláció azt határozza meg, hogy egy tranzakció mennyire van elszigetelve más párhuzamos tranzakcióktól. Célja: konzisztens adat, párhuzamosság támogatása, anomáliák minimalizálása.
Fontos anomáliák: - Dirty Read: Egy tranzakció olyan adatot olvas, amit egy másik még nem commitált. Ha az rollbackel, az első tranzakció nem létező adatot használt. - Non-repeatable Read: Ugyanazon sor kétszeri olvasásakor eltérő értéket kapunk, mert közben egy másik tranzakció commitált módosítást. - Phantom Read: Egy feltételes lekérdezés eredményhalmaza megváltozik (pl. új sor beszúrása miatt).
READ_COMMITTED: - Csak commitált adat olvasható. - Dirty read nem lehetséges. - Non-repeatable read lehetséges. - Phantom read lehetséges. - Jó teljesítmény és skálázhatóság.
REPEATABLE_READ: - Dirty read nem lehetséges. - Non-repeatable read nem lehetséges. - Phantom read adatbázis implementációtól függ. - MVCC (Multi-Version Concurrency Control) segítségével működik: az adatbázis nem felülírja a sort, hanem új verziót hoz létre a verzióláncban; az olvasó tranzakció egy logikai snapshotot kap.
Hogyan működik az MVCC snapshot? - Tranzakció induláskor az adatbázis feljegyzi a tranzakció-IDt (pl. txid: 100). - Ettől fogva minden lekérdezés csak azokat a sorverziókat látja, amelyek txid ≤ 100 előtt commitáltak. - Nem készül fizikai táblamásolat – az adatbázis lustán működik: csak lekérdezéskor nézi meg a verzióláncot, és alkalmazza a szabályt: „csak txid ≤ 100 verziókat mutass". - Ha közben egy másik tranzakció (txid: 101) új sort szúr be vagy módosít, azt a mi tranzakciónk nem látja – az adatbázis a verzióláncból a számunkra látható régi verziót adja vissza. - Példa:
T=0 Tranzakció A indul (txid: 100)
T=1 SELECT * FROM orders → látja a txid<100 előtt commitált sorokat
T=2 Tranzakció B INSERT INTO orders VALUES(...) → COMMIT (txid: 101)
T=3 SELECT * FROM orders → ugyanazt látja mint T=1-ben, a B által beszúrt sor nem látható
- A régi verziókat az adatbázis addig tartja meg, amíg aktív tranzakciónak szüksége lehet rájuk. PostgreSQL-ben a
VACUUMprocess takarítja ki az elavult verziókat.
Locking típusok: - Pesszimista locking: konfliktus valószínűnek feltételezve előre lockolja az adatot. - JPA: entityManager.find(Account.class, 1, LockModeType.PESSIMISTIC_WRITE) - SQL: SELECT * FROM account WHERE id = 1 FOR UPDATE - PESSIMISTIC_READ → shared lock (más olvashat, de nem írhat) - PESSIMISTIC_WRITE → exclusive lock (más sem olvasni, sem írni nem tud) - Hátrány: lassabb, deadlock kockázat. - Optimista locking: konfliktus ritkának feltételezve nem lockolunk, hanem commitkor verzióval ellenőrzünk. - JPA: @Version private Long version; - Mentéskor: UPDATE account SET balance = ?, version = version + 1 WHERE id = ? AND version = ? - Ha a verzió időközben változott → OptimisticLockException – a retry kezelése fejlesztői feladat (pl. @Retryable). - Előny: nincs lock overhead, jó sok olvasás / kevés írás esetén. - Fontos: nem automatikus – csak explicit @Version annotációval aktív.
Izoláció vs Locking: - Az izolációs szint az adatbázis viselkedését szabályozza globálisan. - A locking stratégia alkalmazásszinten finomhangolja az egyedi műveletek konkurenciakezelését. - Ha locking kell, az ajánlott kiindulópont a @Version-alapú optimista locking; pesszimista locking csak indokolt esetben (pl. kritikus pénzügyi műveletek). Alapból egyik sem aktív automatikusan.
Melyik izolációs szintet mikor? - READ_COMMITTED – általános célra, legtöbb alkalmazásban elegendő (pl. PostgreSQL default). - REPEATABLE_READ – ha ugyanazon adat többszöri olvasásának konzisztenciája kritikus (pl. riport generálás tranzakción belül). - SERIALIZABLE – teljes izoláció, phantom read sem lehetséges; erős garancia, de legkisebb párhuzamosság.
7 – Mi az a Bean életciklus (Bean Lifecycle) Spring Boot 3-ban?
A Spring minden beant maga hoz létre és kezel. A lifecycle azt írja le, hogy egy bean hogyan születik, inicializálódik és szűnik meg.
Fázisok sorrendben: 1. Példányosítás – Spring létrehozza az objektumot (reflection-nel). 2. Dependency Injection – Spring befecskendezi a függőségeket (@Autowired, konstruktor, setter). 3. Aware interfészek (BeanNameAware, BeanFactoryAware stb.) – Ha a bean implementálja ezeket az interfészeket, a Spring információt ad át a beannek a saját Spring környezetéről. - BeanNameAware – A Spring beállítja a bean saját nevét (amit az application contextben kapott, pl. "userService") a setBeanName(String name) metóduson keresztül. - BeanFactoryAware – A Spring átadja magát a BeanFactory-t (az a konténer, amely a beaneket kezeli) a beannek, amely ezután közvetlenül lekérhet más beaneket a factory-ból. - Normál üzleti logikában nem szokás Aware interfészeket implementálni, mert szorosan csatolja a beant a Spring keretrendszerhez. Főleg framework-szintű vagy infrastrukturális kódban fordul elő. 4. BeanPostProcessor – postProcessBeforeInitialization – Minden beannél lefut, még a @PostConstruct előtt. Paraméterként megkapja a bean objektumot és a bean nevét, és visszaadja az objektumot (esetleg módosítva). 5. @PostConstruct – Az inicializáló metódus lefut. Itt lehet pl. kapcsolatot ellenőrizni, cache-t feltölteni. Ezen a ponton minden injektált függőség már elérhető. 6. BeanPostProcessor – postProcessAfterInitialization – Minden beannél lefut, a @PostConstruct után. A bean ekkor már teljesen inicializált – itt hozza létre a Spring az AOP proxy-kat (pl. @Transactional). A visszaadott objektum lehet proxy is, ezt regisztrálja a kontextus. 7. Bean használható – A bean elérhető az alkalmazásban. 8. @PreDestroy – Leállításkor lefut. Itt lehet erőforrásokat felszabadítani.
Fontos fogalmak: - Reflection: Java mechanizmus, amellyel futásidőben lehet osztályokat, konstruktorokat, metódusokat vizsgálni és meghívni – a Spring ezt használja a beanek létrehozásához. - BeanPostProcessor: Egy interfész, amellyel minden bean inicializálása előtt és után lehet beavatkozni globálisan. Minden beannél meghívódik, paraméterként megkapja a bean objektumot és nevét, és visszaadhatja az eredeti vagy egy proxy objektumot. Az AOP proxy-k létrehozása a postProcessAfterInitialization fázisban történik. - Scope: - @Scope("singleton") – alapértelmezett, egy példány az egész alkalmazásban. Nem szükséges kiírni. - @Scope("prototype") – minden getBean() hívásnál új példány jön létre, a Spring nem hívja meg rajta a @PreDestroy-t. - Singleton beanbe injektált prototype bean csapda: a DI csak egyszer fut le, ezért az injektált prototype példány végig ugyanaz marad – elvész a prototype viselkedés.
Prototype bean helyes elérése singleton beanből:
ApplicationContext-ből lekérve:
@Service
public class OrderService {
@Autowired
private ApplicationContext context;
public void process() {
CsvParser parser = context.getBean(CsvParser.class); // minden hívásra új példány
parser.parse();
}
}
@Lookup annotációval (elegánsabb, nem kell az ApplicationContext):
@Service
public class OrderService {
public void process() {
CsvParser parser = getCsvParser(); // minden hívásra új példány
parser.parse();
}
@Lookup
protected CsvParser getCsvParser() {
return null; // Spring felülírja runtime-ban proxy-val, ez a sor sosem fut le
}
}
@Lookup metódust a Spring runtime-ban felülírja egy proxy-val, ami minden híváskor getBean()-t hív a háttérben. 8 – Mi az a CAP-tétel (CAP Theorem)?
Elosztott rendszereknél (distributed systems – több gépen futó rendszer) egyszerre csak 2 tulajdonság garantálható a 3-ból:
- C – Consistency (Konzisztencia): Minden olvasás a legfrissebb írást adja vissza.
- A – Availability (Elérhetőség): Minden kérés kap választ (de nem feltétlenül a legfrissebbet).
- P – Partition Tolerance (Partíció-tűrés): A rendszer működik akkor is, ha a csomópontok (node-ok) között megszakad a hálózati kapcsolat.
Hálózati partíció a valóságban mindig előfordulhat, ezért a P általában nem elhagyható. Így a valódi választás: CP vagy AP.
Példák: - CP rendszer: ZooKeeper, HBase – inkább visszautasít egy kérést, mint hogy inkonzisztens adatot adjon. - AP rendszer: Cassandra, DynamoDB – mindig válaszol, de az adat esetleg nem a legfrissebb. - Relációs adatbázis (pl. PostgreSQL) – egyetlen gépen CA, de elosztott környezetben P nélkül nem működik.
Miért fontos senior Java fejlesztőként? Microservice architektúrában dönteni kell, hogy egy szolgáltatás CP vagy AP legyen – ez befolyásolja az adatbázis és messaging technológia választását is.
9 – Mi az a Spring Bean Scope, és mikor melyiket használjuk?
A scope meghatározza, hogy egy beanből mikor és hány példány jön létre a Spring konténerben.
Singleton (alapértelmezett): - Az egész alkalmazásban egyetlen példány létezik. - A Spring az első kéréskor létrehozza, majd cacheli. - Állapot nélküli (stateless) komponenseknél ideális – pl. service, repository réteg.
Prototype: - Minden egyes kérésre (bean lekérésre) új példány jön létre. - A Spring nem kezeli a bean teljes életciklusát – a @PreDestroy nem hívódik meg. - Állapotot tároló (stateful) objektumoknál használatos.
Request (@Scope("request")): - HTTP kérésenként egy példány jön létre. - A kérés végén a Spring megsemmisíti. - Csak web alkalmazásban érvényes. - Párhuzamos kéréseknél minden kérés a saját izolált példányát kapja – thread-safe állapottárolásra alkalmas. - Singleton beanbe injektáláshoz proxyMode = ScopedProxyMode.TARGET_CLASS szükséges; a proxy minden hívásnál az aktuális kéréshez tartozó példányt adja vissza.
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { ... }
Session (@Scope("session")): - HTTP sessionönként egy példány él. - A session lejártával semmisül meg. - Csak web alkalmazásban érvényes. - Egy felhasználó több kérése között megőrzi az állapotot, de különböző felhasználóknak külön példány él – pl. kosár, wizard lépései. - Singleton beanbe injektáláshoz szintén proxyMode = ScopedProxyMode.TARGET_CLASS szükséges. - A session létrehozását valami triggernek kell kiváltania – pl. session scope-os bean aktiválása, explicit HttpSession használat, vagy Spring Security; tiszta stateless REST API-nál session alapból nem jön létre.
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart { ... }
| Scope | Hány példány? | Meddig él? |
|---|---|---|
| Singleton | 1 az egész appban | Az app indulásától leállásáig |
| Prototype | Minden kérésre új | Spring nem kezeli a végét |
| Request | 1 HTTP kérésenként | A kérés végéig |
| Session | 1 HTTP sessionönként | A session lejártáig |
Fontos probléma – Scope mismatch: Ha egy singleton bean egy prototype beant injektál, a prototype bean csak egyszer jön létre (a singleton példányosításakor), és utána nem cserélődik le – elveszti a prototype jellegét. Megoldás: ApplicationContext-ből manuálisan kérjük le, vagy @Lookup annotációt használunk.
@Lookup: Egy Spring mechanizmus, amellyel minden metódushívásnál a konténer friss prototype példányt ad vissza, nem a cacheltet.
10 – Mi az a Spring @Autowired, és hogyan működik a dependency injection?
A dependency injection (DI – függőség befecskendezés) egy tervezési minta, amelynek lényege, hogy egy objektum nem maga hozza létre a függőségeit, hanem kívülről kapja meg azokat. A Spring konténer felel a függőségek összerakásáért.
A @Autowired jelzi a Spring számára, hogy az adott mezőt, konstruktort vagy settert automatikusan töltse fel a megfelelő beannel.
3 injection típus:
Konstruktor injection (ajánlott): - A függőségek a konstruktoron keresztül érkeznek. - A bean azonnal teljes és valid állapotban van. - Kötelező függőségeknél használatos. - Spring 4.3 óta egyetlen konstruktor esetén a @Autowired elhagyható. - A mező final lehet, így immutable és biztonságosabb.
Setter injection (kerülendő): - A függőség egy setter metóduson keresztül érkezik. - Opcionális függőségeknél használatos, de modernebb megoldás az ObjectProvider konstruktor injectionnel. - ObjectProvider<T>: Spring-specifikus wrapper, amellyel egy függőség opcionálisan kérhető le – ha nem létezik a bean, nem dob hibát, hanem null-t vagy egy alapértelmezett értéket ad vissza. Így a konstruktor injection előnyei megmaradnak, az optionalitás mégis kezelhető.
Field injection (kerülendő): - A @Autowired közvetlenül a mezőre kerül. - Reflection-nel működik, nehezebben tesztelhető (unit tesztben nem lehet egyszerűen mock-ot beadni). - Elrejti a függőségeket – konstruktorral egyben látható lenne az összes. - A mező nem lehet final, így semmi nem garantálja, hogy az értéke nem változik meg később. - Éles kódban nem ajánlott.
Összefoglalva: 1. Kötelező függőség → konstruktor injection 2. Opcionális függőség → konstruktor injection + ObjectProvider<T> 3. Setter és field injection → kerülendő, legacy
Ha több ugyanolyan típusú bean létezik: - @Qualifier("beanNeve") – megmondja a Springnek, melyik beant injektálja. - @Primary – az adott beant jelöli alapértelmezettnek, ha nincs más megkötés.
@Autowired(required = false): Ha a függőség opcionális és nem található megfelelő bean, a Spring nem dob hibát, hanem null-t hagy a mezőben.