Abstract classes & interfaces: interview questions & scenarios
These are the abstract class and interface questions I ask most, from the warm-up "can an abstract class have a constructor?" to the senior follow-ups about default-method conflicts, sealed hierarchies and hot interface call sites. They look like vocabulary questions, but every one of them has a "why" behind it, and the why is what I'm grading.
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'd ask next, so treat them as part of the question. If one stumps you, go back to the lesson: abstract classes, interfaces, static and default methods and sealed classes and interfaces.
Can an abstract class have a constructor? If nobody can instantiate it, what is the constructor for?
Tests abstract classes, constructor chaining, object initialisation
Show answerHide answer
Model answer
Yes. You can't write new PaymentProcessor(...), but every subclass object contains the abstract class's fields, and something has to initialise them. When you call new CardProcessor(), the JVM allocates one object for the whole hierarchy, then runs constructors top-down through super(...): Object(), then the abstract class constructor, then the subclass constructor.
So the constructor is where the abstract class enforces its invariants: required arguments, null checks and default state. If its only constructor takes a String name, every subclass is forced to supply a name.
public abstract class PaymentProcessor {
private final String name;
protected PaymentProcessor(String name) {
this.name = Objects.requireNonNull(name);
}
}
final class CardProcessor extends PaymentProcessor {
CardProcessor() { super("card"); }
}
This is the construction order I want you to picture when you explain it.
The diagram shows one CardProcessor object whose construction runs Object first, then the abstract parent, then the concrete child.
How I’d say it in the interview
"Yes. It can't be instantiated directly, but its constructor runs as part of every subclass construction through the super chain, because the subclass object contains the parent's fields. It's where the abstract class enforces its invariants."
Signals depth
Explaining one object with fields from the whole hierarchy, making the constructor protected to document intent, and mentioning that new AbstractList<>() { ... } creates an anonymous concrete subclass, not an instance of the abstract class.
Red flags
"No, because you can't instantiate it." Also: "it has a constructor only so the compiler is happy".
Follow-ups interviewers ask next
What if the abstract class has no abstract methods at all?
That's legal:
abstract class BaseEntity { protected Instant createdAt = Instant.now(); }compiles.abstracton a class only means "no direct instances". People use it for base classes that make no sense alone.What if I make that abstract class's constructor private?
Then only nested classes inside it can extend it, because they can call the private constructor as nestmates. It's an old trick for a closed hierarchy; since Java 17,
sealedsays the same thing explicitly and the compiler can use it for exhaustiveness.
Why is abstract illegal together with final, static or private?
Tests modifier rules, overriding
Show answerHide answer
Model answer
abstract means "a subclass will provide the implementation". Each of the other modifiers makes that impossible:
finalforbids overriding, or subclassing when used on a class.staticmethods are hidden, not overridden, so dynamic dispatch cannot reach a subclass version.privatemethods are not inherited, so no subclass can see the method to implement it.
javac reports an illegal combination of modifiers. abstract synchronized and abstract native are rejected too, because both describe an implementation that does not exist.
How I’d say it in the interview
"Abstract says a subclass must implement it. Final, static and private each stop a subclass from overriding it, so the combination could never be satisfied."
Signals depth
Explaining static as hiding rather than overriding, and noting that an abstract class can still have final, static and private concrete methods.
Red flags
Saying "an abstract class can't have static methods" or "an abstract class can't have final methods".
Follow-ups interviewers ask next
What if an interface declares a
privatemethod? Isn't that the same contradiction?No, because an interface
privatemethod, since Java 9, must have a body. It's a concrete helper for default methods, not an abstract member.private abstractis illegal in interfaces too.What if an abstract class is itself
final?That is illegal for the same reason.
abstract classasks for subclasses, whilefinal classforbids them.
Does this compile? If not, why?
Tests implicit interface modifiers, overriding access
Show answerHide answer
Model answer
No. Interface methods without a body are implicitly public abstract, so FlatDiscount.apply is package-private and tries to reduce visibility: attempting to assign weaker access privileges; was public. Add public to the implementation.
MAX_DISCOUNT is fine: interface fields are implicitly public static final, so it is a constant shared by everyone, not per-object state.
How I’d say it in the interview
"It fails because interface methods are implicitly public, and the implementation is package-private, which narrows access. Making it public fixes it. The field is implicitly public static final."
Signals depth
Listing the full set: methods public abstract, fields public static final, nested types public static, the interface itself abstract; and knowing protected or final on an interface method is a compile error.
Red flags
"It compiles; the default access is fine." Or thinking MAX_DISCOUNT is an instance field each implementation can change.
Follow-ups interviewers ask next
What if another module changes
MAX_DISCOUNTto 30 and only that jar is redeployed?Classes that read
MAX_DISCOUNTstill use 50. It is a compile-time constant, sojavaccopied the value into each caller's bytecode. That's one reason constants in interfaces, and the constant interface anti-pattern, cause trouble.What if a class implements an interface only to use its constants?
That's the constant interface anti-pattern:
class OrderDao implements DbConstantsleaksDbConstantsinto the public type. Use afinalutility class or an enum, plusimport staticif you want short names.
Can an interface have constructors, fields with state, private methods and static methods?
Tests interface members, Java 8 and Java 9 features, instance state
Show answerHide answer
Model answer
An interface cannot have constructors or instance fields. Its fields are always public static final, so they are constants, not per-object state. Since Java 8 it can have default and static methods; since Java 9 it can have private helper methods used by other interface methods.
The important boundary is instance state. If you need each object to carry data owned by the base type, that points toward an abstract class or composition.
How I’d say it in the interview
"No constructors and no instance fields. Interface fields are public static final constants. Defaults and statics arrived in Java 8, private helpers in Java 9, but none of that gives the interface per-object state."
Signals depth
Separating private interface methods from private class methods, and knowing static interface methods are called on the interface name and are not inherited by implementations.
Red flags
"Interfaces can have fields now", or "private methods in interfaces are abstract".
Follow-ups interviewers ask next
What if a default method needs shared helper code?
Put the helper in a private interface method if it only serves defaults in the same interface. Use a static method if callers or other types should call it by name.
What if two interfaces define the same static method?
There is no conflict for the implementing class because static interface methods are not inherited. You call
Left.parse(...)orRight.parse(...)explicitly.
Your notification service started printing channel = null in prod after a refactor moved logging into an abstract constructor. Walk me through it.
Tests constructor order, dynamic dispatch during construction
Show answerHide answer
Model answer
It prints channel = null. The Notifier constructor runs before SmsNotifier's field initialisers. The call to channel() still dispatches to SmsNotifier.channel(), because virtual dispatch uses the object's real class even during construction, but the field still holds its zeroed default.
The production fix is to remove the overridable call from the constructor. Pass data up instead: Notifier(String channel) and super("sms"), or do logging after the object is fully built.
How I’d say it in the interview
"Null. The parent constructor runs before the child's field initialisers, but the call dispatches to the child anyway, so it reads uninitialised state. Constructors should not call overridable methods."
Signals depth
Knowing that dispatch works on the real class during construction, unlike C++, and tying the incident to the Effective Java rule: avoid overridable method calls from constructors.
Red flags
"sms", or "it throws AbstractMethodError".
Follow-ups interviewers ask next
What if the field is
private final String channel = "sms";?Then many candidates will see
channel = sms, because afinalfield initialised with a constant expression is a constant variable andjavaccan inline the literal intochannel(). Change it tofinal String channel = String.valueOf("sms")and you are back tonull.What if the prod log only happens under one subclass?
I would inspect the subclass initialisation path, not the abstract constructor alone. The bug shows up when the override reads fields initialised after
super(...), opens sockets, reads injected collaborators, or touches state populated by a framework later.
Abstract class or interface: since Java 8 interfaces can have method bodies, how do you choose?
Tests abstract class vs interface, default methods, design
Show answerHide answer
Model answer
The difference that still matters is state and identity, not "can it have code". An abstract class can have instance fields, constructors, protected and final methods, and a class can extend only one. An interface has no instance state and no constructors, all non-private members are public, and a class can implement many.
My rule: use an abstract class when related types share state or construction rules that must be enforced in one place, or when you need a final template method. Use an interface for a capability that unrelated classes may have alongside others, like Comparable or AutoCloseable. Often the best design is both: a small interface as the public type plus an optional skeletal abstract class, like List and AbstractList.
How I’d say it in the interview
"Interfaces can have defaults now, so I decide on state. Shared fields, constructor invariants or a final template method mean an abstract class. A capability that unrelated classes mix in means an interface. For libraries I usually ship both: the interface as the type and an abstract skeleton for convenience."
Signals depth
Mentioning that defaults cannot override Object methods or be final, that interface calls use invokeinterface, and Effective Java's "prefer interfaces to abstract classes" with the skeletal implementation pattern.
Red flags
"Interfaces can't have method bodies" (pre-Java 8), or "abstract classes are faster".
Follow-ups interviewers ask next
What if you need shared mutable state but also multiple inheritance?
Then inheritance is the wrong tool. Put the state in a separate class and use composition: the interfaces define the types, and each implementation holds a helper object that owns the state.
What if you are designing a public library API?
Prefer the interface as the public dependency point. You can add abstract skeletal classes as convenience, but forcing users into your base class spends their single
extendsslot.
A class implements two interfaces that both define default String tag(). What happens, and how do you fix it?
Tests default method conflicts, X.super syntax
Show answerHide answer
Model answer
It does not compile: types Logger and Metrics are incompatible; class Telemetry inherits unrelated defaults for tag(). Neither interface is more specific than the other, so the class must override the method. Inside the override, Interface.super.method() calls a specific parent's default:
interface Logger { default String tag() { return "log"; } }
interface Metrics { default String tag() { return "metrics"; } }
class Telemetry implements Logger, Metrics {
@Override public String tag() {
return Logger.super.tag() + "+" + Metrics.super.tag();
}
}
The resolution rule is short enough to draw on a whiteboard.
The diagram shows Java resolving a method by checking class methods first, then the most specific interface default, then requiring an explicit override for unrelated defaults.
How I’d say it in the interview
"Compile error, because the defaults are unrelated. I override tag() in the class and pick one, or combine them, with Logger.super.tag()."
Signals depth
Stating the full rule order: class wins, then most specific interface, then explicit override; and knowing Logger.super.tag() compiles to invokespecial on an interface method.
Red flags
"The first interface in the implements list wins", or "Java picks one at random".
Follow-ups interviewers ask next
What if
MetricsextendsLoggerand overridestag()?Then there is no conflict:
Metrics.tag()is more specific and wins, even if the class listsLoggerexplicitly.What if , in that case, I write
Logger.super.tag()to reach the original?It fails:
bad type qualifier Logger in default super call. You cannot skip past a more specific override that another direct superinterface provides.
What does this print?
Tests class wins rule, static interface methods
Show answerHide answer
Model answer
base to u1
dropped to u2
The class wins. OrderService inherits send from BaseService and a default send from Notifier, and a method from the superclass chain always beats an interface default, even though BaseService has never heard of Notifier.
The second line goes through the static factory. The lambda implements channel(), the only abstract method, and the default send builds "dropped to u2".
How I’d say it in the interview
"First line is base to u1, because a superclass method beats an interface default. Second is dropped to u2: the lambda only implements channel, so the default send is used."
Signals depth
Stating that the rule also covers abstract superclass methods: an abstract BaseService.send would force OrderService to implement it, and the default would not fill the gap. Defaults do not count toward a functional interface's single abstract method.
Red flags
"email to u1", or claiming it is ambiguous and will not compile.
Follow-ups interviewers ask next
What if I call
OrderService.noop()?Compile error:
cannot find symbol. Static interface methods are not inherited by implementing classes or sub-interfaces, not even through an instance. You must writeNotifier.noop().What if I ask why interface statics are not inherited when class statics are?
A class can implement many interfaces, so inheriting their statics would create name clashes for little benefit. Keeping them on the interface makes every call site say exactly which type owns the factory, like
List.oforComparator.comparing.
Your API starts throwing NotSerializableException for cached user sessions after a release. Why does Serializable matter if it has no methods?
Tests marker interfaces, annotations, RandomAccess, transient fields
Show answerHide answer
Model answer
Serializable is a marker interface: it gives the class a type that library code can check with instanceof. ObjectOutputStream.writeObject throws NotSerializableException when the object graph reaches a non-serializable object, unless that field is marked transient.
A marker interface defines a type, so the compiler can enforce it in method signatures. An annotation can carry values and mark methods, fields or packages, but it does not make cache(Object) refuse the wrong object at compile time. My rule: if you would ever want a parameter of that type, use a marker interface; otherwise, an annotation.
How I’d say it in the interview
"Serializable has no methods, but it is still a type. The serialization library checks that type at runtime, and the compiler can use it in APIs. For metadata with values, I would use an annotation instead."
Signals depth
Naming RandomAccess and what it changes in Collections, and knowing that Cloneable is another marker interface whose meaning is enforced by library code, not magic syntax.
Red flags
"It tells the JVM to serialize the object" (the JVM does not; library code checks it), or "marker interfaces were replaced by annotations".
Follow-ups interviewers ask next
What if the session class is
Serializablebut one field's type is not?Serialising the object fails when traversal reaches that field. Mark the field
transient, replace it with a serializable DTO, or avoid Java serialization for that cache format. See the transient keyword.What if I only need to mark a method for a framework?
Use an annotation. Marker interfaces only mark classes and define types; annotations can target methods and carry configuration.
Your prod deployment starts failing with IncompatibleClassChangeError: Conflicting default methods after two dependency jars upgraded. Walk me through it.
Tests interface evolution, binary compatibility, IncompatibleClassChangeError
Show answerHide answer
Model answer
Default methods let interfaces evolve without breaking every implementer. That is how the JDK added stream(), forEach(), removeIf() and Map.getOrDefault() with bodies written in terms of existing methods.
The catch is conflicts. Adding a default is binary compatible, so old class files can load, but if a class implements two interfaces that each add default String describe() in separately compiled jars, calling describe() can throw IncompatibleClassChangeError. Recompiling turns it into a compile error and forces the class to override.
How I’d say it in the interview
"Defaults made interface evolution possible, but a new default is still an API change. If two upgraded interfaces add the same default, an old implementation can fail at runtime with IncompatibleClassChangeError. I would recompile, override the method, and treat the default name as part of the compatibility surface."
Signals depth
Distinguishing source compatibility from binary compatibility, naming IncompatibleClassChangeError, and citing a real JDK example like JEP 431's reversed() return-type pressure across collection interfaces.
Red flags
"Default methods are always safe to add", or confusing default methods with abstract class methods.
Follow-ups interviewers ask next
What if a default method is slow for my implementation?
Override it. Defaults are correct generic implementations written against the abstract methods.
ArrayListoverridesremoveIfwith one O(n) compaction pass instead of repeatediterator().remove()calls.What if the old class file has not been recompiled?
That is where binary compatibility shows its edge. The class may link until the conflicting method is selected, then fail. Recompile in CI so the conflict becomes a compile-time error before production.
Model a payment that can be pending, authorised with an amount and auth code, captured, or failed with a reason, so the compiler catches a missing case.
Tests sealed interfaces, records, exhaustive switch
Show answerHide answer
Model answer
Use a sealed interface with one record per state. Records are implicitly final, each carries its own data, and a switch over the sealed type is checked for exhaustiveness:
sealed interface PaymentState permits Pending, Authorized, Captured, Failed { }
record Pending(String orderId) implements PaymentState { }
record Authorized(String orderId, BigDecimal amount, String authCode) implements PaymentState { }
record Captured(String orderId, BigDecimal amount) implements PaymentState { }
record Failed(String orderId, String reason) implements PaymentState { }
static String describe(PaymentState s) {
return switch (s) {
case Pending p -> "waiting for " + p.orderId();
case Authorized a -> "authorised " + a.amount();
case Captured c -> "captured " + c.amount();
case Failed f -> "failed: " + f.reason();
};
}
An enum does not fit well, because each constant is a singleton with the same fields; here every state carries different data.
How I’d say it in the interview
"Sealed interface plus a record per state. Records carry state-specific data, and a switch without default is exhaustive, so adding a new state is a compile error everywhere it is not handled."
Signals depth
Explaining why not an enum, mentioning record patterns where available, and the same-module or same-package rule for permitted subclasses.
Red flags
A String or int status field with if chains, or an open interface plus instanceof checks and a default branch.
Follow-ups interviewers ask next
What if all the records live in the same file as the interface?
Then you can drop the
permitsclause; the compiler infers it from the subtypes in that compilation unit. Nesting the records inside the interface is a common compact style.What if another team needs to add their own payment states?
Then the set is not closed, and sealing is the wrong tool. Either make one permitted subtype
non-sealedas an extension point, or use a plain interface and accept that switches need adefault.
An interviewer says, "interfaces are contracts and abstract classes are partial implementations." What is missing from that answer?
Tests contracts, skeletal implementations, multiple inheritance limits
Show answerHide answer
Model answer
It is a useful starting sentence, but it misses the design trade-off. Interfaces are contracts because they define a type that unrelated classes can implement, and Java lets a class implement many. Abstract classes are partial implementations, but they also own construction rules, state, protected hooks and final template methods.
The stronger answer is to talk about API shape. Public APIs usually depend on the interface; shared code can live in a skeletal abstract class for implementations that want it. That gives callers flexibility without forcing every implementer into the same superclass.
How I’d say it in the interview
"The contract versus partial implementation line is okay, but I would add state and inheritance cost. Interfaces give a public type and multiple implementation. Abstract classes give state, constructors and final template methods, but they consume the one superclass slot."
Signals depth
Mentioning skeletal implementations, constructor invariants, protected extension points and the ability to evolve interfaces with default methods carefully.
Red flags
Treating the choice as syntax only, or saying one is always better.
Follow-ups interviewers ask next
What if an abstract class implements an interface but leaves methods unimplemented?
That is legal if the class is abstract. It can implement the common pieces, leave the rest for concrete subclasses, and still let callers depend on the interface.
What if the abstract class has a final template method?
Then subclasses cannot break the algorithm order. They fill in protected hooks, while the final method owns validation, metrics and transaction boundaries.
Your payment service added Refunded, but prod started hitting IllegalStateException inside sealed-type switches instead of failing in CI. Walk me through it.
Tests exhaustiveness, MatchException, PermittedSubclasses
Show answerHide answer
Model answer
Someone probably added default -> throw new IllegalStateException() to switches over the sealed type. That default turns off exhaustiveness checking. Without it, adding Refunded makes every incomplete switch a compile error. With it, the code compiles and fails at runtime.
The runtime safety is already there. For an exhaustive switch with no default, javac inserts a hidden fallback that throws MatchException in Java 21 if stale code sees a subtype it did not know about. The JVM also enforces the sealed hierarchy through the PermittedSubclasses attribute at class load.
How I’d say it in the interview
"The default threw away the compile-time check. For a sealed type, leave the default out so new permitted subtypes break the build. Stale binaries already fail with MatchException, so we do not need a manual catch-all."
Signals depth
Naming MatchException, the PermittedSubclasses attribute and the class-load check, and noting that guarded case ... when patterns do not count toward exhaustiveness.
Red flags
"A default is always good practice in switch statements", which is useful for open types and wrong for sealed ones.
Follow-ups interviewers ask next
What if one permitted subclass is declared
non-sealed?The switch over the sealed parent is still exhaustive with a
casefor that subclass, because any further subclasses are instances of it. You cannot switch exhaustively over the non-sealed class's own children.What if I call
getPermittedSubclasses()on a class that is not sealed?It returns
null, not an empty array. For a sealed type it returns the permitted classes, andisSealed()returnstrue.
Why can't an interface provide a default equals() or toString()?
Tests default methods, Object methods, resolution rules
Show answerHide answer
Model answer
Because it could never run. Every class extends Object, which already has equals, hashCode and toString, and under the class-wins rule any method from the class hierarchy beats an interface default. A default equals would be dead code, so javac rejects it.
An interface can still redeclare these methods as abstract to document a contract; Comparator does that with equals. It cannot supply a body.
How I’d say it in the interview
"Class wins: Object's version is always inherited through the class hierarchy, so a default would never be selected. The compiler rejects it to prevent dead code."
Signals depth
Tying the rule to the resolution order rather than memorising it, and mentioning that records or an abstract base class are the right place for shared equals and toString logic.
Red flags
"Because interfaces can't have method bodies", or "because Object methods are final". Only some Object methods are final, like getClass and wait.
Follow-ups interviewers ask next
What if I really want every implementation to share the same
toString()format?Use a default method with a different name, like
describe(), and have implementations call it, or move the shared logic into an abstract base class where overridingtoString()is allowed.What if the interface is a functional interface?
equals,hashCodeandtoStringstill do not count as its single abstract method, because every implementation already has them fromObject.
Your service got slower after adding several NotificationSender implementations behind one interface. Is calling through an interface slower than calling through a class?
Tests invokeinterface, itables, JIT inline caches, megamorphic calls
Show answerHide answer
Model answer
At the bytecode level, yes in principle; in production, the bigger issue is often lost inlining. A call through a class type is invokevirtual, which reads a fixed vtable slot. A call through an interface type is invokeinterface, where HotSpot may look through the class's itable to find the matching interface method.
The JIT usually removes the difference. A monomorphic call site gets an inline cache and is usually inlined. Bimorphic sites get two checks. Megamorphic sites, often 3 or more receiver classes, stop inlining and use a slower stub. If latency changed after adding implementations, I would inspect receiver type profiles and whether a hot call site became megamorphic.
How I’d say it in the interview
"invokeinterface has an itable lookup path, but the JIT makes monomorphic interface calls as fast as class calls. The real prod risk is a hot call site becoming megamorphic and losing inlining, not interfaces being slow by default."
Signals depth
Knowing the monomorphic, bimorphic and megamorphic distinction, class hierarchy analysis, and that the same performance cliff can happen with abstract classes if the call site sees many receiver classes.
Red flags
"Interfaces are slow, so use abstract classes for performance", or "there is no difference at all at the bytecode level".
Follow-ups interviewers ask next
What if the abstract class version has three hot implementations too?
Then it is megamorphic as well and can lose inlining. The fix for a truly hot site is restructuring: split call sites, move decisions earlier, use a sealed type with a
switch, or measure whether the site matters.What if only one implementation is loaded in production?
Class hierarchy analysis can devirtualise the call and inline it. That is why a benchmark with one implementation can show no interface cost, while a real routing path with many implementations behaves differently.