Encapsulation, polymorphism, inheritance, and abstraction form the foundation of object-oriented programming in Java. Encapsulation is usually easy to understand, but the lines between polymorphism, inheritance, and abstraction tend to blur—even for experienced developers. In this blog, we’ll revisit these concepts and unpack the subtle differences that are often the real source of confusion.
Inheritance and abstraction are modelling tools that enable Java developers to design and simulate real-world entities as Java objects. This naturally brings us to the Object class in Java, which is implicitly inherited by every class—whether it’s user-defined or provided by the language itself, such as String. This implicit inheritance forms the foundation of Java’s type hierarchy and serves as the most fundamental example of inheritance in the language.
Polymorphism
In real-world terms, refers to a single conceptual entity that exists in multiple concrete forms depending on context. A simple example is a bike: city bikes, gravel bikes, and mountain bikes are all bikes by design, yet each is built to function optimally in different terrain conditions with city bike being the most basic form of a bike. In Java, this kind of polymorphism is often modelled using inheritance—each variant satisfies the core characteristics of a (city)bike while providing specialised behaviour suited to its purpose.
Code reuse through extension is an added advantage of inheritance. Continuing with the bike example, if a new type of bike designed for ferrying goods or young children needs to be introduced, a new LoadBike class can be created by inheriting all attributes of city bike with additional featues, such as the total load it can carry, while still reusing the common behavior of a bike.
To stay within the scope we shall restrict ourselves to one way of achieving polymorphism in java. Yet, that is enough to make it clear that polymorphism is an outcome of modeling real-world entities, and inheritance is a powerful tool to achieve it in Java. With that foundation in place, it’s time to explore why abstract classes exist and what problems they are designed to solve.
Abstraction in Java
Is realised through abstract classes and interfaces. At its core, abstraction defines behaviour and enforces contracts that can only be fulfilled by concrete objects. For this reason, neither abstract classes nor interfaces can be instantiated directly—they exist to describe what an object must do, not how it is created. Abstract classes enforce behaviour through shared implementation, while interfaces enforce expectations across class boundaries through contracts.
Abstract class – a design choice
In our example, we could create a Bike class to model the concept of a bike and deliver common behaviour for city bikes, mountain bikes, gravel bikes, and load bikes. The shared implementation could be reused, overridden, or enforced through abstract methods- the choice is left to the developer. By making the Bike class abstract, the developer prevents direct instantiation and can leverage polymorphism as a design pattern throughout the code.
public abstract class Bike {
private int yearOfMake;
protected abstract maximumRpm();
protected int setYearOfMake(int year) {
this.yearOfMake = year;
}
}
Need for interface
In Java, classes not only define attributes but also describe behaviour, giving them clear responsibilities. Objects of different classes work together to fulfil these responsibilities, modelling interactions in the real world. Interfaces in Java are a way to define and enforce the responsibilities of one class to another, without worrying about how each class achieves it. Built-in interfaces like Serialisable and Runnable are some interfaces that Java provides.
TL;DR
Inheritance is a means to realise polymorphism seen in real world objects. Abstraction is a definition and ability to impose it. Abstract classes are a design choice and define behaviour. Interface enforce contracts and do not care about implementation.

