Kihagyás

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.

String a = new String("hello").intern();
String b = "hello";
// a == b → true

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 VACUUM process 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
    }
}
A @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.