Classes & objects: interview questions & scenarios
These are the class and object questions I open most interviews with, because they look easy and quietly test whether you can picture the heap, the constructor chain and what this really is. The later ones are production stories, and they separate people who've debugged Java services from people who've only read about them.
Here's how to use this page: read the question, say your answer out loud first, then open the model answer. The "What if..." follow-ups are the ones I actually ask next. If one stumps you, go back to the lessons: Java classes and objects, Concrete classes in Java, A guide to constructors in Java, A guide to creating objects in Java, The this keyword in Java and Access modifiers.
What's the difference between a class and an object? What does == compare for two objects?
Tests class vs instance, references, identity
Show answerHide answer
Model answer
A class is a type: it declares fields and methods, and it's loaded once per class loader, with its metadata in Metaspace and one java.lang.Class object representing it. An object is one instance of that class on the heap: a header plus its own copy of the instance fields. Method code is never copied per object.
Variables hold references, not objects. So a == b asks "do these point at the same object?", never "do they have the same contents?". Contents are what equals compares, if the class overrides it.
Order a = new Order(7, "alice");
Order b = new Order(7, "alice");
System.out.println(a == b); // false: two objects
System.out.println(a.equals(b)); // false unless Order overrides equals
How I’d say it in the interview
"A class is the type and the code, loaded once; an object is an instance on the heap with its own field values. Variables hold references, so == checks identity, and equals checks contents if the class overrides it."
Signals depth
Mentioning the object header (mark word plus class pointer, 12 bytes by default on 64-bit HotSpot) and that getClass() returns the same Class object for every instance.
Red flags
"The class is stored inside each object" or "== compares values for objects too".
Follow-ups interviewers ask next
What if
Orderis a record?Records get
equalsandhashCodegenerated from their components, soa.equals(b)becomestrue.a == bstaysfalse: records are still separate objects with identity.
If you don't write any constructor, what does the class get? And what happens when you add one with parameters?
Tests default constructor, no-arg constructor
Show answerHide answer
Model answer
If a class declares no constructors, javac adds a default constructor: no parameters, same access as the class, body that only calls super(). The moment you declare any constructor, even one with parameters, the default one is no longer generated.
class Customer {
private final String email;
Customer(String email) { this.email = email; }
}
new Customer(); // compile error: constructor Customer in class Customer cannot be applied to given types
How I’d say it in the interview
"Write no constructor and javac gives you a no-arg one that calls super(). Write any constructor and that free one disappears, so if frameworks or subclasses need a no-arg constructor, I declare it myself."
Signals depth
Knowing that a subclass with an implicit super() then fails to compile when the parent lost its no-arg constructor, and that the default constructor's access matches the class's access.
Red flags
"Java always gives every class a no-arg constructor."
Follow-ups interviewers ask next
What if the subclass doesn't declare a constructor at all?
Its default constructor calls
super(). If the parent has onlyCustomer(String), the subclass fails with "constructor Customer in class Customer cannot be applied to given types". The fix is an explicit subclass constructor callingsuper(email).
Can a constructor be private, final, static or abstract? Can it return a value?
Tests constructor modifiers, singletons, factories
Show answerHide answer
Model answer
A constructor can be public, protected, package-private or private. It can't be final, static, abstract or synchronized, because none of those make sense for something that isn't inherited, needs an instance, and always has a body. It has no return type; if you write void Order(), that's a regular method that happens to share the class name.
A private constructor is common: utility classes (private Util() {}), singletons, and classes that force callers through a static factory such as Money.of(...).
How I’d say it in the interview
"Access modifiers yes, including private for factories and singletons. Final, static and abstract no: constructors aren't inherited or overridden. No return type; add void and it's a method, not a constructor."
Signals depth
Mentioning that at the bytecode level a constructor is a method named <init> returning void, invoked with invokespecial, and that new already produced the reference.
Red flags
"A constructor returns the object."
Follow-ups interviewers ask next
What if a class has only a
privateconstructor? Can you subclass it?Not from outside the class: the subclass constructor must call
super(...), which it can't access. A nested class inside it can, which is how some sealed-style hierarchies worked beforesealed.
What is a concrete class? Can an abstract class have a constructor if you can't instantiate it?
Tests concrete vs abstract, interfaces, constructors
Show answerHide answer
Model answer
A concrete class is one you can instantiate with new: every method has an implementation, inherited or its own. An abstract class can't be instantiated directly, and an interface never can.
An abstract class can and often does have constructors. They run when a concrete subclass is created, through super(...), to initialise the abstract parent's fields. Interfaces have no constructors and no instance fields.
abstract class Notification {
protected final String recipient;
protected Notification(String recipient) { this.recipient = recipient; }
abstract void send();
}
class EmailNotification extends Notification {
EmailNotification(String to) { super(to); }
@Override void send() { System.out.println("email to " + recipient); }
}
How I’d say it in the interview
"Concrete means instantiable, with no abstract methods left. Abstract classes still have constructors; they run as part of creating a subclass object, via super."
Signals depth
Mentioning that new Runnable() { ... } doesn't instantiate an interface: it creates an anonymous concrete class that implements it.
Red flags
"Abstract classes can't have constructors because you can't create them."
Follow-ups interviewers ask next
What if the abstract constructor calls
send()?That calls the subclass override before the subclass's fields are initialised. It's the same trap as the "overridable method in a constructor" question further down.
What's the difference between this and this()? Can a constructor call both this() and super()?
Tests this reference, constructor chaining
Show answerHide answer
Model answer
this is a reference to the current object; you use it to reach fields shadowed by parameters, pass yourself to other code, or return yourself for chaining. this(...) is a call to another constructor of the same class, used to keep construction logic in one place.
this(...) and super(...) must each be the first statement of a constructor in Java 21, so a constructor can use one or the other, never both. The constructor at the end of a this(...) chain is the one that calls super, and field initialisers run only there.
How I’d say it in the interview
"this is the current object; this() delegates to another constructor. Only one of this() or super() can appear, and it must come first, so the chain always ends in exactly one super call."
Signals depth
Saying field initialisers are compiled only into constructors that call super, so chaining never runs them twice. Mentioning that Java 25 (JEP 513) allows statements before this(...)/super(...) that don't touch this.
Red flags
"this() creates a new object."
Follow-ups interviewers ask next
What if
thisappears inside a lambda written in an instance method?The enclosing object. Lambdas don't introduce a new scope, so
thisandtoString()refer to the class the lambda is written in.What if it is inside an anonymous class instead?
The anonymous object itself. To reach the outer object you write
Outer.this, which reads a hidden field the compiler adds.
What does this print? Think about initialisation order.
Tests static and instance initialisation order
Show answerHide answer
Model answer
class Base {
static { System.out.println("Base static"); }
{ System.out.println("Base block"); }
Base() { System.out.println("Base ctor"); }
}
class Card extends Base {
static { System.out.println("Card static"); }
private final String label = log("Card field");
{ System.out.println("Card block"); }
Card() { System.out.println("Card ctor"); }
static String log(String s) { System.out.println(s); return s; }
}
new Card();
new Card();
The first new Card() prints Base static, Card static, Base block, Base ctor, Card field, Card block, Card ctor. The second prints only the last five: static initialisation happens once per class. The diagram shows the order.
Order of execution for two new Card() calls: class initialisation runs Base static then Card static once; each instance then runs Base's instance block and constructor, then Card's field initialiser, instance block and constructor
Rules behind it: the parent class initialises before the child; for each instance, super() runs first, then that class's field initialisers and instance blocks in textual order, then the rest of the constructor body.
How I’d say it in the interview
"Statics first, parent before child, once. Then per object: the parent's instance part completes before the child's, and within a class it's field initialisers and instance blocks in source order, then the constructor body."
Signals depth
Explaining that javac copies field initialisers and instance blocks into each <init> right after the super() call.
Red flags
"Constructor runs before the instance block" or putting Card static before Base static.
Follow-ups interviewers ask next
What if
labelis declared after the instance block?Then
Card blockprints beforeCard field. Textual order decides.
What are all the ways to create an object in Java? Which ones don't call the constructor?
Tests new, reflection, clone, deserialization, factories
Show answerHide answer
Model answer
new, plus static factories, builders and constructor references (Order::new), which all callnew.- Reflection:
getDeclaredConstructor(...).newInstance(...)or aMethodHandlefromfindConstructor. clone().- Deserialization (Java serialization).
The first two groups run a constructor. clone() copies fields from an existing object without any constructor. Java deserialization runs only the no-arg constructor of the first non-serializable superclass, so the serializable class's own constructor and its validation are skipped. Records are the exception: they're deserialized through their canonical constructor.
How I’d say it in the interview
"New, factories, builders, reflection and method handles all end in a constructor. Clone and Java deserialization don't: clone copies fields, and deserialization only calls the first non-serializable parent's no-arg constructor."
Signals depth
Mentioning that Class.newInstance() has been deprecated since Java 9, that reflective failures arrive wrapped in InvocationTargetException, and that Unsafe.allocateInstance skips constructors entirely.
Red flags
"Deserialization calls the default constructor of the class."
Follow-ups interviewers ask next
What if you need a copy of an object?
Prefer a copy constructor or a static
copyOffactory: it runs your validation and makes deep versus shallow copying explicit.clone()needsCloneable, is shallow by default and bypasses constructors.
What happens step by step when you write new Order(42)?
Tests new bytecode, TLAB, zeroing, header, constructor chain
Show answerHide answer
Model answer
In bytecode it's new, dup, push arguments, invokespecial Order.<init>, store. In HotSpot that becomes six steps:
- Class check: resolve
Order; load, link and initialise it on first use. - Allocate: bump the pointer in this thread's TLAB by the object size, no lock.
- Zero: every field gets its default (
0,false,null). - Header: write the mark word and the class pointer.
- Constructor chain:
<init>runs, callingsuper()up toObject, then initialisers and body on the way down. - Publish: the reference is stored in your variable.
How I’d say it in the interview
"The class gets initialised if needed, memory comes from a pointer bump in the thread-local allocation buffer, fields are zeroed, the header is written, the constructor chain runs from Object down, and only then does my variable get the reference."
Signals depth
Explaining the dup (one reference consumed as this by invokespecial, one kept for the caller) and noting that the JIT can remove the allocation entirely through escape analysis.
Red flags
"The constructor allocates the memory."
Follow-ups interviewers ask next
What if the constructor throws?
The memory was already allocated and zeroed, but the reference is never stored, so the object becomes garbage, unless the constructor leaked
thisfirst.
Explain private, package-private, protected and public. Can a top-level class be private?
Tests access modifiers, protected across packages
Show answerHide answer
Model answer
private is visible only inside the top-level class that declares it (including its nested classes). Package-private (no modifier) adds the rest of the package. protected adds subclasses in other packages, with a catch. public is visible everywhere the module exports the package. The grid below shows who can see what.
Access grid: private is visible only within the same class; package-private adds the same package; protected adds subclasses in other packages but only through their own type; public is visible everywhere
The catch: a subclass in another package can access a protected member only through references of its own type (or a subtype), not through a plain parent reference.
package com.shop.base;
public class Account { protected long balance; }
package com.shop.app;
class Savings extends com.shop.base.Account {
void move(com.shop.base.Account other, Savings mine) {
long a = this.balance; // ok
long b = mine.balance; // ok
long c = other.balance; // compile error: balance has protected access
}
}
A top-level class can only be public or package-private. private and protected are allowed on nested classes.
How I’d say it in the interview
"Private is the class, default is the package, protected is package plus subclasses, public is everyone. Top-level classes are public or package-private. And protected across packages only works through the subclass's own type."
Signals depth
The other.balance rule (JLS 6.6.2), and noting that since Java 9 public alone doesn't make a type visible outside its module unless the package is exported.
Red flags
"Protected means only subclasses" (forgetting the same package) or "default means public".
Follow-ups interviewers ask next
What if you override a
protectedmethod and make itprivate?Compile error: an override can't reduce visibility. It may widen it to
public.
When would you use a static factory method or a builder instead of a constructor?
Tests Effective Java items 1 and 2, API design
Show answerHide answer
Model answer
A static factory helps when a name explains intent (Money.ofCents(500) versus Money.ofRupees(5)), when you want to cache or reuse instances (Integer.valueOf, List.of()), when you want to return a subtype or hide the implementation class, or when the object must be fully built before a side effect such as registration.
A builder helps when there are many parameters, especially optional ones, or several of the same type that are easy to swap. It names each argument and validates once in build().
I default to constructors for small, always-valid classes; a factory when naming, caching or post-construction steps matter; and a builder past about four or five parameters.
How I’d say it in the interview
"Constructors for small simple classes. Static factories when I need a name, caching, a subtype, or a safe place to publish the object after it's built. Builders when the parameter list gets long or has optional fields."
Signals depth
Mentioning that factories can't be subclassed through (a class with only private constructors is effectively final), and the self-type generic trick for builders that subclasses extend.
Red flags
"Builders are always better" or "factories are only for singletons".
Follow-ups interviewers ask next
What if the builder is used in a loop running 10 million times?
It's one extra short-lived object per iteration. Escape analysis often removes it once the code is hot and inlined; if profiling shows it doesn't, a direct constructor in that hot path is fine.
You added a constructor with parameters to an entity, and now the app fails at startup with "No default constructor for entity". What happened?
Tests default constructor, JPA, Jackson
Show answerHide answer
Model answer
Adding any constructor removed the compiler-generated no-arg constructor. JPA providers instantiate entities reflectively through a no-arg constructor (the spec requires it to be public or protected), then fill fields. Jackson does the same by default unless you annotate a constructor with @JsonCreator or compile with -parameters and its parameter-names module.
The fix is to declare the no-arg constructor explicitly, protected so application code still uses the meaningful one:
@Entity
public class Shipment {
@Id private Long id;
private String trackingCode;
protected Shipment() {} // for JPA
public Shipment(String trackingCode) { this.trackingCode = trackingCode; }
}
How I’d say it in the interview
"Declaring a constructor removed the default one, and JPA needs a no-arg constructor to create entities reflectively. I add a protected no-arg constructor so the framework is happy but callers still use the real one."
Signals depth
Knowing JPA's public or protected requirement, that Hibernate also needs the class non-final for lazy proxies, and the Jackson alternatives (@JsonCreator, records).
Red flags
"Make all fields public and remove the constructor."
Follow-ups interviewers ask next
What if it's a record you're deserializing with Jackson?
Jackson (2.12+) uses the record's canonical constructor, so no no-arg constructor is needed and your compact-constructor validation runs.
What does this print when a parent constructor calls an overridden method?
Tests overridable call in constructor, initialisation order
Show answerHide answer
Model answer
class Notifier {
Notifier() { send(); }
void send() { System.out.println("base send"); }
}
class SmsNotifier extends Notifier {
private String phone = "+91-98000-00000";
private int retries = 3;
@Override void send() { System.out.println("sms to " + phone + ", retries " + retries); }
}
new SmsNotifier(); // prints: sms to null, retries 0
Notifier() runs first, as super() inside the SmsNotifier constructor, and calls send() through virtual dispatch, which picks the override. At that moment SmsNotifier's field initialisers haven't run yet, so the fields still hold their zeroed defaults.
How I’d say it in the interview
"It prints null and 0. The parent constructor calls an overridden method before the child's field initialisers run, so the override sees default values. That's why constructors shouldn't call overridable methods."
Signals depth
Mentioning that javac -Xlint:this-escape (Java 21) can warn about this, and that making send private or final (or the class final) removes the trap.
Red flags
"base send", or "sms to +91..., retries 3".
Follow-ups interviewers ask next
What if
phoneisprivate final String phone = "+91-98000-00000";?Then it prints the number, because a
finalfield initialised with a constant expression is a constant variable andjavacinlines the literal intosend().retrieswould still be0unless it'sfinaltoo. That twist is why I ask this one.
Your event listener registers itself in its constructor. Under load, it sometimes throws NullPointerException on a field that the constructor sets. Walk me through it.
Tests this escape, safe publication, JLS 17.5
Show answerHide answer
Model answer
The constructor published this before it finished. Something like bus.register(this) runs, then the fields are assigned. If the bus delivers an event on another thread in that window, onEvent reads the field while it's still null. Tests miss it because test buses are usually single-threaded.
Moving register(this) to the last line narrows the window but isn't a real fix: the Java Memory Model guarantees other threads see correct final field values only if this doesn't escape during construction (JLS 17.5). The fix is to separate building from publishing:
public static PriceListener start(EventBus bus, String symbol) {
PriceListener l = new PriceListener(symbol); // private ctor, fields only
bus.register(l); // publish the finished object
return l;
}
How I’d say it in the interview
"this escaped from the constructor, so the bus thread saw a half-built object. Reordering lines isn't enough because final-field guarantees need the constructor to finish first. I make the constructor pure and register from a static factory."
Signals depth
Pointing out hidden escapes: creating an inner class or a lambda that uses instance state inside the constructor and handing it out, or starting a thread there.
Red flags
"Add volatile to every field" or "add a Thread.sleep".
Follow-ups interviewers ask next
What if the constructor starts a background thread that uses
this?Same bug.
Thread.start()does create a happens-before edge for everything the constructor did before it, but anything assigned afterstart()is racy, and subclass fields aren't set yet. Start threads in a separatestart()method after construction.
An Order loaded from a Redis cache has a negative amount, but the constructor throws on negative amounts. How is that possible?
Tests deserialization skips constructors, readObject, records
Show answerHide answer
Model answer
The cache uses Java serialization, and deserialization doesn't run Order's constructor. It allocates the object, runs the no-arg constructor of the first non-serializable superclass (Object), then writes field values from the bytes. Old cache entries written before the validation existed, or a tampered stream, produce an Order that skipped the check.
Fixes, in order of preference: switch the cached type to a record (deserialized through its canonical constructor) or to JSON bound through a constructor; or add a readObject that re-validates and throws InvalidObjectException; or use the serialization proxy pattern with readResolve.
How I’d say it in the interview
"Java deserialization bypasses the class's constructors, so validation never ran on that object. I'd move to records or constructor-based JSON, or at minimum re-check invariants in readObject."
Signals depth
Mentioning that the same bypass is the root of deserialization security bugs, and JEP 290 serialization filters (ObjectInputFilter) as defence.
Red flags
"The JVM must have a bug" or "make the field final to fix it" (deserialization sets final fields too).
Follow-ups interviewers ask next
What if the class is
Externalizableinstead?Then the public no-arg constructor runs first, followed by
readExternal, so validation must live inreadExternal.
Heap dumps show thousands of request handler objects that should have been garbage collected. They're all kept alive by an event bus. What's going on?
Tests anonymous classes, Outer.this, memory leaks
Show answerHide answer
Model answer
Each handler registered an anonymous inner class listener with a long-lived event bus. An anonymous class created in an instance method holds a hidden reference to its enclosing instance (this$0) whenever it uses outer state, so the bus keeps the listener alive, the listener keeps the handler alive, and the handler keeps its whole request graph alive. Nobody ever unregisters.
Fixes: unregister when the handler is done (try/finally or a close hook), register a static nested class or a lambda that captures only the data it needs, or use a bus that holds listeners weakly.
How I’d say it in the interview
"The anonymous listener captured the enclosing handler through Outer.this. The bus is long-lived, so it pins every handler. I'd unregister on completion and make the listener capture only what it needs."
Signals depth
Reading the leak path in the heap dump's "path to GC roots" (EventBus -> listeners -> Handler$1 -> this$0 -> Handler). Knowing that a lambda captures this only when its body uses instance members.
Red flags
"Call System.gc()" or "increase the heap".
Follow-ups interviewers ask next
What if you switch to a lambda that calls
this.process(event)?It still captures
this, so the leak remains. The real fix is the registration lifetime, not the syntax.
Profiling shows a hot method allocating 2 GB/s of small objects. A teammate says "new is expensive, let's pool them". What do you say?
Tests TLAB allocation, GC cost model, escape analysis
Show answerHide answer
Model answer
Allocation itself is cheap: a TLAB pointer bump costs a few nanoseconds, and young-generation GC cost scales with the objects that survive, not the ones that die. Pooling small objects usually makes things worse: pooled objects live long, get promoted to the old generation, need resetting, and add contention.
What I'd actually check: GC logs or JFR for young-pause frequency and promotion rate; whether objects survive longer than they should (held in a batch or cache); and whether the JIT already scalar-replaces them. Then cut allocation at the source: primitives instead of boxed values, presized collections, reusing a StringBuilder inside a method.
How I’d say it in the interview
"New is a pointer bump; what costs is survival. I'd look at GC logs and JFR allocation profiles before pooling, because pools make objects old and that's the expensive case. Usually the win is removing boxing or accidental retention."
Signals depth
Mentioning escape analysis and scalar replacement, and that pools still make sense for expensive resources: connections, threads, large direct buffers.
Red flags
"Java allocation is slow, always pool" or "add more heap and move on".
Follow-ups interviewers ask next
What if the allocations are 50 MB byte arrays?
That's different: large arrays may bypass the TLAB, and in G1 anything of half a region or more is a humongous object allocated in the old generation. Reusing or chunking those buffers is worth it.