Java: Interfaces & Abstract Classes

20 Jul 2026 13 min read
1
In this article, we explore interfaces and abstract classes in depth, understand their differences, examine their modern capabilities, and learn when each should be used in real-world applications.

What is an Interface?

An interface defines a contract that specifies what a class can do without dictating how it performs those operations.

Any class that implements an interface agrees to provide implementations for its abstract methods.

Unlike inheritance, where a class acquires behavior from a parent class, an interface focuses on defining capabilities that multiple unrelated classes can share.

For example, different payment providers may process payments differently, but they all expose the same operation.
public interface PaymentService {
    void processPayment(double amount);
}
Any class implementing this interface must provide its own implementation.
public class CreditCardPayment implements PaymentService {

    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment.");
    }
}
public class UPIPayment implements PaymentService {

    @Override
    public void processPayment(double amount) {
        System.out.println("Processing UPI payment.");
    }
}
Although both classes implement the same interface, each performs the payment differently.

Interfaces Enable Polymorphism

One of the biggest advantages of interfaces is that application code depends on the interface rather than a specific implementation.

For example, the following method works with any payment provider implementing the interface.
public class CheckoutService {

    public void checkout(PaymentService paymentService) {
        paymentService.processPayment(5000);
    }
}
The checkout service does not know whether the payment is processed using a credit card, UPI, or any future payment mechanism.

Interfaces promote loose coupling by allowing application code to depend on contracts instead of concrete implementations.

Interface Inheritance

Interfaces can inherit from other interfaces using the extends keyword.

Unlike classes, an interface may extend multiple interfaces, allowing it to combine multiple contracts into a single, more specialized contract.

When an interface extends another interface, it inherits all of its abstract, default, and static method declarations (although static methods are not inherited by implementing classes).

Any class implementing the child interface must satisfy the contracts defined by all parent interfaces.

Consider the following example.
public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}

public interface MultiFunctionDevice extends Printer, Scanner {
    void fax();
}
The MultiFunctionDevice interface combines the contracts of both Printer and Scanner while introducing its own operation.

A class implementing the child interface must implement every inherited method.
public class OfficePrinter implements MultiFunctionDevice {
    @Override
    public void print() {
        System.out.println("Printing...");
    }

    @Override
    public void scan() {
        System.out.println("Scanning...");
    }

    @Override
    public void fax() {
        System.out.println("Faxing...");
    }
}
This allows larger contracts to be composed from smaller, reusable interfaces, promoting modular and maintainable API design.

The Java Collections Framework makes extensive use of interface inheritance.

For example, the List, Queue, and Set interfaces all extend the Collection interface, inheriting common operations while defining their own specialized behavior.

What is an Abstract Class?

An abstract class is a partially implemented class that cannot be instantiated directly.

It provides a common base implementation for related classes while allowing subclasses to provide implementations for operations that vary.

Unlike an interface, an abstract class can contain both abstract methods and fully implemented methods.

It can also define constructors, instance variables, and common business logic shared by all subclasses.

Consider an application managing different types of employees.
public abstract class Employee {

    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public void printName() {
        System.out.println(name);
    }

    public abstract double calculateSalary();
}
Each employee type calculates its salary differently.
public class FullTimeEmployee extends Employee {

    public FullTimeEmployee(String name) {
        super(name);
    }

    @Override
    public double calculateSalary() {
        return 80000;
    }
}
public class Contractor extends Employee {

    public Contractor(String name) {
        super(name);
    }

    @Override
    public double calculateSalary() {
        return 50000;
    }
}
Both subclasses inherit the common state and behavior from the abstract class while implementing their own salary calculation logic.

An abstract class reduces code duplication by centralizing shared fields, constructors, validation logic, and utility methods.

Subclasses inherit this common functionality while implementing only the behavior that varies.

Interface vs Abstract Class

Although both interfaces and abstract classes are used to achieve abstraction, they are designed to solve different problems.

The following table summarizes the major differences.
Feature Interface Abstract Class
Purpose Defines a contract or capability Provides a common base implementation
Inheritance A class can implement multiple interfaces A class can extend only one abstract class
Constructors Not allowed Supported
Instance Variables Not allowed (only constants) Supported
Method Types Abstract, default, static and private methods Abstract and concrete methods
State Cannot maintain object state Can maintain object state
Relationship Represents a "can-do" capability Represents an "is-a" relationship
Typical Use Service contracts, APIs, plugins Common business logic shared by related classes

Multiple Inheritance with Interfaces

Java does not support multiple inheritance of classes.

A class can extend only one superclass because inheriting implementation from multiple classes can lead to ambiguity, commonly known as the Diamond Problem.

However, Java does support multiple inheritance of type through interfaces.

A class can implement multiple interfaces simultaneously, allowing it to expose multiple independent capabilities without inheriting conflicting state.

For example, consider a smart television that supports both internet connectivity and voice control.
public interface InternetEnabled {
    void connectToInternet();
}

public interface VoiceControlled {
    void processVoiceCommand();
}
A class can implement both interfaces.
public class SmartTV implements InternetEnabled, VoiceControlled {

    @Override
    public void connectToInternet() {
        System.out.println("Connected to Wi-Fi.");
    }

    @Override
    public void processVoiceCommand() {
        System.out.println("Voice command received.");
    }
}
The SmartTV class inherits both capabilities while remaining free to provide its own implementation for each operation.

Multiple interface inheritance allows a class to combine several independent behaviors without creating deep inheritance hierarchies.

The Diamond Problem

Suppose Java allowed multiple inheritance of classes.
class A {
    void display() {
        System.out.println("A");
    }
}

class B extends A {
    @Override
    void display() {
        System.out.println("B");
    }
}

class C extends A {
    @Override
    void display() {
        System.out.println("C");
    }
}

// Not allowed in Java
class D extends B, C {
}
If class D called display(), the JVM would not know whether to invoke the implementation from B or C.

This ambiguity is known as the Diamond Problem, and it is one of the primary reasons Java prohibits multiple inheritance of classes.

How Interfaces Avoid the Diamond Problem

Traditional interfaces contained only abstract methods, so there was no inherited implementation to cause ambiguity.

Since Java 8, interfaces can also define default methods, which introduces the possibility of conflicts when multiple interfaces provide the same default implementation.

Java resolves this by requiring the implementing class to explicitly override the conflicting method.
public interface Camera {
    default void start() {
        System.out.println("Camera started.");
    }
}

public interface MusicPlayer {
    default void start() {
        System.out.println("Music started.");
    }
}
The implementing class must resolve the conflict.
public class Smartphone implements Camera, MusicPlayer {
    @Override
    public void start() {
        System.out.println("Starting smartphone.");
    }
}
By forcing the developer to resolve the ambiguity explicitly, Java avoids the classic Diamond Problem while still allowing multiple interface inheritance.

Default Methods

Prior to Java 8, interfaces could contain only abstract methods. Every implementing class was required to provide an implementation for each method.

This created a compatibility problem. If a new method was added to a widely used interface, every existing implementation would immediately fail to compile.

To solve this problem, Java 8 introduced default methods.

A default method provides a concrete implementation directly inside the interface, allowing existing implementations to continue working without modification.

A default method is declared using the default keyword.
public interface Logger {
    void log(String message);

    default void logError(String message) {
        System.out.println("ERROR: " + message);
    }
}
A class implementing the interface automatically inherits the default implementation.
public class ConsoleLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println(message);
    }
}
The inherited method can be used directly.
Logger logger = new ConsoleLogger();

logger.log("Application started.");
logger.logError("Connection failed.");

Overriding Default Methods

An implementing class may override a default method if custom behavior is required.
public class FileLogger implements Logger {

    @Override
    public void log(String message) {
        System.out.println(message);
    }

    @Override
    public void logError(String message) {
        System.out.println("Writing error to file.");
    }
}
The class is free to use the inherited implementation or replace it with its own. Default methods allow Java libraries to evolve without breaking existing implementations.

For example, the Java Collections Framework introduced several default methods, such as forEach(), removeIf(), and replaceAll(), without requiring every existing collection implementation to be rewritten.

Static Methods in Interfaces

Java 8 introduced static methods in interfaces, allowing utility methods that are closely related to the interface to be defined within the interface itself.

Unlike default methods, static methods do not belong to implementing classes. They belong to the interface and can only be invoked using the interface name.

This allows interfaces to group related helper methods without requiring separate utility classes.
public interface MathOperations {
    static int square(int number) {
        return number * number;
    }
}
The method is called using the interface name.
int result = MathOperations.square(5);
System.out.println(result);
Attempting to invoke the static method through an implementing class or object is not allowed.

Private Methods in Interfaces

Java 9 further enhanced interfaces by introducing private methods. Private methods are intended for internal use within the interface.

They allow common logic to be shared among multiple default or static methods without exposing helper methods to implementing classes.
public interface Logger {

    default void logInfo(String message) {
        print("INFO", message);
    }

    default void logError(String message) {
        print("ERROR", message);
    }

    private void print(String level, String message) {
        System.out.println(level + ": " + message);
    }
}
Both default methods reuse the same private helper method while keeping it hidden from implementations. Private Methods:

- Can be called only from methods within the same interface.
- Cannot be overridden by implementing classes.
- Help eliminate duplicate code among default and static methods.
- Improve readability by hiding implementation details.

Functional Interfaces

A functional interface is an interface that contains exactly one abstract method. Such interfaces serve as the foundation for lambda expressions and the Stream API.

Although a functional interface has only one abstract method, it may also contain any number of default, static, and private methods.
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
}
The interface can be implemented using a lambda expression.
Calculator add = (a, b) -> a + b;
System.out.println(add.calculate(10, 20));
The @FunctionalInterface annotation is optional but recommended because the compiler verifies that the interface contains only one abstract method.
@FunctionalInterface
public interface Printer {
    void print(String message);
}
If another abstract method is added, compilation fails.

Common Functional Interfaces

The JDK provides several widely used functional interfaces in the java.util.function package.
Interface Purpose
Predicate<T> Evaluates a condition and returns a boolean.
Function<T, R> Transforms one value into another.
Consumer<T> Consumes a value without returning a result.
Supplier<T> Produces a value without taking input.
UnaryOperator<T> Operates on and returns the same type.
BinaryOperator<T> Combines two values of the same type.
Functional interfaces are covered in much greater depth in the dedicated articles on Lambda Expressions and the Stream API.

Marker Interfaces

A marker interface is an interface that contains no methods or fields.

Its purpose is to mark a class as having a particular property so that the JVM or a framework can apply special behavior.
public interface Auditable {
}
A class indicates the capability simply by implementing the interface.
public class Employee implements Auditable {
}
Unlike functional interfaces, marker interfaces do not define behavior. They convey metadata through the type system.

Some well-known marker interfaces in the JDK include:
Marker Interface Purpose
Serializable Indicates that objects can be serialized.
Cloneable Allows objects to be cloned using Object.clone().
Remote Identifies remote objects used by Java RMI.
Today, many frameworks prefer annotations for attaching metadata, but marker interfaces remain useful when the information should participate in Java's type system.

Sealed Interfaces

Java 17 introduced sealed interfaces, allowing developers to explicitly control which classes or interfaces are permitted to implement an interface.

Before sealed interfaces, any class could implement a public interface.

While this flexibility is often desirable, there are situations where an API should support only a fixed set of implementations. Sealed interfaces make this possible by restricting the inheritance hierarchy.

A sealed interface declares its permitted implementations using the permits clause.
public sealed interface Shape
        permits Circle, Rectangle, Triangle {
    double area();
}
Only the listed classes are allowed to implement the interface.
public final class Circle implements Shape {
    @Override
    public double area() {
        return 100;
    }
}

public final class Rectangle implements Shape {
    @Override
    public double area() {
        return 200;
    }
}

public final class Triangle implements Shape {
    @Override
    public double area() {
        return 150;
    }
}
Any attempt by another class to implement Shape results in a compilation error.

Permitted Subclasses

Every permitted implementation of a sealed interface must explicitly declare one of the following modifiers.
Modifier Meaning
final No further inheritance is allowed.
sealed Further inheritance is allowed, but only for explicitly permitted subclasses.
non-sealed Removes the restriction, allowing unrestricted inheritance.
Sealed interfaces improve API design by making inheritance explicit, enabling better compiler checks, safer pattern matching, and more maintainable class hierarchies.

Abstract Classes with Constructors

Although an abstract class cannot be instantiated directly, it can define one or more constructors.

These constructors are invoked whenever a concrete subclass is created, making them ideal for initializing common state shared by all subclasses.
public abstract class Vehicle {
    protected String registrationNumber;

    public Vehicle(String registrationNumber) {
        this.registrationNumber = registrationNumber;
    }
}
The subclass invokes the constructor using super().
public class Car extends Vehicle {
    public Car(String registrationNumber) {
        super(registrationNumber);
    }
}
Although the following is illegal because abstract classes cannot be instantiated,
Vehicle vehicle = new Vehicle("UK07AB1234"); // Compilation Error
creating a subclass automatically invokes the abstract class constructor.
Vehicle vehicle = new Car("UK07AB1234"); 
Constructors in abstract classes are commonly used to initialize shared fields, perform validation, allocate resources, or enforce invariants that every subclass must satisfy.

Template Method Pattern

One of the most common uses of abstract classes is implementing the Template Method Pattern.

The Template Method Pattern defines the overall algorithm in a concrete method while allowing subclasses to customize specific steps.

This ensures that the sequence of operations remains consistent while permitting variation where required. Consider a simple document generation framework.
public abstract class DocumentGenerator {
    public final void generate() {
        readData();
        processData();
        saveDocument();
    }

    protected void readData() {
        System.out.println("Reading data...");
    }

    protected abstract void processData();

    protected void saveDocument() {
        System.out.println("Saving document...");
    }
}
The overall workflow is fixed, while subclasses implement only the variable step.
public class PdfGenerator extends DocumentGenerator {
    @Override
    protected void processData() {
        System.out.println("Generating PDF...");
    }
}
Executing the algorithm produces the following sequence.
DocumentGenerator generator = new PdfGenerator();
generator.generate();
Output:
Reading data...
Generating PDF...
Saving document...
The subclass cannot alter the algorithm itself because the generate() method is declared final. It can customize only the steps intentionally left abstract by the base class.

Final Notes

Interfaces define contracts that enable loose coupling, polymorphism, and multiple inheritance of type, making them ideal for APIs, service layers, plugin architectures, and dependency injection.

Modern enhancements such as default methods, static methods, private methods, and sealed interfaces have made interfaces significantly more expressive while preserving backward compatibility.

Abstract classes complement interfaces by providing shared state, constructors, and reusable implementations for closely related classes.

They are particularly well suited for implementing common workflows, enforcing invariants, and applying design patterns such as the Template Method Pattern.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion