When do you choose an abstract class over an interface?
Not on the old answer. Since Java 8 an interface carries behaviour, and since 9 it has private helper methods, so 'interfaces have no implementation' is out of date. Two differences remain: an abstract class can hold instance state and run a constructor, and you get only one. Choose it when subtypes must share state or a construction invariant; otherwise the interface.
The Answer
- The textbook answer — "an interface has no implementation" — has been wrong since Java 8. Interfaces have
defaultmethods,staticmethods, and Java 9+privatemethods. - What is left is short. An abstract class can have instance state and a constructor; an interface can have neither. An interface's fields are always
public static final. - And you can extend one class but implement many interfaces. That is the constraint that usually decides it.
- Choose an abstract class when subtypes must share mutable state, or when construction has an invariant every subtype must run through.
- Choose an interface for a capability, for anything that might be mixed into an unrelated hierarchy, and as the default for a public API.
- Inheriting two conflicting defaults is a compile error you must resolve with
Interface.super.method(). But a class implementation always wins over an interface default, silently.
Understand It
What an interface can do now
Every Java 8 and 9 addition in one declaration, all of it running:
Auditable simple = Auditable.of("ORD-9");
System.out.println("constant : " + Auditable.CHANNEL);
System.out.println("static factory gave : " + simple.id());
System.out.println("default + private : " + simple.summary());
System.out.println("it is a lambda : " + Auditable.class.isInterface());constant : audit
static factory gave : ORD-9
default + private : [audit] ORD-9
it is a lambda : truesummary() is a default method that calls prefix(), a private interface method. The implementation is a lambda — one abstract method, so Auditable is a functional interface — and it still inherits behaviour it never declared.
So "interfaces are just contracts" is out of date by more than a decade. The question is what genuinely remains.
The two differences that actually remain
State. An abstract class can have instance fields, and they can be mutable and protected. An interface cannot have instance fields at all — every field is implicitly public static final, whether you write the modifiers or not:
Field channel = Auditable.class.getDeclaredField("CHANNEL");
int m = channel.getModifiers();
System.out.println("declared as just `String CHANNEL = \"audit\";`");
System.out.println(" public : " + Modifier.isPublic(m));
System.out.println(" static : " + Modifier.isStatic(m));
System.out.println(" final : " + Modifier.isFinal(m));declared as just `String CHANNEL = "audit";`
public : true
static : true
final : trueTry to assign to one and the compiler tells you exactly what it is:
error: cannot assign a value to static final variable count
A default method therefore has no per-instance state to work with. It can only call other methods on the interface. That is the hard limit, and it is why default methods are good at deriving things and bad at holding them.
Construction. An abstract class has a constructor, which runs before any subclass body, so it can enforce an invariant every subtype must pass through. An interface has no constructor, so it cannot.
Invoice invoice = new Invoice("anita");
System.out.println("state set by the abstract constructor : " + invoice.summary());state set by the abstract constructor : owned by anitaThose two — state and construction — are the whole list. Everything else you may have learned as a difference either went away in Java 8 or was never true.
The class always wins, and it does so silently
Invoice extends Document, which has a concrete summary(), and it also implements Timestamped, which has a default summary(). Two inherited implementations, one signature, and it compiled without complaint. The output above shows which one ran: the class's.
This is a real rule with a name — the class always wins — and it exists so that adding a default method to an interface can never change the behaviour of an existing class that already had that method. That is the guarantee that let the JDK add forEach to Iterable without breaking the world.
It is also a trap. Adding Timestamped to a class that already has summary() looks like it does something and does nothing, with no warning. If you meant the interface's version, you have to say so explicitly.
The diamond is a compile error, on purpose
Two interfaces with the same default and no class implementation is the case Java refuses to guess about:
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B { }
error: types A and B are incompatible;
class C inherits unrelated defaults for hello() from types A and B
You must resolve it, and the syntax for reaching a specific super-interface is Interface.super.method():
System.out.println(new Receipt().summary());[audit] ORD-1 / at 09:00This is the answer to "does Java have multiple inheritance?" and it is worth being precise: Java has multiple inheritance of behaviour, but not of state. Receipt inherits two implementations of summary(). What it cannot inherit is two sets of fields — which is the half that caused C++'s diamond problem in the first place. Removing state from the picture is what made multiple inheritance of behaviour safe enough to add.
Why default methods exist at all
This is the "why" that separates a memorised answer from an understood one. default methods were not added to make interfaces more convenient. They were added to solve a library evolution problem.
Before Java 8, adding a method to a published interface was a breaking change: every implementation in every codebase stopped compiling. So the JDK could not add stream() to Collection, and libraries shipped an AbstractXxx adapter class next to each interface so implementors had somewhere safe to inherit from.
default methods removed that constraint. Collection.stream(), Iterable.forEach, Comparator.reversed(), Map.getOrDefault — all added to interfaces that thousands of classes already implemented, none of them breaking. Knowing this reframes the design guidance: a default method is best understood as a migration tool and a convenience derived from the abstract methods, not as a place to put your real logic.
Reference
The comparison, current as of Java 21
abstract class | interface | |
|---|---|---|
| Instance fields | yes, any modifier | no — fields are public static final |
| Constructor | yes, runs before subclass body | no |
| Method bodies | yes | yes — default, static, Java 9+private |
| How many | extend exactly one | implement many |
| Access modifiers on methods | any | public by default; private allowed since 9 |
final / synchronized methods | yes | no |
| Can be a lambda target | no | yes, if exactly one abstract method |
| Restrict who extends | final, or sealed Java 17+since 17 | sealed Java 17+since 17 |
| Conflict resolution | single parent, no conflict | Interface.super.method() |
Choosing, as a sequence of questions
1. Do subtypes need to SHARE MUTABLE STATE?
yes -> abstract class. An interface cannot hold it.
2. Does construction have an invariant every subtype must run?
yes -> abstract class. An interface has no constructor.
3. Might an implementor already extend something else?
yes -> interface. You only get one superclass, and taking it is
a decision you make on the implementor's behalf.
4. Is this a CAPABILITY rather than an IS-A?
Comparable, Closeable, Serializable — these are things a type can DO.
-> interface.
5. Still unsure?
-> interface. It is the less constraining choice, it can be a lambda
target, and you can always add an abstract base class beside it.
Point 5 is the practical default and the JDK follows it: List is the interface, AbstractList is the convenience beside it, and implementors choose. You are not obliged to pick one.
Both, which is usually the right answer for a library
// The contract everyone codes against.
public interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
// Derived from the abstract methods — safe as a default, because it
// needs no state of its own.
default boolean exists(ID id) {
return findById(id).isPresent();
}
}
// The convenience for implementors who want it, holding the shared state
// a default method cannot.
public abstract class AbstractCachingRepository<T, ID> implements Repository<T, ID> {
private final Map<ID, T> cache = new ConcurrentHashMap<>(); // state
protected AbstractCachingRepository(int warmupSize) { // constructor
// an invariant every subtype passes through
}
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(cache.computeIfAbsent(id, this::load));
}
protected abstract T load(ID id);
}
The traps, each in one line
// 1. The class always wins — silently. This compiles and prints the CLASS's
// version, even though you just added the interface for its default.
class Invoice extends Document implements Timestamped { }
// 2. A default method cannot override anything from Object. equals, hashCode
// and toString are all rejected, because the class always wins anyway and
// a default could never be reached.
interface Bad { default String toString() { return "x"; } }
// error: default method toString in interface Bad overrides a member of java.lang.Object
// 3. An interface field is shared, not per-instance. This is one counter for
// the whole program, and it is final, so you cannot even increment it.
interface Counted { int count = 0; }
// 4. Adding an abstract method to a published interface still breaks every
// implementor. Only DEFAULT methods are safe to add.
Scenarios
A framework needs a new method on an interface a hundred teams implement. This is precisely the problem default methods were built for. Ship it with a default that derives a sensible answer from the existing abstract methods, and it is source- and binary-compatible for every implementor. If no sensible default exists, that is real information: the method probably does not belong on this interface, or you are looking at a new interface and a major version.
You want an abstract class so subtypes cannot skip validation. Good instinct, and the abstract class is right if the validation needs constructor-time state. But check the cost first: you are spending the implementor's single extends slot, and if any of them already extends a framework base class they are stuck. The alternative is an interface plus a static factory that performs the validation and returns the implementation — same guarantee, no inheritance spent. Choose the abstract class when subtypes genuinely need the shared fields, not merely the shared check.
Someone adds an interface to a class and nothing changes. They added Timestamped expecting summary() to start returning a timestamp, and the class's own summary() kept winning. No error, no warning, and the code review passed because the diff looked right. Worth knowing as a rule: if you want the interface's default, @Override it and call Timestamped.super.summary(). If you did not want the class's, the class should not have had the method.
Two libraries you do not control both declare default close(). You implement both interfaces and the class will not compile — inheriting unrelated defaults. There is no way to ask for "whichever", and that is correct: the compiler cannot know which semantics you want. You write the override and delegate explicitly, and the two lines you write are documentation of a decision that would otherwise have been made by accident. This is the diamond problem handled rather than avoided.
Interviewer's Next Move
1. "Can an interface have state?"
No instance state. Every field in an interface is implicitly public static final, so it is one shared constant, not per-instance data — and you cannot assign to it. That is the main reason abstract classes still exist: a default method has no fields of its own to work with and can only call other interface methods.
2. "What problem do default methods solve, and what did they reintroduce?"
They solve library evolution: before Java 8 adding a method to a published interface broke every implementor, which is why Collection.stream() could not exist. They reintroduced multiple inheritance — of behaviour, not state. Inheriting two conflicting defaults is a compile error you resolve with Interface.super.method().
3. "Does Java have multiple inheritance?" Of behaviour, yes, since 8. Of state, no, and that is the distinction that matters: the classic diamond problem is about duplicated fields, and since interfaces cannot hold fields, the hard case cannot arise. What remains is ambiguous behaviour, and Java makes that a compile error rather than guessing.
4. "A class extends a base class with summary() and implements an interface with a default summary(). Which runs?"
The class's. The rule is "the class always wins", and it exists so that adding a default to an interface can never change how an existing class behaves — the compatibility guarantee that made default methods possible at all. It compiles with no warning, which makes it a genuine trap when someone adds the interface expecting the default to take effect.
5. "Can a default method override equals or toString?"
No — it is a compile error. Every class inherits those from Object, and since the class always wins, a default could never be reached; allowing it would be declaring dead code. It also stops someone weakening the equals contract for every implementor of an interface at once.
Code traps
interface Greeter {
String name = "default";
default String greet() { return "hello " + name; }
}
class Person implements Greeter {
String name = "ravi";
}
System.out.println(new Person().greet());
Answer
hello default. The interface's name is a public static final constant, and the default method resolves name against the interface at compile time — it has no access to Person's field and would not use it even if it could. A default method can only call methods on the interface; a field with the same name in an implementing class is unrelated. This is the clearest demonstration that an interface has no instance state.
interface A { default void run() { System.out.println("A"); } }
interface B extends A { default void run() { System.out.println("B"); } }
class C implements A, B { }
new C().run();
Answer
B — and importantly it compiles, unlike the earlier diamond. There is no ambiguity because B extends A, so B.run() is more specific and the "most specific interface wins" rule applies. The compile error only occurs for unrelated defaults. Implementing both A and B is redundant but harmless.
Common wrong answers
| Said in interviews | Reality |
|---|---|
| "An interface can't have implementation." | Out of date since Java 8: default, static, and private since 9. |
| "Use an abstract class when you want to share code." | Shared code is what default methods are for. Use an abstract class when you need shared state or a constructor. |
| "Interfaces are slower / faster." | No meaningful difference — invokeinterface vs invokevirtual is not something to design around. |
| "Java has no multiple inheritance." | It has multiple inheritance of behaviour since 8. Not of state. |
| "A default method conflict is resolved by declaration order." | It is a compile error. There is no implicit rule for unrelated defaults. |
| "Adding a default method is always safe." | Safe for compilation. It can still break a subclass that had an unrelated method of the same name — and adding an abstract method to an interface is never safe. |
| "Fields in an interface are per-instance if you don't write static." | Every interface field is public static final, written or not. |
Check Yourself
Q1. What can an abstract class do that an interface cannot, in one sentence each?
Answer
Hold instance state — an interface's fields are all public static final, so a default method has no per-instance data. And run a constructor, so it can enforce an invariant every subtype passes through before its own body executes.
Q2. Why is inheriting two conflicting defaults an error, while a class method silently beating an interface default is not?
Answer
Because the second case has a rule that preserves compatibility: the class always wins, which guarantees that adding a default to an interface can never change how an existing class behaves. The first case has no such rule — neither interface has priority, and the compiler will not guess — so it makes you say which you meant.
Q3. You are designing a public API and genuinely cannot decide. What do you ship?
Answer
The interface, plus an abstract base class beside it if implementors would benefit. The interface is the less constraining choice: it does not spend the implementor's single extends, it can be a lambda target, and you can add the abstract class later. The JDK does exactly this — List and AbstractList.
Practice
| Tier | Exercise | Time |
|---|---|---|
| Warm-up | Make the compiler explain the diamond | 5 min |
| Challenge | Add a method to a published interface | 20 min |
| Production | The audit trail that stopped being written | 45 min |
| Interview | Full round replay — abstract vs interface | 10 min |
What changed, and when
Version boundaries are where follow-ups live. Know the change and the behaviour it replaced.
- Java 8LTS
Interfaces gain default and static methods, so an interface can ship behaviour — which is how Collection.stream() and Iterable.forEach were added to interfaces every JDK user had already implemented.
Before Java 8: An interface was signatures only. Adding a method to a published interface broke every implementation, so libraries shipped an abstract Adapter class beside each interface instead.
- Java 9
Interfaces gain private and private static methods, so two default methods can share code without exposing a helper as part of the public contract.
Before Java 9: Shared logic between default methods had to be a public static method on the interface — visible to every caller — or duplicated.
- Java 17LTS
Sealed interfaces let you close the implementation set, which removes the last common reason to prefer an abstract class for control over who can extend.
Before Java 17: Only a class could restrict extension, via a package-private constructor, and that was a convention rather than something the compiler enforced.
Practice ladder
Reading this page is not knowing it. Four tiers, ending in a real incident.
- Warm-up5 min
Make the compiler explain the diamond
One concept, guided. Near-impossible to fail.
- Challenge20 min
Add a method to a published interface
Edge cases. You have to reason, and two valid fixes differ.
- Production incident45 min
The audit trail that stopped being written
A real incident: symptom first, cause hidden, tradeoff at the end.
- Interview replay10 min
Full round replay — abstract vs interface
Timed verbal replay with pass/fail criteria per follow-up.
Where this question goes next
Questions that lead here
What is the difference between overloading and overriding?
Overloading is resolved by the compiler from the declared argument types; overriding is resolved by the JVM from the object's runtime type. That one sentence predicts every surprising case — an Object variable holding a String picks the Object overload, a static method is hidden rather than overridden, and fields are not polymorphic at all.
Asked constantlyjunior0–8 yrs10 min readOopWhen should you use a record instead of a class?
When the type IS its data. A record is a semantic claim, not a boilerplate saving — you are declaring that two instances with the same components are the same thing, and that the components are the whole state. If that is not true, a record is the wrong shape however much typing it would save.
Asked constantlyintermediate1–10 yrs11 min readOop
Every runnable example above was compiled and executed against openjdk 21.0.12 on this build, and its output diffed against what this page claims. Last updated 2026-09-13.