Java Abstract Class vs Interface
Java Abstract Class vs Interface
Every Java developer reaches a point in system design where they must choose: should this shared contract be an abstract class or an interface? Both can declare methods that subclasses must implement. Both cannot be instantiated directly. Both establish type hierarchies that polymorphism depends on.
The similarity ends there. An abstract class can hold state, run constructor logic, and provide concrete implementation shared across all subclasses. An interface defines a pure contract — what an object can do, with no assumption about what it is. Java 8 blurred the line by adding default and static methods to interfaces, but the fundamental distinction remains. Understanding it changes how you design every class hierarchy you write.
The Core Distinction in One Sentence
An abstract class defines what a thing is — a partial implementation that subclasses complete. An interface defines what a thing can do — a capability contract that any class can adopt regardless of its place in the hierarchy.
Abstract Class — IS-A relationship:
Vehicle (abstract)
|
has: fuelType, engineSize fields
has: refuel() concrete method
has: startEngine() abstract method
|
+--+--+
| |
Car Truck
| |
implements implements
startEngine() startEngine()
Interface — CAN-DO relationship:
Serializable Printable Comparable
| | |
Any class can Any class can Any class can
implement implement implement
regardless of regardless of regardless of
its hierarchy its hierarchy its hierarchy
A Car IS-A Vehicle — it makes sense to extend the abstract class. A Car CAN-DO Serializable — it makes sense to implement the interface. These are different kinds of relationships, and mixing them up produces designs that are painful to maintain.
Abstract Class — When State and Shared Logic Matter
An abstract class is the right tool when multiple related classes share concrete implementation — fields, constructor logic, or non-trivial methods — and differ only in specific behaviours.
1// File: ReportGenerator.java
2
3public abstract class ReportGenerator {
4
5 // Shared state — all reports have these
6 private final String reportTitle;
7 private final String preparedBy;
8 private final java.time.LocalDate generatedOn;
9
10 // Constructor — runs for every subclass via super()
11 protected ReportGenerator(String reportTitle, String preparedBy) {
12 if (reportTitle == null || reportTitle.isBlank()) {
13 throw new IllegalArgumentException("Report title cannot be empty.");
14 }
15 this.reportTitle = reportTitle;
16 this.preparedBy = preparedBy;
17 this.generatedOn = java.time.LocalDate.now();
18 }
19
20 public String getReportTitle() { return reportTitle; }
21 public String getPreparedBy() { return preparedBy; }
22
23 // Concrete method — shared header logic, no need to repeat in each subclass
24 protected String buildHeader() {
25 return "=".repeat(50)
26 + "\n Report : " + reportTitle
27 + "\n Prepared : " + preparedBy
28 + "\n Date : " + generatedOn
29 + "\n" + "=".repeat(50);
30 }
31
32 // Template method — controls the report generation flow
33 public final String generate() {
34 return buildHeader() + "\n" + buildBody() + "\n" + buildFooter();
35 }
36
37 // Abstract — each report type provides its own body
38 protected abstract String buildBody();
39
40 // Hook — subclasses may override if they need a custom footer
41 protected String buildFooter() {
42 return "\n--- End of " + reportTitle + " ---";
43 }
44}1// File: SalesReport.java
2
3public class SalesReport extends ReportGenerator {
4
5 private final double totalRevenue;
6 private final int totalOrders;
7 private final String region;
8
9 public SalesReport(String preparedBy, double totalRevenue,
10 int totalOrders, String region) {
11 super("Monthly Sales Report", preparedBy);
12 this.totalRevenue = totalRevenue;
13 this.totalOrders = totalOrders;
14 this.region = region;
15 }
16
17 @Override
18 protected String buildBody() {
19 double avgOrder = totalOrders > 0 ? totalRevenue / totalOrders : 0;
20 return " Region : " + region
21 + "\n Total Orders : " + totalOrders
22 + "\n Total Revenue : Rs." + String.format("%.2f", totalRevenue)
23 + "\n Avg Order : Rs." + String.format("%.2f", avgOrder);
24 }
25}1// File: InventoryReport.java
2
3public class InventoryReport extends ReportGenerator {
4
5 private final int totalSkus;
6 private final int lowStockItems;
7 private final double inventoryValue;
8
9 public InventoryReport(String preparedBy, int totalSkus,
10 int lowStockItems, double inventoryValue) {
11 super("Inventory Summary Report", preparedBy);
12 this.totalSkus = totalSkus;
13 this.lowStockItems = lowStockItems;
14 this.inventoryValue = inventoryValue;
15 }
16
17 @Override
18 protected String buildBody() {
19 return " Total SKUs : " + totalSkus
20 + "\n Low Stock : " + lowStockItems + " items"
21 + "\n Inventory Val : Rs." + String.format("%.2f", inventoryValue);
22 }
23}1// File: AbstractClassDemo.java
2
3public class AbstractClassDemo {
4
5 public static void main(String[] args) {
6
7 ReportGenerator sales = new SalesReport(
8 "Ananya Krishnan", 485000.0, 320, "South India"
9 );
10 ReportGenerator inventory = new InventoryReport(
11 "Rohan Desai", 1240, 38, 2850000.0
12 );
13
14 System.out.println(sales.generate());
15 System.out.println();
16 System.out.println(inventory.generate());
17 }
18}Output:
==================================================
Report : Monthly Sales Report
Prepared : Ananya Krishnan
Date : 2024-01-15
==================================================
Region : South India
Total Orders : 320
Total Revenue : Rs.485000.00
Avg Order : Rs.1515.63
--- End of Monthly Sales Report ---
==================================================
Report : Inventory Summary Report
Prepared : Rohan Desai
Date : 2024-01-15
==================================================
Total SKUs : 1240
Low Stock : 38 items
Inventory Val : Rs.2850000.00
--- End of Inventory Summary Report ---
ReportGenerator owns the fields, the constructor validation, the header format, and the generate() flow. Subclasses provide only what genuinely varies: the body content. Without the abstract class, every report would duplicate the header building, the date tracking, and the constructor validation.
Interface — When Capability Matters More Than Lineage
An interface is the right tool when unrelated classes need to share a contract but share no implementation. A PaymentProcessor, a FileUploader, and a CacheWarmer might all need to be Retryable — but they extend different parent classes and share no concrete logic.
1// File: Retryable.java
2
3public interface Retryable {
4
5 // Abstract — implementing class provides the retry logic
6 int getMaxRetries();
7 long getRetryDelayMs();
8
9 // Default method — shared retry outcome description, overridable
10 default String describeRetryPolicy() {
11 return "Max retries: " + getMaxRetries()
12 + ", Delay: " + getRetryDelayMs() + "ms";
13 }
14}1// File: Validatable.java
2
3public interface Validatable {
4 boolean isValid();
5 String getValidationMessage();
6}1// File: PaymentRequest.java
2
3// PaymentRequest is both Retryable and Validatable — two unrelated capabilities
4public class PaymentRequest implements Retryable, Validatable {
5
6 private final String orderId;
7 private final double amount;
8 private final String paymentMethod;
9
10 public PaymentRequest(String orderId, double amount, String paymentMethod) {
11 this.orderId = orderId;
12 this.amount = amount;
13 this.paymentMethod = paymentMethod;
14 }
15
16 @Override public int getMaxRetries() { return 3; }
17 @Override public long getRetryDelayMs() { return 1000L; }
18
19 @Override
20 public boolean isValid() {
21 return amount > 0 && orderId != null && !orderId.isBlank()
22 && paymentMethod != null && !paymentMethod.isBlank();
23 }
24
25 @Override
26 public String getValidationMessage() {
27 if (amount <= 0) return "Amount must be positive.";
28 if (orderId == null) return "Order ID is required.";
29 return "Valid payment request.";
30 }
31
32 public String getOrderId() { return orderId; }
33 public double getAmount() { return amount; }
34}1// File: InterfaceDemo.java
2
3public class InterfaceDemo {
4
5 // Methods accept interface types — work with any implementing class
6 public static void processWithRetry(Retryable retryable) {
7 System.out.println("Processing with policy: " + retryable.describeRetryPolicy());
8 }
9
10 public static void validate(Validatable validatable) {
11 boolean valid = validatable.isValid();
12 System.out.println("Valid: " + valid + " — " + validatable.getValidationMessage());
13 }
14
15 public static void main(String[] args) {
16
17 PaymentRequest req1 = new PaymentRequest("ORD-501", 2499.0, "UPI");
18 PaymentRequest req2 = new PaymentRequest("", -50.0, "");
19
20 processWithRetry(req1);
21 validate(req1);
22 System.out.println();
23 processWithRetry(req2);
24 validate(req2);
25 }
26}Output:
Processing with policy: Max retries: 3, Delay: 1000ms
Valid: true — Valid payment request.
Processing with policy: Max retries: 3, Delay: 1000ms
Valid: false — Amount must be positive.
PaymentRequest implements two completely unrelated interfaces. It does not extend any parent class. Both capabilities are tested through their respective interface types — no knowledge of PaymentRequest needed. This is the flexibility that interfaces provide: any class, regardless of its hierarchy, can adopt any capability.
Side-by-Side Comparison — Abstract Class vs Interface
| Aspect | Abstract Class | Interface |
|---|---|---|
| Keyword | abstract class | interface |
| Used with | extends (one only) | implements (many) |
| Multiple inheritance | No — single extends only | Yes — a class can implements many |
| Instance fields | Yes — any type | No — only public static final constants |
| Constructors | Yes — called via super() | No |
| Abstract methods | Yes — abstract keyword | Yes — all methods abstract by default |
| Concrete methods | Yes — any visibility | default and static only (Java 8+) |
| Private methods | Yes | Yes — private allowed in interface (Java 9+) |
| Access modifiers on methods | Any — public, protected, package | public by default |
| State (mutable data) | Yes | No |
| IS-A relationship | Yes — strong type hierarchy | Yes — capability-based type |
| When to use | Shared state + shared implementation | Pure contract, multiple capabilities |
| Instantiation | No | No |
@Override | Recommended | Recommended |
Java 8+ Default Methods — The Overlap Zone
Java 8 added default methods to interfaces, giving them concrete implementations. This narrowed the gap between abstract classes and interfaces and created genuine confusion about when to choose which.
1// File: Loggable.java
2
3public interface Loggable {
4
5 // Abstract — each implementing class provides its own logger name
6 String getLoggerName();
7
8 // Default — shared logging implementation any class can use
9 default void logInfo(String message) {
10 System.out.println("[INFO][" + getLoggerName() + "] " + message);
11 }
12
13 default void logError(String message, Exception e) {
14 System.out.println("[ERROR][" + getLoggerName() + "] " + message
15 + " — " + e.getMessage());
16 }
17
18 // Static — utility method on the interface itself
19 static String formatMessage(String level, String name, String msg) {
20 return "[" + level + "][" + name + "] " + msg;
21 }
22}1// File: OrderService.java
2
3public class OrderService implements Loggable {
4
5 private final String serviceName = "OrderService";
6
7 @Override
8 public String getLoggerName() { return serviceName; }
9
10 public void processOrder(String orderId) {
11 logInfo("Processing order: " + orderId);
12 // processing logic
13 logInfo("Order processed: " + orderId);
14 }
15}1// File: DefaultMethodDemo.java
2
3public class DefaultMethodDemo {
4
5 public static void main(String[] args) {
6
7 OrderService service = new OrderService();
8 service.processOrder("ORD-7821");
9
10 // Static interface method — called on interface name
11 System.out.println(Loggable.formatMessage("WARN", "Gateway", "Timeout detected"));
12 }
13}Output:
[INFO][OrderService] Processing order: ORD-7821
[INFO][OrderService] Order processed: ORD-7821
[WARN][Gateway] Timeout detected
Default methods let interfaces provide shared behaviour without forcing every implementing class to write it. But they cannot access instance fields — they can only call abstract methods declared in the same interface. When the shared behaviour needs to read or write instance state, only an abstract class can provide that.
Abstract Class vs Interface After Java 8 — What Still Differs
| Capability | Abstract Class | Interface (Java 8+) |
|---|---|---|
| Instance fields (mutable state) | Yes | No — constants only |
| Constructor | Yes | No |
| Access instance state in methods | Yes | No — default methods cannot access fields |
protected methods | Yes | No — interface methods are public |
| Multiple inheritance | No | Yes |
| Can extend a class | Yes | No |
| Partial implementation pattern | Yes | Partial via default methods |
private helper methods | Yes | Yes (Java 9+) — but only for default method helpers |
| Intended relationship | IS-A (type lineage) | CAN-DO (capability) |
Even after Java 8, the decisive differences are: instance state and multiple inheritance. If the shared logic needs to read or write fields on the object, use an abstract class. If a class needs to satisfy multiple independent contracts simultaneously, use interfaces.
Choosing Between Them — Decision Guide
START HERE
|
v
Does the shared code need to access
or modify instance-level state (fields)?
|
Yes | No
| |
v v
Use ABSTRACT CLASS Does a class need to
implement multiple
contracts simultaneously?
|
Yes | No
| |
v v
Use INTERFACE Does the hierarchy
have a clear IS-A
relationship?
|
Yes | No
| |
v v
Either works — INTERFACE
prefer INTERFACE (more flexible)
In practice, the most common pattern in modern Java combines both: an interface defines the contract, an abstract class provides a partial implementation, and concrete classes extend the abstract class. Java's AbstractList (abstract class) implements List (interface) is the canonical example.
Real-World Example — Notification Delivery System
The Business Problem
A notification platform at a company like Swiggy or Zepto sends alerts through SMS, email, and push channels. All delivery handlers share common logic: request validation, retry policy tracking, delivery status recording. Each channel has its own delivery mechanism. The delivery contract is defined as an interface — any class in any hierarchy can be a delivery handler. The common pipeline logic lives in an abstract class — no need to duplicate it across channels.
1// File: NotificationDelivery.java
2
3public interface NotificationDelivery {
4 boolean deliver(String recipient, String content);
5 String getChannelName();
6 default String deliveryTag() {
7 return "[" + getChannelName().toUpperCase() + "]";
8 }
9}1// File: BaseDeliveryHandler.java
2
3// Abstract class — holds state and enforces the validated delivery pipeline
4public abstract class BaseDeliveryHandler implements NotificationDelivery {
5
6 private final int maxRetries;
7 private int successCount;
8 private int failureCount;
9
10 protected BaseDeliveryHandler(int maxRetries) {
11 this.maxRetries = maxRetries;
12 this.successCount = 0;
13 this.failureCount = 0;
14 }
15
16 public int getMaxRetries() { return maxRetries; }
17 public int getSuccessCount() { return successCount; }
18 public int getFailureCount() { return failureCount; }
19
20 // Concrete shared logic — validates and delegates to channel-specific send
21 @Override
22 public final boolean deliver(String recipient, String content) {
23
24 if (recipient == null || recipient.isBlank()) {
25 System.out.println(deliveryTag() + " Rejected: empty recipient.");
26 failureCount++;
27 return false;
28 }
29
30 if (content == null || content.isBlank()) {
31 System.out.println(deliveryTag() + " Rejected: empty content.");
32 failureCount++;
33 return false;
34 }
35
36 boolean sent = sendToChannel(recipient, content);
37
38 if (sent) {
39 successCount++;
40 System.out.println(deliveryTag() + " Delivered to " + recipient + ". "
41 + "Total sent: " + successCount);
42 } else {
43 failureCount++;
44 System.out.println(deliveryTag() + " Failed for " + recipient + ". "
45 + "Total failed: " + failureCount);
46 }
47 return sent;
48 }
49
50 // Abstract — each channel sends differently
51 protected abstract boolean sendToChannel(String recipient, String content);
52}1// File: SmsDeliveryHandler.java
2
3public class SmsDeliveryHandler extends BaseDeliveryHandler {
4
5 private final String smsGateway;
6
7 public SmsDeliveryHandler(String smsGateway) {
8 super(3); // 3 retries for SMS
9 this.smsGateway = smsGateway;
10 }
11
12 @Override
13 public String getChannelName() { return "SMS"; }
14
15 @Override
16 protected boolean sendToChannel(String recipient, String content) {
17 System.out.println(" Sending via " + smsGateway + " → "
18 + recipient + ": " + content);
19 return true; // simulated success
20 }
21}1// File: EmailDeliveryHandler.java
2
3public class EmailDeliveryHandler extends BaseDeliveryHandler {
4
5 private final String smtpHost;
6
7 public EmailDeliveryHandler(String smtpHost) {
8 super(5); // 5 retries for email
9 this.smtpHost = smtpHost;
10 }
11
12 @Override
13 public String getChannelName() { return "Email"; }
14
15 @Override
16 protected boolean sendToChannel(String recipient, String content) {
17 if (!recipient.contains("@")) {
18 System.out.println(" Invalid email: " + recipient);
19 return false;
20 }
21 System.out.println(" Sending via " + smtpHost + " → "
22 + recipient + " | Subject: " + content);
23 return true;
24 }
25}1// File: NotificationSystemDemo.java
2
3import java.util.List;
4
5public class NotificationSystemDemo {
6
7 // Accepts interface type — works with any delivery handler
8 public static void sendAll(List<NotificationDelivery> handlers,
9 String recipient, String content) {
10 System.out.println("\n--- Sending to: " + recipient + " ---");
11 for (NotificationDelivery handler : handlers) {
12 handler.deliver(recipient, content);
13 }
14 }
15
16 public static void main(String[] args) {
17
18 SmsDeliveryHandler sms = new SmsDeliveryHandler("AWS-SNS");
19 EmailDeliveryHandler email = new EmailDeliveryHandler("smtp.sendgrid.net");
20
21 // Interface type in the list — upcasting
22 List<NotificationDelivery> handlers = List.of(sms, email);
23
24 sendAll(handlers, "9876543210", "Your OTP is 482910.");
25 sendAll(handlers, "dev@company.in", "Deployment approved.");
26 sendAll(handlers, "", "Empty recipient test.");
27
28 System.out.println("\n--- Delivery Summary ---");
29 System.out.println("SMS — Sent: " + sms.getSuccessCount()
30 + ", Failed: " + sms.getFailureCount());
31 System.out.println("Email — Sent: " + email.getSuccessCount()
32 + ", Failed: " + email.getFailureCount());
33 }
34}Output:
--- Sending to: 9876543210 ---
Sending via AWS-SNS → 9876543210: Your OTP is 482910.
[SMS] Delivered to 9876543210. Total sent: 1
Invalid email: 9876543210
[EMAIL] Failed for 9876543210. Total failed: 1
--- Sending to: dev@company.in ---
Sending via AWS-SNS → dev@company.in: Deployment approved.
[SMS] Delivered to dev@company.in. Total sent: 2
Sending via smtp.sendgrid.net → dev@company.in | Subject: Deployment approved.
[EMAIL] Delivered to dev@company.in. Total sent: 1
--- Sending to: ---
[SMS] Rejected: empty recipient.
[EMAIL] Rejected: empty recipient.
--- Delivery Summary ---
SMS — Sent: 2, Failed: 1
Email — Sent: 1, Failed: 2
The interface NotificationDelivery defines the contract. The abstract class BaseDeliveryHandler holds the retry counter, success/failure tracking, and validates inputs — all things that need state. Concrete handlers provide only the channel-specific send logic. Every component does exactly one job with no duplication.
Best Practices
Start with an interface; move to an abstract class only when state or shared logic demands it. Interfaces impose fewer constraints — a class can implement many but extend only one. Beginning with an interface keeps future design options open. If you later discover that three implementations all repeat the same field or the same validation logic, extract it into an abstract class that implements the interface.
Combine interface and abstract class for maximum flexibility. The standard library pattern — interface defines the contract, abstract class provides partial implementation, concrete classes complete it — gives callers the flexibility of the interface type while giving implementors the convenience of the shared base. Collection → AbstractCollection → ArrayList is the canonical Java example.
Never add methods to an existing interface without a default implementation. Adding an abstract method to a widely-used interface breaks every class that implements it. If the new method cannot be made default, create a new interface. This is why Java 8 introduced default methods — not to give interfaces state, but to allow them to evolve without breaking existing code.
Use abstract classes for template method pattern; interfaces for strategy pattern. When the algorithm skeleton is fixed but individual steps vary, an abstract class with a final template method is the right structure. When the entire algorithm is swappable — and many implementations may coexist at runtime — an interface is the right structure.
Common Mistakes
Mistake 1 — Putting State Into an Interface
1// Compile error — interface fields are implicitly public static final
2public interface OrderTracker {
3 int orderCount = 0; // this is a constant, not mutable state
4
5 void incrementCount(); // implementing class cannot store count here
6 int getCount(); // nothing for this to read
7}
8
9// What you actually need
10public abstract class OrderTracker {
11 private int orderCount = 0; // real mutable state
12
13 public void incrementCount() { orderCount++; }
14 public int getCount() { return orderCount; }
15}Every field in an interface is implicitly public static final. You cannot use an interface to share mutable state across instances. When shared mutable state is part of the design, an abstract class is the only option.
Mistake 2 — Using Abstract Class Where Multiple Inheritance Is Needed
1// Problem — a class cannot extend two abstract classes
2public abstract class Flyable { public abstract void fly(); }
3public abstract class Swimmable { public abstract void swim(); }
4
5public class Duck extends Flyable, Swimmable { // compile error — two extends
6}
7
8// Fix — make them interfaces
9public interface Flyable { void fly(); }
10public interface Swimmable { void swim(); }
11
12public class Duck implements Flyable, Swimmable {
13 @Override public void fly() { System.out.println("Duck flies."); }
14 @Override public void swim() { System.out.println("Duck swims."); }
15}When a class needs to represent multiple independent capabilities from unrelated lineages, interfaces are the only mechanism Java provides. Reaching for abstract classes in this situation forces a single-parent constraint that the design cannot accommodate.
Mistake 3 — Treating Default Methods as a Replacement for Abstract Class State
1public interface StatefulCounter {
2
3 // This does NOT work — default methods cannot access instance fields
4 // There is no "count" field to read in an interface
5 default int increment() {
6 // count++; // compile error — no such field
7 return -1; // meaningless without state
8 }
9}Default methods in interfaces share code — they cannot share state. A default method can only call other methods declared in the same interface. It cannot read or write instance fields because interfaces have no instance fields. When the shared behaviour requires reading or mutating object state, an abstract class is the correct choice regardless of what Java 8 added.
Interview Questions
Q1. What is the main difference between an abstract class and an interface in Java?
An abstract class can hold instance fields, constructors, and both abstract and concrete methods with any access modifier. A class can extend only one abstract class. An interface defines abstract methods with no instance state or constructors — since Java 8 it can have default and static methods. A class can implement multiple interfaces simultaneously. Use an abstract class when related classes share mutable state or common implementation. Use an interface for pure contracts, capability definitions, or when a class needs to satisfy multiple independent contracts.
Q2. Can an abstract class implement an interface in Java?
Yes. An abstract class can implement an interface without implementing all of the interface's abstract methods. The unimplemented methods become abstract members of the abstract class, to be implemented by its concrete subclasses. This is a common layered design: the interface defines the full contract, the abstract class provides shared partial implementation, and concrete classes complete the remaining methods.
Q3. Why can a class implement multiple interfaces but extend only one class?
Java avoids the diamond problem of C++ — when two parent classes define the same method, the compiler cannot determine which version to inherit. Interfaces solved this in Java 8+ by requiring the implementing class to explicitly resolve any default method conflicts. Classes are not required to implement methods inherited from abstract parents — the diamond problem is real for state and concrete methods. Multiple interface implementation is safe because interfaces share no instance state.
Q4. When would you choose an abstract class over an interface even in modern Java?
Choose an abstract class when the shared code needs to access or modify instance fields — default methods in interfaces cannot do this. Choose it when constructor logic needs to run and be reusable across subclasses — interfaces have no constructors. Choose it when protected or package-private methods need to be shared — interface methods must be public. Choose it for the template method pattern where the algorithm skeleton is fixed and must not be overridden by implementors.
Q5. What changed for interfaces in Java 8 and does it make abstract classes obsolete?
Java 8 added default methods — concrete implementations in interfaces — and static methods. This allowed interfaces to evolve without breaking existing implementors and to provide shared utility behaviour. However, interfaces still cannot hold instance state, have constructors, or declare non-public methods. Abstract classes are not obsolete — they remain the only option whenever shared mutable state or constructor chaining is needed. The typical modern pattern uses both: an interface for the contract and an abstract class for the shared partial implementation.
Q6. Can an interface extend an abstract class in Java?
No. Interfaces can only extend other interfaces — they cannot extend classes, abstract or otherwise. An abstract class can implement an interface. A concrete class can extend an abstract class and implement interfaces simultaneously. The hierarchy is one-directional: interfaces are extended by interfaces; classes extend classes (at most one) and implement interfaces (many).
FAQs
Can an abstract class have a constructor in Java?
Yes. Abstract classes can have constructors, and they serve an important purpose: initialising shared fields that subclasses inherit. The constructor is called via super() from each subclass constructor. It cannot be called directly since abstract classes cannot be instantiated with new, but it runs every time a concrete subclass object is created.
Can an interface have private methods in Java?
Yes, since Java 9. Private methods in interfaces are helper methods that default methods in the same interface can call internally. They reduce code duplication within the interface body without exposing the helper logic as part of the public contract. They are invisible to implementing classes — only the default methods in the same interface can call them.
Should you always prefer interfaces over abstract classes?
Interfaces are generally preferred when there is no shared state because they impose fewer restrictions. A class that implements an interface still has its extends slot free for a future hierarchy. However, when shared fields, constructor logic, or protected helper methods are genuinely needed, forcing everything into an interface produces clunky designs with duplicated code. The right choice depends on whether the design needs shared state — not on a blanket preference.
What happens when two interfaces a class implements have the same default method?
The implementing class must override the conflicting default method to resolve the ambiguity — the compiler enforces this with an error. Inside the override, the class can call a specific interface's version using InterfaceA.super.method(), or provide an entirely new implementation. This is Java's explicit resolution of the diamond problem for default methods.
Can you instantiate an abstract class using an anonymous class?
Yes. An anonymous class can extend an abstract class and provide implementations for all abstract methods at the point of instantiation. The result is a nameless concrete subclass that exists only at that call site. This pattern appears frequently in older Java code — lambdas and method references have largely replaced it for single-method cases in modern Java.
Summary
Abstract classes and interfaces solve different design problems. Abstract classes are for IS-A relationships with shared state and shared implementation — when multiple related types share fields, constructor logic, or non-trivial methods, the abstract class holds what they have in common. Interfaces are for CAN-DO relationships — when any class, regardless of its lineage, should be able to declare a capability.
Java 8 narrowed the gap with default methods, but the decisive differences remain: instance state, constructors, and access modifiers. When the shared logic needs to read or write object fields, an abstract class is the only correct choice. When a class must satisfy multiple independent contracts simultaneously, interfaces are the only mechanism available.
The most common production pattern combines both — an interface establishes the contract, an abstract class provides partial implementation, and concrete classes complete the work. This gives callers the flexibility of the interface type while sparing implementors from repeating identical boilerplate. Recognising which layer belongs where is a skill that separates junior code from senior design.
What to Read Next
| Topic | Link |
|---|---|
| How abstract classes enforce the template method pattern with concrete subclasses | Abstract Classes → |
| How interfaces define pure contracts and enable multiple type implementation | Interfaces → |
| How polymorphism uses both abstract class and interface references for dispatch | Polymorphism → |
| How inheritance creates the IS-A hierarchy that abstract classes build on | Inheritance → |
| How encapsulation protects state that abstract classes manage on behalf of subclasses | Encapsulation → |