Home Java Java Interfaces Explained — Contracts, Default Methods and Real-World Patterns

Java Interfaces Explained — Contracts, Default Methods and Real-World Patterns

In Plain English 🔥
Imagine every electrical outlet in your house follows the same standard shape. Your phone charger, your lamp, your laptop — they all just plug in and work, even though they're completely different devices made by different companies. Nobody cares how the device works internally; it just has to fit the socket. A Java interface is that socket — it's a contract that says 'if you want to plug into this system, here's the shape you must have'. The class is the device; it can work however it likes inside, as long as it honors the contract.
⚡ Quick Answer
Imagine every electrical outlet in your house follows the same standard shape. Your phone charger, your lamp, your laptop — they all just plug in and work, even though they're completely different devices made by different companies. Nobody cares how the device works internally; it just has to fit the socket. A Java interface is that socket — it's a contract that says 'if you want to plug into this system, here's the shape you must have'. The class is the device; it can work however it likes inside, as long as it honors the contract.

Every serious Java codebase you'll ever work on leans heavily on interfaces. They're the backbone of the Collections framework, the secret behind Spring's dependency injection, and the reason you can swap a MySQL database for PostgreSQL without rewriting your entire application. Interfaces aren't just a syntax feature — they're a design philosophy baked into the language from day one.

The problem they solve is coupling. Without interfaces, your code has to know exactly what class it's talking to. Swap that class for a different one, and you're rewriting callers everywhere. With an interface in between, your code only knows the contract — the 'what', not the 'who'. The concrete class behind it can change, grow, or be replaced entirely, and the rest of your system doesn't feel a thing. That's the Open/Closed Principle in action, and interfaces are its primary vehicle in Java.

By the end of this article you'll know why interfaces exist (not just how to declare them), how default and static methods changed the game in Java 8, how to spot when an interface is the right tool versus an abstract class, and how to write code that senior developers recognize as well-designed. You'll also see three patterns — Strategy, Callback, and Marker — that you'll encounter in real codebases within your first week on any Java project.

What an Interface Actually Is — The Contract Model

An interface is a pure description of capability. It says: 'any class that claims to be Printable must be able to print itself'. It doesn't say how. It doesn't provide a body. It just defines the method signature and leaves the implementation entirely to whoever signs up.

This is the key mental shift. Stop thinking of interfaces as 'classes without bodies' — that's a syntax description, not a conceptual one. Think of them as roles or capabilities that multiple unrelated classes can share. A Dog and a PDF Document are nothing alike, but both can implement Printable. The interface doesn't care about their relationship; it only cares about the capability.

Before Java 8, interfaces could only contain abstract method signatures and constants. Java 8 added default methods (implementations that live inside the interface itself) and static methods. Java 9 added private methods for code reuse within the interface. Each addition was driven by a real-world problem: how do you evolve an interface without breaking every class that implements it? Default methods were the answer, and they changed how library authors design APIs.

PaymentProcessor.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
// This file demonstrates the core contract model of interfaces.
// Imagine we're building a payment system that must support
// multiple payment providers (Stripe, PayPal, bank transfer).
// We don't want our checkout code tied to any specific provider.

// The interface defines the CONTRACT — what any payment processor must do.
public interface PaymentProcessor {

    // Abstract method: every implementor MUST provide this.
    // No body here — the 'how' is left to the class.
    boolean processPayment(String accountId, double amountInDollars);

    // Default method: added in Java 8.
    // Implementors INHERIT this but can override it if they need to.
    // This lets us add behaviour to the interface without breaking existing classes.
    default String getReceiptMessage(double amountInDollars) {
        return String.format("Payment of $%.2f processed successfully.", amountInDollars);
    }

    // Static method: belongs to the interface itself, not to any instance.
    // Call it as PaymentProcessor.validateAmount(...), never on an instance.
    static boolean validateAmount(double amountInDollars) {
        return amountInDollars > 0 && amountInDollars <= 10_000;
    }
}

// StripeProcessor signs the contract by using 'implements'.
// It MUST provide a body for processPayment, or the compiler refuses to compile it.
class StripeProcessor implements PaymentProcessor {

    @Override
    public boolean processPayment(String accountId, double amountInDollars) {
        // In real life this would call the Stripe SDK.
        // We're simulating success for any valid account.
        System.out.println("[Stripe] Charging account: " + accountId);
        return accountId != null && !accountId.isBlank();
    }
    // Note: we do NOT override getReceiptMessage — we're happy with the default.
}

// PayPalProcessor also signs the same contract.
// It provides a completely different implementation — but the same signature.
class PayPalProcessor implements PaymentProcessor {

    @Override
    public boolean processPayment(String accountId, double amountInDollars) {
        System.out.println("[PayPal] Redirecting to PayPal for account: " + accountId);
        return accountId != null && !accountId.isBlank();
    }

    // This class DOES override the default — PayPal wants a custom receipt format.
    @Override
    public String getReceiptMessage(double amountInDollars) {
        return String.format("PayPal confirmed your payment of $%.2f. Check your email.", amountInDollars);
    }
}

// Checkout doesn't know or care whether it's talking to Stripe or PayPal.
// It only knows the PaymentProcessor contract. This is the whole point.
class Checkout {

    private final PaymentProcessor processor; // typed to the INTERFACE, not a concrete class

    public Checkout(PaymentProcessor processor) {
        this.processor = processor;
    }

    public void completePurchase(String accountId, double amount) {
        // Use the static utility to validate before we even try
        if (!PaymentProcessor.validateAmount(amount)) {
            System.out.println("Invalid amount: $" + amount);
            return;
        }

        boolean success = processor.processPayment(accountId, amount);

        if (success) {
            // getReceiptMessage calls whichever version the concrete class provides
            System.out.println(processor.getReceiptMessage(amount));
        } else {
            System.out.println("Payment failed. Please try again.");
        }
    }
}

// Main entry point — wire it all together
class Main {
    public static void main(String[] args) {

        // Swap the concrete class here and NOTHING else changes
        Checkout stripeCheckout = new Checkout(new StripeProcessor());
        stripeCheckout.completePurchase("acct_1234", 49.99);

        System.out.println("---");

        Checkout paypalCheckout = new Checkout(new PayPalProcessor());
        paypalCheckout.completePurchase("pp_user_99", 49.99);

        System.out.println("---");

        // Test the static validator directly on the interface
        Checkout invalidCheckout = new Checkout(new StripeProcessor());
        invalidCheckout.completePurchase("acct_5678", -5.00);
    }
}
▶ Output
[Stripe] Charging account: acct_1234
Payment of $49.99 processed successfully.
---
[PayPal] Redirecting to PayPal for account: pp_user_99
PayPal confirmed your payment of $49.99. Check your email.
---
Invalid amount: $-5.0
⚠️
Pro Tip:Always declare your variable type as the interface (PaymentProcessor processor), never the concrete class (StripeProcessor processor). This single habit is the difference between flexible and brittle code. The moment you type the concrete class, you've locked yourself in.

Multiple Interface Implementation — Why Java Chose This Over Multiple Inheritance

Java deliberately does not allow a class to extend more than one class. The reason is the 'Diamond Problem': if ClassA and ClassB both define a method called shutdown(), and ClassC extends both, which shutdown() does ClassC inherit? There's no safe answer, so Java simply forbids it.

But a class CAN implement multiple interfaces. Why is that safe? Because (before Java 8) interfaces had no method bodies, so there was nothing to conflict. The implementing class always provides the one true body, resolving any ambiguity by definition.

With Java 8's default methods, a soft version of the Diamond Problem returned. If two interfaces both provide a default method with the same signature, Java forces you to override it in the implementing class — no ambiguity is allowed to slip through silently. The compiler just refuses to build until you resolve it explicitly.

This design means a class can wear many hats. A DatabaseLogger can simultaneously be a Logger, an AutoCloseable (for try-with-resources), and a Serializable — three completely unrelated capabilities, all layered on via interfaces. No inheritance tree required.

SmartDevice.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
// A smart home device needs several unrelated capabilities.
// Using multiple interfaces lets us compose those capabilities cleanly.

// Capability 1: the device can be turned on and off
interface Switchable {
    void turnOn();
    void turnOff();

    default String getStatusMessage() {
        return "Device status updated."; // generic default
    }
}

// Capability 2: the device can report its power usage
interface PowerMonitor {
    double getCurrentWattage();

    default String getStatusMessage() {
        // Same method name as Switchable.getStatusMessage() — conflict!
        return "Power monitor status updated.";
    }
}

// Capability 3: the device can be scheduled
interface Schedulable {
    void scheduleOnAt(int hourOfDay);
    void scheduleOffAt(int hourOfDay);
}

// SmartBulb implements ALL THREE interfaces simultaneously.
// Because both Switchable and PowerMonitor have a conflicting default method,
// the compiler FORCES us to override it here. No silent ambiguity.
public class SmartBulb implements Switchable, PowerMonitor, Schedulable {

    private boolean isOn = false;
    private double wattage;
    private final String deviceName;

    public SmartBulb(String deviceName, double wattage) {
        this.deviceName = deviceName;
        this.wattage = wattage;
    }

    @Override
    public void turnOn() {
        isOn = true;
        System.out.println(deviceName + " turned ON.");
    }

    @Override
    public void turnOff() {
        isOn = false;
        System.out.println(deviceName + " turned OFF.");
    }

    @Override
    public double getCurrentWattage() {
        // Only draw power when on
        return isOn ? wattage : 0.0;
    }

    @Override
    public void scheduleOnAt(int hourOfDay) {
        System.out.printf("%s scheduled to turn ON at %02d:00%n", deviceName, hourOfDay);
    }

    @Override
    public void scheduleOffAt(int hourOfDay) {
        System.out.printf("%s scheduled to turn OFF at %02d:00%n", deviceName, hourOfDay);
    }

    // We MUST override this because both Switchable and PowerMonitor define it.
    // We can also call either parent's default using InterfaceName.super.method()
    @Override
    public String getStatusMessage() {
        // Explicitly delegate to Switchable's version, or write our own — your call.
        return deviceName + " is currently " + (isOn ? "ON" : "OFF")
               + ", drawing " + getCurrentWattage() + "W."; // our own implementation
    }

    public static void main(String[] args) {
        SmartBulb livingRoomBulb = new SmartBulb("Living Room Bulb", 9.5);

        livingRoomBulb.scheduleOnAt(18);
        livingRoomBulb.turnOn();
        System.out.println(livingRoomBulb.getStatusMessage());

        livingRoomBulb.scheduleOffAt(23);
        livingRoomBulb.turnOff();
        System.out.println(livingRoomBulb.getStatusMessage());

        // SmartBulb can be referenced by ANY of its interface types
        Switchable switchable = livingRoomBulb;
        PowerMonitor monitor = livingRoomBulb;
        // Each reference only exposes methods from that interface — intentional
        System.out.println("Wattage via monitor ref: " + monitor.getCurrentWattage() + "W");
    }
}
▶ Output
Living Room Bulb scheduled to turn ON at 18:00
Living Room Bulb turned ON.
Living Room Bulb is currently ON, drawing 9.5W.
Living Room Bulb scheduled to turn OFF at 23:00
Living Room Bulb turned OFF.
Living Room Bulb is currently OFF, drawing 0.0W.
Wattage via monitor ref: 0.0W
⚠️
Watch Out:If two interfaces you implement both declare a default method with the same name and signature, your code will NOT compile until you override it in your class. The compiler error reads: 'class SmartBulb inherits unrelated defaults'. Don't panic — just override the method and call InterfaceName.super.methodName() if you want to reuse one of the defaults.

Three Patterns You'll See in Every Real Java Codebase

Knowing interface syntax is table stakes. What separates junior from senior developers is recognizing the patterns that interfaces enable. Here are the three you'll see constantly.

Strategy Pattern — swap algorithms at runtime without changing the code that uses them. Sorting strategies, pricing strategies, validation strategies — any 'pluggable behaviour' is Strategy. Spring's PasswordEncoder is a perfect real-world example.

Callback / Functional Interface — pass behaviour as a parameter. This is how Java's Comparator, Runnable, and all the java.util.function types work. Java 8 lambdas are just a shorthand for implementing a single-method interface (a functional interface). If you've written list.sort((a, b) -> a.compareTo(b)), you've already used this pattern.

Marker Interface — an interface with no methods at all, used purely to tag a class. Serializable is the classic example. The JVM checks instanceof Serializable before serializing an object. It's a capability flag, not a behavioural contract. Modern Java often prefers annotations for this, but you'll still see marker interfaces in legacy codebases.

ReportGenerator.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

// ── STRATEGY PATTERN ──────────────────────────────────────────────
// Define a family of formatting algorithms behind one interface.
// The ReportGenerator doesn't care which format — it just calls format().

interface ReportFormatter {
    String format(String title, List<String> rows);
}

class CsvFormatter implements ReportFormatter {
    @Override
    public String format(String title, List<String> rows) {
        StringBuilder csv = new StringBuilder(title).append("\n");
        rows.forEach(row -> csv.append(row).append("\n"));
        return csv.toString();
    }
}

class HtmlFormatter implements ReportFormatter {
    @Override
    public String format(String title, List<String> rows) {
        StringBuilder html = new StringBuilder("<h1>").append(title).append("</h1><ul>");
        rows.forEach(row -> html.append("<li>").append(row).append("</li>"));
        html.append("</ul>");
        return html.toString();
    }
}

// The generator is blissfully unaware of CSV vs HTML — that's the strategy doing its job.
class ReportGenerator {
    private final ReportFormatter formatter; // holds the strategy

    public ReportGenerator(ReportFormatter formatter) {
        this.formatter = formatter;
    }

    public String generate(String title, List<String> data) {
        return formatter.format(title, data);
    }
}

// ── FUNCTIONAL INTERFACE / CALLBACK PATTERN ────────────────────────
// @FunctionalInterface enforces that this interface stays single-method.
// That means callers can use a lambda instead of writing a full class.
@FunctionalInterface
interface SalesFilter {
    boolean test(double saleAmount); // one abstract method = functional interface
}

class SalesAnalyzer {
    private final List<Double> sales;

    public SalesAnalyzer(List<Double> sales) {
        this.sales = sales;
    }

    // Accepts a SalesFilter — but callers pass a lambda. Clean and readable.
    public long countSalesMatching(SalesFilter filter) {
        return sales.stream()
                    .filter(filter::test) // the callback fires for each sale
                    .count();
    }
}

// ── MARKER INTERFACE PATTERN ───────────────────────────────────────
// No methods — purely a tag that says 'this type can be exported'
interface Exportable {
    // intentionally empty — it's a capability flag
}

class SalesReport implements Exportable {
    public final String content;
    public SalesReport(String content) { this.content = content; }
}

class InternalMemo {
    // Does NOT implement Exportable — should never leave the system
    public final String content;
    public InternalMemo(String content) { this.content = content; }
}

class ExportService {
    public void export(Object document) {
        if (document instanceof Exportable) {
            System.out.println("Exporting document to external system...");
        } else {
            // The marker stopped a dangerous operation at runtime
            System.out.println("Export BLOCKED — document is not marked Exportable.");
        }
    }
}

// ── DEMO ───────────────────────────────────────────────────────────
public class ReportGenerator {
    public static void main(String[] args) {
        List<String> salesData = Arrays.asList("Alice,500", "Bob,1200", "Carol,850");

        // Strategy: swap formatters without touching ReportGenerator
        ReportGenerator csvGen = new ReportGenerator(new CsvFormatter());
        System.out.println("=== CSV Output ===");
        System.out.println(csvGen.generate("Q1 Sales", salesData));

        ReportGenerator htmlGen = new ReportGenerator(new HtmlFormatter());
        System.out.println("=== HTML Output ===");
        System.out.println(htmlGen.generate("Q1 Sales", salesData));

        // Functional interface: pass logic as a lambda — no class needed
        SalesAnalyzer analyzer = new SalesAnalyzer(Arrays.asList(200.0, 450.0, 1100.0, 75.0, 600.0));
        long highValueSales = analyzer.countSalesMatching(amount -> amount > 400.0);
        System.out.println("High-value sales (over $400): " + highValueSales);

        // Marker interface: tag-based access control
        ExportService exporter = new ExportService();
        exporter.export(new SalesReport("Q1 data"));
        exporter.export(new InternalMemo("Do not share"));
    }
}
▶ Output
=== CSV Output ===
Q1 Sales
Alice,500
Bob,1200
Carol,850

=== HTML Output ===
<h1>Q1 Sales</h1><ul><li>Alice,500</li><li>Bob,1200</li><li>Carol,850</li></ul>
High-value sales (over $400): 3
Exporting document to external system...
Export BLOCKED — document is not marked Exportable.
🔥
Interview Gold:When asked about the Strategy Pattern in an interview, don't just define it — mention a concrete Java API example. Say: 'Java's own Comparator is a Strategy. When you call Collections.sort(list, comparator), you're injecting a sorting strategy at runtime. The sort method doesn't care how comparison works — that's the strategy's job.' That answer shows you understand patterns in context, not just in theory.

Interface vs Abstract Class — Choosing the Right Tool

This is one of the most common design decisions you'll face, and the wrong choice creates technical debt that's painful to unwind later. The rule of thumb most senior devs use: reach for an interface when you're defining a capability that unrelated types share; reach for an abstract class when you're defining a base type in a clear inheritance hierarchy with shared state or construction logic.

Interfaces cannot hold instance state (fields). They can have constants (public static final fields), but they can't have instance variables that track data per object. Abstract classes can. So if your contract requires shared setup — say, all report generators need a Logger and an output path — an abstract class lets you declare and initialise those fields once.

The practical modern advice: start with an interface. If you later find yourself duplicating code across implementations, introduce an abstract class that implements your interface and holds the shared logic. The interface stays as the public contract; the abstract class is an internal convenience. This is exactly how the Java Collections framework is designed — List is an interface, AbstractList is the helper abstract class, ArrayList is the concrete implementation.

NotificationSystem.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
// This example shows the interface + abstract class combo.
// The interface defines the public contract.
// The abstract class handles boilerplate common to all notifications.
// Concrete classes only write the code that's unique to them.

// PUBLIC CONTRACT — what every notification channel must do
interface NotificationSender {
    void send(String recipientId, String message);
    boolean isAvailable(); // is this channel currently operational?
}

// ABSTRACT BASE — handles the retry logic so every subclass gets it free
// This class is not public API — it's an internal implementation helper.
abstract class AbstractNotificationSender implements NotificationSender {

    // Shared state: every channel tracks how many retries are allowed
    private final int maxRetries;
    private final String channelName;

    protected AbstractNotificationSender(String channelName, int maxRetries) {
        this.channelName = channelName;
        this.maxRetries = maxRetries;
    }

    // Template method: handles retry logic. Delegates the actual send to subclasses.
    public void sendWithRetry(String recipientId, String message) {
        int attempts = 0;
        while (attempts < maxRetries) {
            if (!isAvailable()) {
                System.out.printf("[%s] Channel unavailable. Attempt %d of %d.%n",
                                  channelName, attempts + 1, maxRetries);
                attempts++;
                continue;
            }
            send(recipientId, message);
            System.out.printf("[%s] Sent on attempt %d.%n", channelName, attempts + 1);
            return; // success — exit
        }
        System.out.printf("[%s] Failed after %d attempts.%n", channelName, maxRetries);
    }

    protected String getChannelName() {
        return channelName;
    }
}

// CONCRETE CLASS 1 — only implements what's unique to email
class EmailSender extends AbstractNotificationSender {

    private final boolean smtpServerReachable;

    public EmailSender(boolean smtpServerReachable) {
        super("Email", 3); // pass shared config to abstract class
        this.smtpServerReachable = smtpServerReachable;
    }

    @Override
    public void send(String recipientId, String message) {
        System.out.printf("[Email] Sending to %s: \"%s\"%n", recipientId, message);
    }

    @Override
    public boolean isAvailable() {
        return smtpServerReachable;
    }
}

// CONCRETE CLASS 2 — only implements what's unique to SMS
class SmsSender extends AbstractNotificationSender {

    private final boolean carrierApiOnline;

    public SmsSender(boolean carrierApiOnline) {
        super("SMS", 2);
        this.carrierApiOnline = carrierApiOnline;
    }

    @Override
    public void send(String recipientId, String message) {
        System.out.printf("[SMS] Texting %s: \"%s\"%n", recipientId, message);
    }

    @Override
    public boolean isAvailable() {
        return carrierApiOnline;
    }
}

public class NotificationSystem {
    public static void main(String[] args) {
        // Email is up, SMS carrier is down
        EmailSender email = new EmailSender(true);
        SmsSender sms = new SmsSender(false);

        System.out.println("--- Email notification ---");
        email.sendWithRetry("user@example.com", "Your order has shipped!");

        System.out.println("--- SMS notification ---");
        sms.sendWithRetry("+1-555-0199", "Your order has shipped!");

        // External code still uses the interface type — not EmailSender, not AbstractNotificationSender
        NotificationSender sender = new EmailSender(true);
        sender.send("boss@company.com", "Server is healthy.");
    }
}
▶ Output
--- Email notification ---
[Email] Sending to user@example.com: "Your order has shipped!"
[Email] Sent on attempt 1.
--- SMS notification ---
[SMS] Channel unavailable. Attempt 1 of 2.
[SMS] Channel unavailable. Attempt 2 of 2.
[SMS] Failed after 2 attempts.
[Email] Sending to boss@company.com: "Server is healthy."
⚠️
Pro Tip:The interface + abstract class + concrete class layering is called the 'Skeletal Implementation' pattern (Joshua Bloch coined it in Effective Java). It gives you maximum flexibility — other developers can implement your interface from scratch if they want, but they can also extend your abstract class to save work. Never force them into one path.
Feature / AspectInterfaceAbstract Class
Can hold instance state (fields)No — only constants (public static final)Yes — any access modifier
Multiple inheritanceA class can implement many interfacesA class can only extend one abstract class
ConstructorNo constructor allowedYes — can define constructors
Method implementationsDefault & static methods only (Java 8+)Any mix of abstract and concrete methods
Use when...Defining a capability shared across unrelated typesSharing code/state within a clear type hierarchy
Real-world Java exampleList, Comparable, Runnable, AutoCloseableAbstractList, HttpServlet, AbstractQueuedSynchronizer
Performance overheadSlight (virtual dispatch), negligible in practiceSame virtual dispatch — no meaningful difference
Can be used as lambda targetYes — if it has exactly one abstract methodNo — lambdas only target functional interfaces

🎯 Key Takeaways

    ⚠ Common Mistakes to Avoid

    • Mistake 1: Putting too much into one interface — violating Interface Segregation Principle — Symptom: implementing classes have to provide stub/empty bodies for methods they don't use (e.g. throwing UnsupportedOperationException). Fix: split the bloated interface into smaller, focused ones. If a class says 'I don't need method X', that's the interface telling you it does too much.
    • Mistake 2: Declaring fields in an interface without realising they're automatically public static final — Symptom: you expect each implementing class to have its own copy of the field, but every class shares the same constant and you can't change it per instance. Fix: if you need per-instance mutable state, it belongs in the implementing class or an abstract class — not the interface.
    • Mistake 3: Using a concrete class as the variable type instead of the interface — Example: writing ArrayList names = new ArrayList<>() instead of List names = new ArrayList<>() — Symptom: when you need to swap ArrayList for a LinkedList or a synchronized list, you have to hunt down and change every declaration. Fix: always program to the interface. Declare List, Map, Set — the right-hand side can be whatever concrete type you need.

    Interview Questions on This Topic

    • QWhat is the difference between an interface and an abstract class, and how do you decide which one to use in a design?
    • QJava 8 introduced default methods in interfaces. Why was this added, and does it reintroduce the Diamond Problem? How does Java resolve it?
    • QWhat is a functional interface, and how do Java lambdas relate to interfaces under the hood? Can you give an example of writing a custom functional interface and consuming it with a lambda?
    🔥
    TheCodeForge Editorial Team Verified Author

    Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

    ← PreviousAbstraction in JavaNext →Abstract Classes in Java
    Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged