static, final & transient: interview questions & scenarios
These are the static, final and transient questions I hear most in Java screens. Small keywords lead straight into class initialisation, the memory model and serialization, which is why interviewers keep coming back to them.
Here's how to use this page: read the question, say your answer out loud first, then open the model answer. If a question stumps you, the lessons are in this topic: The static keyword, The final keyword and The transient keyword.
Explain final, finally and finalize().
Tests keywords, exception handling, finalization
Show answerHide answer
Model answer
final: a modifier. Assign once for variables, do not override for methods, and do not extend for classes.finally: a block aftertry/catchthat runs whether or not an exception was thrown, except when the JVM halts, crashes or the process is killed.finalize(): an oldObjecthook the GC might call before reclaiming an object. It is deprecated since Java 9 and deprecated for removal since Java 18 through JEP 421. Use try-with-resources, orCleaneras a safety net.
How I’d say it in the interview
"Final is the assign-once, no-override, no-subclass modifier. Finally is the cleanup block. Finalize is the old GC hook, deprecated for removal, and I avoid it in favour of try-with-resources."
Signals depth
Knowing --finalization=disabled (Java 18+) lets you test whether code still depends on finalizers.
Red flags
"finalize is like a destructor and always runs."
Follow-ups interviewers ask next
What if
finallycontains areturn?It overrides the
tryreturn value and swallows any exception thrown intry.javac -Xlint:finallyand most IDEs flag it; never return fromfinally.What if the JVM exits inside the
tryblock?System.exit, a crash, or an external kill can preventfinallyfrom running. For external resources, I design for idempotent cleanup outside the process too.
A final List can still be modified. Explain.
Tests final reference vs immutable object
Show answerHide answer
Model answer
final locks the reference, not the object. final List<String> tags = new ArrayList<>(); means tags always points to the same list, so tags = new ArrayList<>() will not compile, but tags.add("vip") is fine.
Immutability is a property of the object's class. Use List.of(...) or List.copyOf(...), or a record with immutable components. For a field, combine both: a private final field holding an unmodifiable list.
How I’d say it in the interview
"Final means the variable cannot point anywhere else. The list it points to is still an ArrayList, so add works. If I want the contents fixed, I need an unmodifiable list like List.copyOf, and I keep the field final too."
Signals depth
Mentioning that Collections.unmodifiableList is only a view, so changes to the backing list still show through, while List.copyOf takes a snapshot.
Red flags
"Final makes the object immutable."
Follow-ups interviewers ask next
What if the list holds mutable objects?
Then even
List.copyOfis not deep immutability: the elements can still change. Immutability has to go all the way down, or you copy defensively.What if I expose that list through a getter?
Return an unmodifiable view or snapshot, not the mutable internal list. Otherwise callers can mutate your object's state without going through your invariants.
What does this print?
Tests static method hiding, invokestatic
Show answerHide answer
Model answer
class Base { static String who() { return "Base"; } String me() { return "Base"; } }
class Derived extends Base {
static String who() { return "Derived"; }
@Override String me() { return "Derived"; }
}
Base b = new Derived();
System.out.println(b.who() + " " + b.me()); // Base Derived
Static methods are hidden, not overridden. b.who() compiles to invokestatic Base.who, chosen from the declared type of b. b.me() is an invokevirtual, dispatched on the runtime class, so it runs Derived.me().
This is the picture interviewers want you to have in your head: one call is selected from the variable type, the other from the object type.
The diagram shows a Base reference pointing to a Derived object, with static dispatch going to Base.who and virtual dispatch going to Derived.me.
How I’d say it in the interview
"Base Derived. The static call is bound at compile time to the declared type, Base, because static methods are hidden, not overridden. The instance method uses dynamic dispatch, so it runs Derived's version."
Signals depth
Naming invokestatic vs invokevirtual, and noting that @Override on a static method is a compile error.
Red flags
"Derived Derived, because the object is a Derived." That treats statics as polymorphic.
Follow-ups interviewers ask next
What if
bisnull?b.who()still printsBase, with noNullPointerException. The JVM never uses the reference for a static call.b.me()would throw.What if I call
Derived.who()explicitly?Then the compiler emits a call to
Derived.who. Static calls are clearest when written through the class name rather than an instance reference.
When exactly does a static block run?
Tests class initialisation triggers, constant variables
Show answerHide answer
Model answer
Once, on the class's first active use (JLS 12.4.1): creating an instance, calling a static method, assigning a static field, reading a static field that is not a compile-time constant, initialising a subclass, reflection like Class.forName, or being the main class.
It does not run when you read a compile-time constant, create an array with new Config[10], use Config.class, or read a parent's static field through a subclass name. Loading a class and initialising it are different steps.
How I’d say it in the interview
"On first active use, not on load: new, a static method call, or a non-constant static field access. Reading a static final int or String constant does not trigger it because javac inlined the value, and neither do array creation or a class literal."
Signals depth
Mentioning the bytecodes (new, getstatic, putstatic, invokestatic) and that static initialisers run in textual order inside one <clinit> method, parent first.
Red flags
"When the class is loaded" or "when the program starts."
Follow-ups interviewers ask next
What if the static block throws an exception?
The first access gets
ExceptionInInitializerError. The class is marked as failed, and every later access throwsNoClassDefFoundError: Could not initialize class ..., without the original cause. Look for the first error in the logs.What if two classes' static initialisers reference each other and two threads start them together?
They can deadlock: each thread holds one class's initialisation lock and waits for the other. Keep static initialisers small and free of cross-class cycles.
Where are static fields stored in memory?
Tests Class mirror, Metaspace, GC roots
Show answerHide answer
Model answer
In HotSpot, since JDK 7, static fields live in the class's java.lang.Class object (the mirror) on the regular Java heap. The class metadata, meaning bytecode, method tables and the constant pool, lives in Metaspace, native memory that replaced PermGen in Java 8.
Static fields exist once per class per class loader, and they stay reachable as long as the class is loaded, which for application classes usually means for the lifetime of the JVM.
Here is the split I want you to draw if the interviewer asks where the memory actually is.
The diagram separates the Class mirror on the Java heap from class metadata in Metaspace, with static fields reachable through the mirror.
How I’d say it in the interview
"Static fields are stored with the Class object on the heap; class metadata is in Metaspace since Java 8. There is one copy per class per class loader, and it lives as long as the class does, which is why static collections can leak."
Signals depth
Separating the mirror on the heap from the klass metadata in Metaspace, and the "per class loader" detail.
Red flags
"Static fields are in PermGen" (gone since Java 8) or "on the stack."
Follow-ups interviewers ask next
What if two web apps in one Tomcat load the same class?
Each web app has its own class loader, so there are two distinct classes and two independent sets of static fields. A static reference from a shared parent loader into a web app's objects prevents that web app from being unloaded on redeploy.
What if a static map grows forever?
It is reachable from the class mirror, so the GC cannot reclaim its entries while the class is loaded. Use a bounded cache with eviction, or move ownership to a component whose lifecycle you control.
Your payments service uses a static int request counter. In prod, the dashboard undercounts during traffic spikes. Walk me through it.
Tests static state, atomicity, contention
Show answerHide answer
Model answer
count++ on a static int is a read, an add and a write. Two threads can read the same value, both add one, and one write overwrites the other. static gives you one shared copy; it does not make the update atomic.
I would use AtomicLong for a correct counter, LongAdder for a hot metrics counter with many writers and occasional reads, or synchronized when the counter must update together with other state. volatile alone only gives visibility, not atomicity.
How I’d say it in the interview
"The spike exposes a lost-update race. Static means shared, so all request threads hit the same field, but plus-plus is read-modify-write. For metrics I would normally use LongAdder; for a single exact value I would use AtomicLong."
Signals depth
Distinguishing visibility from atomicity, and picking LongAdder only when its relaxed read semantics are acceptable.
Red flags
"Static variables are shared, so all threads see the same value, so it is safe."
Follow-ups interviewers ask next
What if the static field is a
HashMapused as a cache?Concurrent writes can corrupt it or lose entries, and since it never evicts, it is also a memory leak. Use a bounded, concurrent cache, owned by a normal application component.
What if product needs exact per-request billing, not metrics?
Do not use
LongAdderas the source of truth. Put the billing event in durable storage or a queue, make the operation idempotent, and treat in-memory counters as observability only.
What happens to transient fields when an object is deserialized?
Tests transient, deserialization constructor, readObject
Show answerHide answer
Model answer
They are left at their type defaults: null, 0, false. Deserialization does not run the class's own constructors or field initialisers; it only runs the no-arg constructor of the first non-serializable superclass, usually Object. So transient Map<String, Object> cache = new HashMap<>() comes back as null, and transient int retries = 3 comes back as 0.
To restore them, write a private readObject(ObjectInputStream in) that calls in.defaultReadObject() and then recomputes the transient fields, or initialise them lazily in a getter.
How I’d say it in the interview
"They come back as null, zero or false, not their initializer values, because the class's constructor does not run on deserialization. I restore them in readObject after defaultReadObject, or lazily."
Signals depth
Knowing that static fields are never serialized anyway, and that a final transient field stays null because readObject cannot assign a final field in the normal way.
Red flags
"Transient fields are reset to their initial values."
Follow-ups interviewers ask next
What if the class implements
Externalizableinstead?Then
transientdoes not matter: you write and read every field yourself inwriteExternalandreadExternal, and the class needs a public no-arg constructor, which does run.What if I serialize the same object with Jackson?
Jackson, by default, only stops using the transient field as an accessor. A public getter for that property can still be serialized, so a
transient passwordwithgetPassword()may end up in JSON. Use@JsonIgnore, enableMapperFeature.PROPAGATE_TRANSIENT_MARKER, or return a DTO without the field.
Why must a local variable used in a lambda be final or effectively final?
Tests lambda capture, effectively final
Show answerHide answer
Model answer
A lambda can run later, possibly on another thread, after the method that created it has returned and its stack frame is gone. Java captures a copy of the value, not the local variable itself. If the variable could still change, the lambda's copy and the method's variable could silently disagree.
Fields do not have this rule, because a lambda reads them through this, which is a captured reference to a heap object.
How I’d say it in the interview
"Because the lambda captures the value, not the variable: the stack frame may be gone when it runs. Effectively final guarantees the copy can never be stale. Instance fields are different because the lambda captures this and reads the field from the heap."
Signals depth
Knowing the captured values become arguments to the synthetic lambda machinery via invokedynamic and LambdaMetafactory, and that mutating fields from lambdas can still cause races.
Red flags
Recommending the one-element-array trick (int[] sum = {0}) as the standard fix.
Follow-ups interviewers ask next
What if I need to sum values inside a
forEach?Do not mutate from the lambda. Use a reduction:
orders.stream().mapToLong(Order::amount).sum(). If you truly need shared mutable state, useLongAdderor collect into a thread-confined structure.What if the lambda captures a final reference to a mutable list?
The reference is stable, but the list can still mutate. Effective finality says nothing about the thread-safety or immutability of the object behind the reference.
When would you use a static utility class instead of a singleton?
Tests static vs singleton, testability, holder idiom
Show answerHide answer
Model answer
A static utility class (Math, Collections, my own MoneyFormat) fits stateless, pure functions with no dependencies. Give it a private constructor.
When there is state or a dependency, I prefer an object: it can implement an interface, be injected and mocked in tests, be lazily initialised, and become two instances later without changing callers. In Spring, that is a regular bean. A hand-written lazy singleton uses the holder idiom, which is thread-safe because class initialisation runs once under a JVM lock.
public final class RateCard {
private RateCard() { }
private static final class Holder { static final RateCard INSTANCE = new RateCard(); }
public static RateCard get() { return Holder.INSTANCE; }
}
How I’d say it in the interview
"Static utilities for stateless functions. As soon as there is state or a dependency, I want an object I can inject and mock, so a singleton, usually a Spring bean. If I hand-roll one, I use the holder idiom or a single-element enum."
Signals depth
Explaining why the holder idiom is thread-safe (JLS 12.4.2 initialisation lock) and lazy (the nested class initialises on first use).
Red flags
"Static is faster, so I always use static methods."
Follow-ups interviewers ask next
What if I ask for a singleton that survives serialization?
A single-element
enumis serialization-safe by design. Otherwise implementreadResolve()to returnINSTANCE, or deserialization creates a second instance.What if the singleton owns an HTTP client or cache?
Then I prefer dependency injection with lifecycle management. Static ownership makes shutdown, testing, metrics and multi-tenant configuration harder.
What guarantee does the Java Memory Model give for final fields?
Tests JLS 17.5, safe publication, this-escape
Show answerHide answer
Model answer
When a constructor completes, its final fields are frozen. Any thread that later obtains a reference to the object, even through a data race with no volatile or locking, is guaranteed to see the final fields' initialised values, and the state of objects reachable through them as of the end of the constructor. Non-final fields get no such guarantee: another thread may see 0 or null.
The condition is that this must not escape the constructor, for example by registering a listener or storing this in a static field. If it escapes, other threads can see the object before the freeze.
How I’d say it in the interview
"Final fields are frozen at the end of the constructor, so any thread that sees the reference sees their initialised values, without synchronization. That is what makes immutable objects like String safe to share. It only holds if this does not leak out of the constructor."
Signals depth
Mentioning that HotSpot uses barriers around construction to support the guarantee, and that the guarantee is about construction safety, not general thread-safety.
Red flags
"Final fields are thread-safe" with no mention of construction or this escape, or confusing it with volatile.
Follow-ups interviewers ask next
What if the final field is a
HashMapthat is modified after construction?The guarantee covers the map's state as of the freeze. Later modifications get no visibility guarantee; you need a concurrent map or synchronization.
What if a constructor starts a thread that reads the object?
That is a
thisescape. The new thread can observe the object before construction is complete, so the final-field guarantee is no longer the safety net you wanted.
We changed a public static final int timeout in a shared library, deployed the new jar, and prod still uses the old value. Walk me through it.
Tests constant variables, inlining, binary compatibility
Show answerHide answer
Model answer
A static final field of primitive or String type initialised with a constant expression is a constant variable. javac copies its value into every class that uses it, so the caller never reads the field at runtime. Swapping the jar does not change already-compiled clients; they need to be recompiled.
For values that may change, avoid constant variables: expose them through a static method, load them from configuration, or initialise them with a non-constant expression such as Integer.valueOf(500).
How I’d say it in the interview
"It was inlined. Static final primitives and Strings with constant initialisers are compile-time constants, so javac copied the value into our classes. Replacing the jar does not touch them; we need to recompile, or stop making it a constant."
Signals depth
Knowing JLS 13.4.9 warns about exactly this, and that reading a constant does not trigger class initialisation either.
Red flags
"The JVM caches static fields" or "it is definitely a classloader issue."
Follow-ups interviewers ask next
What if the field were
static final Integer?Not a constant variable, because the type is not primitive or
String, so it is not inlined; clients read it withgetstaticand pick up the new value after the new class is loaded.What if only one service still sees the old value?
Check whether that service was recompiled and redeployed, not only whether the library jar changed. I would inspect the caller bytecode for an
ldcvalue versus agetstaticinstruction.
After a release, your order service starts throwing NullPointerException during startup from a static singleton. Walk me through it.
Tests static initialisation order, constant variables
Show answerHide answer
Model answer
class Pricing {
static final Pricing INSTANCE = new Pricing();
static final BigDecimal GST = new BigDecimal("0.18");
final BigDecimal rate;
Pricing() { rate = GST; }
}
System.out.println(Pricing.INSTANCE.rate); // null
Static initialisers run in textual order. INSTANCE is created first, and its constructor reads GST before GST has been assigned, so it sees null.
Fix it by declaring GST before INSTANCE, or with the holder idiom so the instance is created after all statics are ready. If GST were a static final double GST = 0.18, a constant variable, it would be inlined and the bug would disappear, which is why this one is easy to miss.
How I’d say it in the interview
"I would suspect class initialisation order. Static fields run top to bottom, so the singleton constructor can read a later static field before it is assigned. Reorder the fields or move the singleton into a lazy holder."
Signals depth
Explaining why a primitive or String constant would not show the bug, and naming <clinit>.
Red flags
"Static final fields are all set before any constructor runs."
Follow-ups interviewers ask next
What if the first request gets
ExceptionInInitializerErrorand later requests getNoClassDefFoundError?That is the same class-initialisation failure pattern. Find the first error in the logs; later failures often hide the original cause.
What if two static initialisers call each other during startup?
You can get partially initialised state or a class-initialisation deadlock. Move work out of static initialisers and into explicit application startup wiring.
Your user service deserializes session objects from Redis. After a dependency upgrade, prod starts executing unexpected code during reads. Walk me through the risk.
Tests gadget chains, ObjectInputFilter, serialization alternatives
Show answerHide answer
Model answer
ObjectInputStream.readObject() instantiates whatever serializable classes the stream names, from anywhere on the classpath, and runs their readObject or readResolve hooks without calling normal constructors. Attackers chain those hooks across library classes, called gadget chains, to reach code execution.
Defences, in order: do not deserialize untrusted Java-serialized data; use JSON, Protobuf or Avro for cross-service data; if you must use Java serialization, install an ObjectInputFilter allow-list; and validate invariants in your own readObject methods or use the serialization proxy pattern.
How I’d say it in the interview
"The stream chooses the classes, and readObject runs hooks during reconstruction. If someone can write a malicious stream into Redis, a later read can execute code through gadget chains. I would remove Java serialization from that boundary, or at least add an ObjectInputFilter allow-list."
Signals depth
Naming gadget chains, ObjectInputFilter (JEP 290, with JVM-wide filter factories in Java 17 through JEP 415), and noting that transient is not a security boundary.
Red flags
"It is safe because the classes implement Serializable" or "mark sensitive fields transient and you are secure."
Follow-ups interviewers ask next
What if the data only comes from our own Redis cache?
It is lower risk, but anyone who can write to that cache can feed streams to every service that reads it. I still add a filter, or migrate the cache format to a data-only encoding.
What if the class has
readObjectvalidation?That helps for your class invariants, but it does not stop gadget classes earlier in the graph from running. Filtering must happen before arbitrary classes are instantiated.