Java Tutorial
🔍

Java Abstraction

Java Abstraction

Abstraction is the principle of hiding complexity and exposing only what is necessary. You interact with a car through a steering wheel, pedals, and a gear lever — the engine internals, fuel injection timing, and combustion mechanics are hidden. The interface is simple; the implementation is complex. That is abstraction.

In Java, abstraction is achieved through two mechanisms: abstract classes and interfaces. Both define what an object can do without specifying how it does it. The caller writes code against the contract — the what — and each implementation class provides the how. The caller never needs to change when the how changes.

What Is Abstraction in Java?

Abstraction is the OOP principle of exposing only essential behaviour while hiding implementation details. It separates the definition of operations from their execution.

A class that declares a method without providing its body is abstract. It tells every subclass: "You must implement this." The abstract class defines the contract; concrete subclasses fulfil it. Callers that work with the abstract type automatically get the correct behaviour regardless of which concrete type they receive.

Java provides two mechanisms for abstraction:

MechanismKeywordCan Have Concrete MethodsCan Have State (Fields)Multiple Inheritance
Abstract classabstract classYesYesNo — single extends
InterfaceinterfaceYes (default methods)No (only constants)Yes — multiple implements

Why Abstraction Matters

Without abstraction, every caller must know the concrete type it is dealing with and must change whenever that type changes. With abstraction, callers depend on a stable contract — the interface — and the implementation can evolve freely behind it.

The practical outcomes:

  • Replaceability — swap RazorpayGateway for PayUGateway without touching the payment service
  • Testability — inject a mock implementation during tests without modifying the system under test
  • Extensibility — add new notification channels without changing the notification dispatcher
  • Reduced coupling — the caller depends on the contract, not the implementation class name

Abstract Classes

An abstract class is a class declared with the abstract keyword. It cannot be instantiated directly — you cannot write new AbstractClass(). It may contain:

  • Abstract methods — declared without a body; subclasses must implement them
  • Concrete methods — fully implemented; subclasses inherit and may override them
  • Fields — instance variables, static fields, constants
  • Constructors — called by subclass constructors through super()
Java
1// File: ReportGenerator.java 2 3public abstract class ReportGenerator { 4 5 private final String reportTitle; 6 private final String generatedBy; 7 8 public ReportGenerator(String reportTitle, String generatedBy) { 9 this.reportTitle = reportTitle; 10 this.generatedBy = generatedBy; 11 } 12 13 public String getReportTitle() { return reportTitle; } 14 public String getGeneratedBy() { return generatedBy; } 15 16 // Abstract — each subclass fetches data differently 17 protected abstract String fetchData(); 18 19 // Abstract — each subclass formats output differently 20 protected abstract String formatReport(String data); 21 22 // Concrete — the generation flow is fixed for all subclasses 23 public final String generate() { 24 String header = "=== " + reportTitle + " ===" 25 + "\nGenerated by: " + generatedBy + "\n"; 26 String data = fetchData(); // subclass provides data 27 String body = formatReport(data); // subclass formats it 28 return header + body; 29 } 30}
Java
1// File: SalesReport.java 2 3public class SalesReport extends ReportGenerator { 4 5 private final double totalSales; 6 private final int totalOrders; 7 private final String region; 8 9 public SalesReport(String generatedBy, double totalSales, 10 int totalOrders, String region) { 11 super("Sales Report", generatedBy); 12 this.totalSales = totalSales; 13 this.totalOrders = totalOrders; 14 this.region = region; 15 } 16 17 @Override 18 protected String fetchData() { 19 // Simulates fetching from a database or analytics service 20 return "Region: " + region 21 + " | Orders: " + totalOrders 22 + " | Total Sales: Rs." + totalSales; 23 } 24 25 @Override 26 protected String formatReport(String data) { 27 double avgOrderValue = totalOrders > 0 ? totalSales / totalOrders : 0; 28 return data + "\nAverage order value: Rs." 29 + String.format("%.2f", avgOrderValue); 30 } 31}
Java
1// File: InventoryReport.java 2 3public class InventoryReport extends ReportGenerator { 4 5 private final int totalSkus; 6 private final int outOfStockSkus; 7 private final String warehouseId; 8 9 public InventoryReport(String generatedBy, int totalSkus, 10 int outOfStockSkus, String warehouseId) { 11 super("Inventory Report", generatedBy); 12 this.totalSkus = totalSkus; 13 this.outOfStockSkus = outOfStockSkus; 14 this.warehouseId = warehouseId; 15 } 16 17 @Override 18 protected String fetchData() { 19 return "Warehouse: " + warehouseId 20 + " | Total SKUs: " + totalSkus 21 + " | Out of stock: " + outOfStockSkus; 22 } 23 24 @Override 25 protected String formatReport(String data) { 26 double stockHealthPercent = totalSkus > 0 27 ? ((double)(totalSkus - outOfStockSkus) / totalSkus) * 100 : 0; 28 return data + "\nStock health: " 29 + String.format("%.1f", stockHealthPercent) + "%"; 30 } 31}
Java
1// File: AbstractClassDemo.java 2 3public class AbstractClassDemo { 4 5 public static void main(String[] args) { 6 7 ReportGenerator salesReport = new SalesReport( 8 "Priya Mehta", 485000.0, 320, "South India" 9 ); 10 11 ReportGenerator inventoryReport = new InventoryReport( 12 "Rahul Desai", 2450, 187, "WH-PUNE-01" 13 ); 14 15 System.out.println(salesReport.generate()); 16 System.out.println(); 17 System.out.println(inventoryReport.generate()); 18 } 19}
Output:
=== Sales Report ===
Generated by: Priya Mehta
Region: South India | Orders: 320 | Total Sales: Rs.485000.0
Average order value: Rs.1515.63

=== Inventory Report ===
Generated by: Rahul Desai
Warehouse: WH-PUNE-01 | Total SKUs: 2450 | Out of stock: 187
Stock health: 92.4%

generate() is final and concrete — it defines the algorithm structure. fetchData() and formatReport() are abstract — each subclass fills in the missing pieces. This is the Template Method Pattern: the parent controls the flow; the children provide the content. Abstraction is what makes this work — the generate() method does not know or care whether it is generating a sales report or inventory report.

Interfaces

An interface is a fully abstract type that defines a contract — a set of method signatures that implementing classes must provide. Since Java 8, interfaces can also contain default and static methods with implementations.

Java
1// File: PaymentGateway.java 2 3public interface PaymentGateway { 4 5 // Abstract methods — every implementor must provide these 6 boolean initiatePayment(String orderId, double amount, String currency); 7 boolean verifyPayment(String transactionId); 8 boolean refundPayment(String transactionId, double amount); 9 10 // Default method — shared logic available to all implementations 11 default String buildPaymentRef(String orderId) { 12 return "PAY-" + orderId + "-" + System.currentTimeMillis(); 13 } 14 15 // Static method — utility on the interface itself 16 static boolean isValidAmount(double amount) { 17 return amount > 0 && amount <= 1000000.0; 18 } 19}
Java
1// File: RazorpayGateway.java 2 3public class RazorpayGateway implements PaymentGateway { 4 5 private final String apiKey; 6 7 public RazorpayGateway(String apiKey) { 8 this.apiKey = apiKey; 9 } 10 11 @Override 12 public boolean initiatePayment(String orderId, double amount, String currency) { 13 String ref = buildPaymentRef(orderId); // inherited default method 14 System.out.println("Razorpay: initiating " + ref 15 + " | Rs." + amount + " " + currency 16 + " [key: " + apiKey.substring(0, 8) + "...]"); 17 return true; 18 } 19 20 @Override 21 public boolean verifyPayment(String transactionId) { 22 System.out.println("Razorpay: verifying " + transactionId); 23 return true; 24 } 25 26 @Override 27 public boolean refundPayment(String transactionId, double amount) { 28 System.out.println("Razorpay: refunding Rs." + amount 29 + " for " + transactionId); 30 return true; 31 } 32}
Java
1// File: PayUGateway.java 2 3public class PayUGateway implements PaymentGateway { 4 5 private final String merchantId; 6 7 public PayUGateway(String merchantId) { 8 this.merchantId = merchantId; 9 } 10 11 @Override 12 public boolean initiatePayment(String orderId, double amount, String currency) { 13 String ref = buildPaymentRef(orderId); // inherited default method 14 System.out.println("PayU: initiating " + ref 15 + " | Rs." + amount + " " + currency 16 + " [merchant: " + merchantId + "]"); 17 return true; 18 } 19 20 @Override 21 public boolean verifyPayment(String transactionId) { 22 System.out.println("PayU: verifying " + transactionId); 23 return true; 24 } 25 26 @Override 27 public boolean refundPayment(String transactionId, double amount) { 28 System.out.println("PayU: refunding Rs." + amount 29 + " for " + transactionId); 30 return true; 31 } 32}
Java
1// File: InterfaceDemo.java 2 3public class InterfaceDemo { 4 5 // Works with any PaymentGateway — abstraction in action 6 public static void processOrder(String orderId, double amount, 7 PaymentGateway gateway) { 8 if (!PaymentGateway.isValidAmount(amount)) { 9 System.out.println("Invalid amount: Rs." + amount); 10 return; 11 } 12 boolean initiated = gateway.initiatePayment(orderId, amount, "INR"); 13 if (initiated) { 14 System.out.println(" Payment initiated for " + orderId); 15 } 16 } 17 18 public static void main(String[] args) { 19 20 PaymentGateway razorpay = new RazorpayGateway("rzp_live_abc12345xyz"); 21 PaymentGateway payU = new PayUGateway("PAYU_MERCHANT_007"); 22 23 processOrder("ORD-1001", 1499.0, razorpay); 24 System.out.println(); 25 processOrder("ORD-1002", 3999.0, payU); 26 System.out.println(); 27 processOrder("ORD-1003", -500.0, razorpay); // invalid amount 28 } 29}
Output:
Razorpay: initiating PAY-ORD-1001-1704067200000 | Rs.1499.0 INR [key: rzp_live...]
  Payment initiated for ORD-1001

PayU: initiating PAY-ORD-1002-1704067200001 | Rs.3999.0 INR [merchant: PAYU_MERCHANT_007]
  Payment initiated for ORD-1002

Invalid amount: Rs.-500.0

processOrder() accepts a PaymentGateway reference — it does not know whether it is dealing with Razorpay or PayU. Switching the entire payment provider for any order requires changing one line at the call site — the object creation. The processing logic never changes. This is abstraction's core promise: the caller is immune to implementation changes.

Abstraction vs Encapsulation

These two pillars are the most consistently confused pair in Java OOP interviews. Understanding their precise distinction matters.

AspectAbstractionEncapsulation
Core ideaHide complexity — show only what the object doesHide data — protect internal state from direct access
FocusThe interface or contract exposed to the worldThe data and how it is protected within a class
Answers the question"What can this object do?""How is the data protected inside this object?"
Implemented usingAbstract classes, interfacesprivate fields, getters, setters
LevelSystem-level — design principle across classesClass-level — within a single class
GoalReduce complexity for the callerProtect data integrity and control modification
ExamplePaymentGateway interface hides Razorpay vs PayUprivate double balance with deposit() and withdraw()
Runtime behaviourEnforced through polymorphism at runtimeEnforced by access modifiers at compile time
Change impactNew implementation — no caller change neededInternal storage change — no caller change needed

The clearest way to remember it: Encapsulation hides the contents of a class (its data). Abstraction hides the existence of implementation details (the how behind a contract). Both hide things — different things, at different levels.

Abstract Class vs Interface

AspectAbstract ClassInterface
Keywordabstract classinterface
InstantiationCannot be instantiatedCannot be instantiated
MethodsAbstract and concreteAbstract, default (Java 8+), static (Java 8+)
FieldsInstance fields, static fields, constantsOnly public static final constants
ConstructorsYes — called via super() in subclassesNo
Access modifiers on methodsAnypublic by default (abstract methods)
Multiple inheritanceNo — extends one class onlyYes — implements multiple interfaces
extends / implementsChild uses extendsClass uses implements
IS-A relationshipYes — concrete subtypeYes — capability contract
When to useShared code + partial implementationPure contract, multiple inheritance of type

Levels of Abstraction in Java

Abstraction exists on a spectrum — from fully concrete (no abstraction) to fully abstract (pure contract).

Fully Concrete Class
(all methods implemented, can be instantiated)
         |
         |   ← add abstract methods
         ↓
Abstract Class
(mix of abstract + concrete methods, cannot be instantiated)
         |
         |   ← remove all concrete methods and fields
         ↓
Interface (pre-Java 8)
(only abstract method signatures, no implementation)
         |
         |   ← add default/static methods
         ↓
Interface (Java 8+)
(abstract + default + static methods, still no state)

The right level of abstraction depends on what callers need to know and what implementations need to share. If different implementations share some common logic, an abstract class with concrete methods is appropriate. If the only need is a contract with no shared implementation, an interface is cleaner.

Real-World Example — Delivery Notification System

The Business Problem

You are building the delivery notification system for a quick-commerce platform — similar to what Zepto or Swiggy Instamart runs for its real-time order updates. The system must send notifications through multiple channels — SMS, email, and in-app push. Each channel has different sending logic. The notification dispatcher must send through all configured channels without knowing which specific implementations it is dealing with.

Implementation

Java
1// File: NotificationChannel.java 2 3public interface NotificationChannel { 4 5 // Every channel must implement these 6 boolean send(String recipientId, String message); 7 String getChannelName(); 8 9 // Default — shared validation logic available to all channels 10 default boolean isValidMessage(String message) { 11 return message != null && !message.isBlank() && message.length() <= 500; 12 } 13}
Java
1// File: BaseNotificationChannel.java 2 3public abstract class BaseNotificationChannel implements NotificationChannel { 4 5 private final String channelName; 6 private int messagesSentToday; 7 8 protected BaseNotificationChannel(String channelName) { 9 this.channelName = channelName; 10 this.messagesSentToday = 0; 11 } 12 13 @Override 14 public String getChannelName() { return channelName; } 15 16 public int getMessagesSentToday() { return messagesSentToday; } 17 18 // Template method — enforces validation before sending 19 @Override 20 public final boolean send(String recipientId, String message) { 21 if (!isValidMessage(message)) { 22 System.out.println("[" + channelName + "] Invalid message — skipping."); 23 return false; 24 } 25 boolean result = doSend(recipientId, message); // subclass provides the actual send 26 if (result) messagesSentToday++; 27 return result; 28 } 29 30 // Abstract — each channel provides its own sending implementation 31 protected abstract boolean doSend(String recipientId, String message); 32}
Java
1// File: SmsChannel.java 2 3public class SmsChannel extends BaseNotificationChannel { 4 5 private final String smsApiEndpoint; 6 7 public SmsChannel(String smsApiEndpoint) { 8 super("SMS"); 9 this.smsApiEndpoint = smsApiEndpoint; 10 } 11 12 @Override 13 protected boolean doSend(String recipientId, String message) { 14 System.out.println(" [SMS] Sending to " + recipientId 15 + " via " + smsApiEndpoint + ": " + message); 16 return true; 17 } 18}
Java
1// File: EmailChannel.java 2 3public class EmailChannel extends BaseNotificationChannel { 4 5 private final String smtpServer; 6 private final String senderEmail; 7 8 public EmailChannel(String smtpServer, String senderEmail) { 9 super("Email"); 10 this.smtpServer = smtpServer; 11 this.senderEmail = senderEmail; 12 } 13 14 @Override 15 protected boolean doSend(String recipientId, String message) { 16 System.out.println(" [Email] From: " + senderEmail 17 + " via " + smtpServer 18 + " | To: " + recipientId + " | " + message); 19 return true; 20 } 21}
Java
1// File: PushChannel.java 2 3public class PushChannel extends BaseNotificationChannel { 4 5 private final String fcmApiKey; 6 7 public PushChannel(String fcmApiKey) { 8 super("Push"); 9 this.fcmApiKey = fcmApiKey; 10 } 11 12 @Override 13 protected boolean doSend(String recipientId, String message) { 14 System.out.println(" [Push] Sending to device: " + recipientId 15 + " [FCM key: " + fcmApiKey.substring(0, 6) + "...] | " + message); 16 return true; 17 } 18}
Java
1// File: NotificationDispatcher.java 2 3import java.util.List; 4 5public class NotificationDispatcher { 6 7 private final List<NotificationChannel> channels; 8 9 public NotificationDispatcher(List<NotificationChannel> channels) { 10 this.channels = channels; 11 } 12 13 // Works with any NotificationChannel — abstraction decouples dispatcher from implementations 14 public void dispatch(String recipientId, String message) { 15 System.out.println("Dispatching to " + channels.size() + " channel(s):"); 16 int successCount = 0; 17 for (NotificationChannel channel : channels) { 18 boolean sent = channel.send(recipientId, message); 19 if (sent) successCount++; 20 } 21 System.out.println("Sent via " + successCount + "/" + channels.size() + " channels.\n"); 22 } 23}
Java
1// File: NotificationDemo.java 2 3import java.util.List; 4 5public class NotificationDemo { 6 7 public static void main(String[] args) { 8 9 List<NotificationChannel> channels = List.of( 10 new SmsChannel("https://sms.twilio.com/api"), 11 new EmailChannel("smtp.sendgrid.net", "noreply@zepto.co"), 12 new PushChannel("AAAA_FCM_API_KEY_xyz") 13 ); 14 15 NotificationDispatcher dispatcher = new NotificationDispatcher(channels); 16 17 System.out.println("=== Order Shipped Notification ==="); 18 dispatcher.dispatch("USER-9876", 19 "Your order ORD-4521 has been shipped. Expected delivery: 2 hours."); 20 21 System.out.println("=== OTP Notification ==="); 22 dispatcher.dispatch("USER-1234", "Your OTP is 847291. Valid for 5 minutes."); 23 24 // Invalid message — too long or blank 25 System.out.println("=== Invalid Message Test ==="); 26 dispatcher.dispatch("USER-5555", ""); 27 } 28}
Output:
=== Order Shipped Notification ===
Dispatching to 3 channel(s):
  [SMS] Sending to USER-9876 via https://sms.twilio.com/api: Your order ORD-4521 has been shipped. Expected delivery: 2 hours.
  [Email] From: noreply@zepto.co via smtp.sendgrid.net | To: USER-9876 | Your order ORD-4521 has been shipped. Expected delivery: 2 hours.
  [Push] Sending to device: USER-9876 [FCM key: AAAA_F...] | Your order ORD-4521 has been shipped. Expected delivery: 2 hours.
Sent via 3/3 channels.

=== OTP Notification ===
Dispatching to 3 channel(s):
  [SMS] Sending to USER-1234 via https://sms.twilio.com/api: Your OTP is 847291. Valid for 5 minutes.
  [Email] From: noreply@zepto.co via smtp.sendgrid.net | To: USER-1234 | Your OTP is 847291. Valid for 5 minutes.
  [Push] Sending to device: USER-1234 [FCM key: AAAA_F...] | Your OTP is 847291. Valid for 5 minutes.
Sent via 3/3 channels.

=== Invalid Message Test ===
Dispatching to 3 channel(s):
[SMS] Invalid message — skipping.
[Email] Invalid message — skipping.
[Push] Invalid message — skipping.
Sent via 0/3 channels.

The NotificationDispatcher depends only on NotificationChannel — an interface. Adding a WhatsAppChannel requires creating one new class that implements NotificationChannel. The dispatcher, all existing channels, and every test remain unchanged. The abstraction layer absorbed the extension completely.

BaseNotificationChannel provides shared concrete behaviour — the validation-before-send template and the message counter — while keeping doSend() abstract. This is the layered abstraction pattern: interface defines the contract, abstract class provides partial implementation, concrete classes complete it.

Best Practices

Depend on abstractions — never on concrete implementations

The Dependency Inversion Principle states that high-level modules should depend on abstractions, not on concrete classes. A CheckoutService that declares PaymentGateway gateway as a field can work with any gateway. A CheckoutService that declares RazorpayGateway gateway is tightly coupled to one provider. Every time you write a type name in a parameter or field, ask: can I make this an interface or abstract class?

Use interfaces for pure contracts, abstract classes for shared implementation

If different implementations have nothing in common except the method signatures they must provide, use an interface. If they share some implementation — a validation step, a logging call, a counter — use an abstract class that provides the shared concrete methods and declares the variable parts as abstract. Both mechanisms serve abstraction; the right choice depends on what implementations share.

Keep abstract method count low — each one is a burden on every implementor

Every abstract method in an abstract class or interface is something every concrete subclass must implement. An interface with twelve abstract methods forces every implementor to write twelve methods — including mock implementations in tests. Keep the contract focused. If an interface has grown large, consider splitting it into smaller role-specific interfaces. This is the Interface Segregation Principle.

Name abstractions after what they do, not what they are

PaymentGateway names a capability — what objects of this type can do. AbstractPayment names a structural classification — what the type is in the hierarchy. The first is a better abstraction name because it focuses on behaviour. Interfaces especially should be named as capabilities or roles: Printable, Comparable, NotificationChannel, DataFetcher.

Common Mistakes

Mistake 1 — Instantiating an Abstract Class

Java
1public abstract class Shape { 2 public abstract double area(); 3} 4 5public class Main { 6 public static void main(String[] args) { 7 Shape shape = new Shape(); // compile error — Shape is abstract 8 // Cannot instantiate abstract class 9 } 10}
Compile error: Shape is abstract; cannot be instantiated

Abstract classes exist to be extended — they cannot be instantiated directly. To get an object, create a concrete subclass. To declare a variable, use the abstract type — Shape shape = new Circle(5.0) is valid because Circle is concrete.

Mistake 2 — Providing Implementation in Every Abstract Class Method

Java
1// Wrong — if all methods are concrete, why is the class abstract? 2public abstract class DataExporter { 3 public void export(String data) { 4 System.out.println("Exporting: " + data); // fully concrete 5 } 6 public void validate(String data) { 7 System.out.println("Validating: " + data); // fully concrete 8 } 9 // No abstract methods — this should just be a regular class 10}

An abstract class with no abstract methods can be declared as abstract to prevent instantiation, but this is rare. If there are no abstract methods, a regular class with a protected constructor usually serves better. The defining purpose of an abstract class is that it has at least one method that subclasses must implement.

Mistake 3 — Using Abstract Class When an Interface Is Sufficient

Java
1// Wrong — no shared state, no shared implementation needed 2// This should be an interface 3public abstract class Printable { 4 public abstract void print(); 5 public abstract String getContent(); 6 // No fields, no concrete methods — interface is cleaner 7} 8 9// Correct 10public interface Printable { 11 void print(); 12 String getContent(); 13}

When an abstract class has only abstract methods and no fields or concrete methods, it is functionally identical to an interface — but more restrictive, because a class can only extend one abstract class. Prefer interfaces for pure contracts; they allow implementing classes more flexibility.

Mistake 4 — Exposing Implementation Details Through the Abstract Interface

Java
1// Wrong — getInternalDbConnection() leaks implementation detail 2public interface ReportService { 3 List<Report> getReports(); 4 void generateReport(String type); 5 Connection getInternalDbConnection(); // leaks that this uses JDBC internally 6} 7 8// Correct — only expose what callers need 9public interface ReportService { 10 List<Report> getReports(); 11 void generateReport(String type); 12}

An abstract interface that exposes implementation-specific methods — internal connections, file handles, adapter instances — defeats the purpose of abstraction. Callers should see only the operations they need. Anything implementation-specific belongs in the concrete class, not in the interface.

Interview Questions

Q1. What is abstraction in Java and how is it achieved?

Abstraction is the OOP principle of hiding implementation complexity and exposing only essential behaviour through a clean interface. In Java, it is achieved through two mechanisms: abstract classes — which can have both abstract (bodyless) and concrete methods, can hold fields, and are extended using extends — and interfaces — which define a pure contract of method signatures that implementing classes must provide, using implements. Both allow callers to write code against the type definition rather than the specific implementation.

Q2. What is the difference between abstraction and encapsulation?

Abstraction hides complexity at the system level — it shows what an object does through an interface without revealing how it does it. Encapsulation hides data at the class level — it protects internal fields from direct external access using private modifiers and controlled methods. Abstraction is about the contract presented to callers across class boundaries. Encapsulation is about protecting state within a single class. Both hide things — abstraction hides the implementation; encapsulation hides the data.

Q3. What is the difference between an abstract class and an interface in Java?

An abstract class can have concrete and abstract methods, can hold instance fields, has constructors, and is extended with extends — a class can extend only one. An interface defines abstract method signatures, can have default and static methods since Java 8, can only hold constants (public static final), has no constructors, and is implemented with implements — a class can implement multiple interfaces. Use an abstract class when implementations share concrete behaviour or state. Use an interface for a pure contract or when multiple inheritance of type is needed.

Q4. Can you instantiate an abstract class in Java?

No. An abstract class cannot be instantiated with new. It exists to be extended by concrete subclasses. However, you can declare a variable of the abstract class type and assign a concrete subclass object to it — this is upcasting, which enables runtime polymorphism. An abstract class with no abstract methods can technically exist, and is used to prevent direct instantiation while still providing shared implementation.

Q5. What is the Template Method Pattern and how does it use abstraction?

The Template Method Pattern defines the skeleton of an algorithm in an abstract class — using a concrete method that calls abstract methods in a fixed sequence. The abstract class controls the flow; the subclasses provide the variable pieces. generate() in ReportGenerator calls fetchData() and formatReport() — both abstract — in a fixed order. Each subclass implements these steps differently without altering the overall flow. This is abstraction enabling the Open-Closed Principle: the algorithm is closed for modification but open for extension through new subclasses.

Q6. What is the Dependency Inversion Principle and how does abstraction support it?

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules — both should depend on abstractions. A CheckoutService that depends on PaymentGateway (an interface) rather than RazorpayGateway (a concrete class) can work with any gateway implementation — present or future — without modification. Abstraction is the mechanism that makes this possible: the interface defines the contract both the high-level service and low-level implementation depend on. This is why every major Java framework — Spring, Jakarta EE — uses interface-based injection rather than concrete class injection.

FAQs

What is abstraction in Java in simple terms?

Abstraction means hiding the complex inner workings and showing only what is needed. When you call gateway.initiatePayment(), you do not need to know whether it is sending an HTTP request to Razorpay or PayU. You know what the method does — you do not know how. That separation of what from how is abstraction.

What is an abstract class in Java?

An abstract class is a class declared with the abstract keyword that cannot be instantiated directly. It can contain abstract methods — which subclasses must implement — and concrete methods with full implementations that subclasses inherit. It is used when multiple classes share some common behaviour but each also has a unique implementation of certain operations.

What is the difference between abstract class and interface in Java?

An abstract class can have fields, constructors, and both concrete and abstract methods — a class extends only one. An interface has only abstract methods, default methods (Java 8+), and constants — a class can implement many. Use an abstract class when implementations share state or concrete behaviour. Use an interface for a pure contract or when you need multiple type inheritance.

Can an abstract class have a constructor in Java?

Yes. An abstract class can have constructors. They are not called directly — since you cannot instantiate the abstract class — but they are called through super() in the child constructor. This is how the abstract class initialises its own fields before the child sets up its own.

Can an interface have a constructor in Java?

No. Interfaces cannot have constructors because they cannot be instantiated. Since Java 8, they can have default and static method implementations, but all state initialisation must happen in the implementing class.

What is the difference between interface and abstract class for testability?

Both improve testability by allowing mock implementations to be injected. Interfaces are typically preferred for testability because creating a mock interface requires less code — just implement the method signatures. Mocking abstract classes requires subclassing and potentially setting up inherited concrete state. Most mocking frameworks (Mockito) work equally well with both, but interfaces produce cleaner and more minimal test doubles.

Summary

Abstraction is the principle of hiding complexity and exposing only the contract. Java provides two mechanisms: abstract classes for partial abstraction — mixing concrete shared behaviour with abstract variable behaviour — and interfaces for pure abstraction — defining what must be done without any implementation. Both decouple callers from implementations, making systems extensible without modification.

The Template Method Pattern demonstrates abstract classes delivering abstraction: the flow is fixed, the steps are variable. The Dependency Inversion Principle demonstrates interfaces delivering abstraction: depend on the contract, not the class name. Both patterns appear in virtually every production Java codebase and every Java framework.

For interviews, be ready to explain the difference between abstraction and encapsulation precisely — they hide different things at different levels. Know the abstract class vs interface trade-off and when to use each. Demonstrate the Template Method Pattern and explain how it enables the open-closed principle. These points appear consistently in both service-based recall questions and product-based design discussions.

What to Read Next

TopicLink
How interfaces define pure contracts and support multiple type inheritanceInterfaces →
How abstract classes provide partial implementation in inheritance hierarchiesAbstract Classes →
How encapsulation protects data while abstraction hides complexityEncapsulation →
How polymorphism uses abstraction to enable runtime method dispatchPolymorphism →
How inheritance provides the class hierarchy that abstraction is built onInheritance →
Java Abstraction | DevStackFlow