Hook: Interviewers love this question because it checks whether you know when to define a contract and when to share code.
Question: What is the difference between an interface and an abstract class in Java?
Answer: An interface is mainly a contract: it says what a class can do. An abstract class is a partial blueprint: it can define shared fields, constructors, and common code, while still leaving some methods unfinished. In Java, a class can implement many interfaces, but it can extend only one class, so interfaces give flexibility and abstract classes give reuse.
Interview-Ready Answer: I use an interface when I want to define a capability, like sendable or payable, and I use an abstract class when I want to share state and common behavior. The big practical difference is that a class can implement many interfaces but extend only one abstract class. Also, interfaces are mostly contracts, while abstract classes can hold instance fields, constructors, and reusable logic, so I choose based on whether I need flexibility or shared implementation.
Detailed Explanation: Think of an interface as a promise and an abstract class as a starter kit. The promise says every implementer must provide certain behavior. The starter kit gives you shared code, shared data, and a base constructor so subclasses do not repeat the same work.
implements an interface, the compiler checks that it provides every required method, unless the class itself is abstract.extends an abstract class, it inherits fields and concrete methods immediately, and it must implement only the remaining abstract methods.O(1).default and static methods; since Java 9, they can also have private helper methods. That makes interfaces more flexible than they used to be, but they still do not replace abstract classes when you need per-object state.| Feature | Interface | Abstract class |
|---|---|---|
| Purpose | Capability | Shared base |
| State | Constants only | Instance fields |
| Constructors | No | Yes |
| Inheritance | Many allowed | Only one class |
| Methods | Abstract, default, static, private | Abstract and concrete |
| Best for | Loose coupling | Reuse and templates |
Comparable, Runnable, or a payment gateway contract.public static final, which means constant values shared by all objects.Memory note: If you remember only one rule, remember this: interface = what it can do; abstract class = what it is plus shared code.
Real-World Story: In a checkout service, a team built multiple payment processors for card, wallet, and bank transfer. They used an interface for the common contract, but the shared retry logic and request validation lived better in an abstract base class. One release, a developer copied validation into one processor and missed a null check, so only that payment path started returning 500 errors during peak traffic. Users saw failed checkouts, logs showed IllegalArgumentException and intermittent retries, and the support team got a spike in abandoned carts.
The fix was to move the shared validation and retry policy into an abstract class and keep the payment capability as an interface. That made the contract clear, reduced duplication, and made future processors safer to add.
import java.util.Arrays;
import java.util.List;
interface Notifier {
void send(String recipient, String message);
}
abstract class BaseNotifier implements Notifier {
private final String serviceName;
private final int maxRetries;
protected BaseNotifier(String serviceName, int maxRetries) {
if (serviceName == null || serviceName.isBlank()) {
throw new IllegalArgumentException("serviceName must not be blank");
}
if (maxRetries < 1) {
throw new IllegalArgumentException("maxRetries must be at least 1");
}
this.serviceName = serviceName;
this.maxRetries = maxRetries;
}
@Override
public final void send(String recipient, String message) {
validate(recipient, message);
RuntimeException lastFailure = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
doSend(recipient, message, attempt);
System.out.println(serviceName + " delivered to " + recipient);
return;
} catch (RuntimeException ex) {
lastFailure = ex;
System.out.println(serviceName + " attempt " + attempt + " failed: " + ex.getMessage());
}
}
throw new IllegalStateException(
serviceName + " could not deliver after " + maxRetries + " attempts",
lastFailure);
}
protected abstract void doSend(String recipient, String message, int attempt);
protected void validate(String recipient, String message) {
if (recipient == null || recipient.isBlank()) {
throw new IllegalArgumentException("recipient must not be blank");
}
if (message == null || message.isBlank()) {
throw new IllegalArgumentException("message must not be blank");
}
}
protected String serviceName() {
return serviceName;
}
}
class EmailNotifier extends BaseNotifier {
EmailNotifier() {
super("Email", 2);
}
@Override
protected void doSend(String recipient, String message, int attempt) {
if (recipient.endsWith("@baddomain.com")) {
throw new RuntimeException("SMTP rejected recipient");
}
System.out.println("Sending email via " + serviceName() + " on attempt " + attempt + ": " + message);
}
}
class SmsNotifier extends BaseNotifier {
SmsNotifier() {
super("SMS", 3);
}
@Override
protected void doSend(String recipient, String message, int attempt) {
if (!recipient.matches("[0-9]{10}")) {
throw new RuntimeException("invalid phone number format");
}
if (attempt < 2 && message.length() > 20) {
throw new RuntimeException("gateway timeout");
}
System.out.println("Sending SMS via " + serviceName() + " on attempt " + attempt + ": " + message);
}
}
public class Main {
public static void main(String[] args) {
List<Notifier> notifiers = Arrays.asList(new EmailNotifier(), new SmsNotifier());
// Polymorphism: the caller knows only the interface, not the concrete class.
notifiers.get(0).send("alice@example.com", "Hello from the notification system");
notifiers.get(1).send("5551234567", "Hello from the notification system");
// Edge case: shared validation in the abstract class blocks bad input early.
try {
new EmailNotifier().send(" ", "This should fail");
} catch (Exception e) {
System.out.println("Edge case caught: " + e.getClass().getSimpleName() + " - " + e.getMessage());
}
// Failure path: a bad domain causes the base retry loop to report the problem clearly.
try {
new EmailNotifier().send("bob@baddomain.com", "This one will fail after retries");
} catch (Exception e) {
System.out.println("Failure caught: " + e.getClass().getSimpleName() + " - " + e.getMessage());
}
}
}Follow-up & Tricky Questions:
default and static methods, and since Java 9 they can also have private helper methods. They still are not a replacement for class state.public static final, so they are shared values, not per-object variables.Common Mistakes:
default, static, and private methods.Memory Hook: Interface = job description. Abstract class = starter kit. A job description tells you what the role must do; a starter kit gives you the tools and the half-built workspace.
Cheat Sheet:
Practice Tasks:
Shape with area() and implement it in Circle and Rectangle.