Object-Oriented Programming (OOP) Concepts in Java
Object-oriented programming (OOP) in Java is built on three classic principles — encapsulation, inheritance and polymorphism — and most modern courses add a fourth, abstraction. That is why you will see both «three OOP concepts» and «four pillars of OOP» in the wild. Below is what each one actually means, plus the Java code that implements it.
What Is Object-Oriented Programming
Object-oriented programming is a way of designing software as a set of interacting objects. An object bundles state (fields) with behaviour (methods), and a class is the template those objects are created from.
In a purely procedural design, data lives in one place and the functions that touch it live in another. OOP glues them together, which buys you three practical things: changes stay local to a class, behaviour can be extended without editing working code, and one call site can drive many different implementations. If you want the contrast spelled out, see procedural vs object-oriented programming.
Java is designed around this model: every method must live inside a class or an interface, and even the main entry point is a method of some class.
Encapsulation
Encapsulation means keeping data and the methods that operate on that data inside one class, and hiding the implementation details from the outside world. Calling code sees a public contract; how the class stores or validates anything is none of its business.
In Java, encapsulation is enforced with access modifiers (private, protected, package-private, public) and by exposing state through methods rather than raw fields.
The practical rules are simple:
- Instance variables are kept protected from direct access — in practice, declared
private. - State is read and modified only through public accessor methods.
- Accessor methods follow the JavaBeans naming convention:
getBalance(),setName(),isActive().
public class BankAccount {
private long balance; // state is hidden from the outside
public void deposit(long amount) {
if (amount <= 0) { // the class invariant is protected here
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
}
public long getBalance() {
return balance;
}
} BankAccount account = new BankAccount();
account.deposit(500);
// account.balance = -100; // will not compile: the field is private
System.out.println(account.getBalance()); // 500 The point is not the private keyword by itself. The point is that the class owns the correctness of its own data: from the outside, a negative balance is simply not reachable. That is why encapsulation is often called data hiding — although hiding is only half of it, the other half is putting state and behaviour in the same place.
When a class really is nothing more than an immutable carrier of values, a record (standard since Java 16) expresses that intent in one line and gives you final fields, accessors, equals(), hashCode() and toString() for free.
public record Point(int x, int y) { } // fields are private and final
Point p = new Point(3, 4);
System.out.println(p.x()); // 3 Worth knowing
A class whose fields are all private but which exposes a mechanically generated getter and setter for every single one of them is not really encapsulated: outside code can still drive the object into any state it likes. Access modifiers are the tool; preserving the object’s invariants is the goal.
Inheritance
Inheritance lets a new class be defined on top of an existing one, reusing its fields and methods. The class being inherited from is the superclass (base or parent class); the new one is the subclass (derived class or child).
In Java you declare inheritance with extends and reach the parent implementation through super.
public class Employee {
protected final String name;
public Employee(String name) {
this.name = name;
}
public double salary() {
return 1000;
}
}
public class Manager extends Employee {
public Manager(String name) {
super(name); // call the superclass constructor
}
@Override
public double salary() {
return super.salary() * 1.5; // reuse the parent's logic
}
} There are two standard reasons to reach for inheritance:
- Code reuse — generic behaviour is written once in the superclass and does not have to be reimplemented in every subclass.
- Polymorphism — a subclass can override an inherited method and still be used anywhere the superclass is expected.
The rules Java imposes:
- A class extends exactly one class — there is no multiple inheritance of classes, although a class can have a whole chain of ancestors.
- A class can implement any number of interfaces, which is how Java gets multiple inheritance of type.
- Every class implicitly inherits from
java.lang.Object. - A
finalclass cannot be extended at all —Stringis the classic example. - Since Java 17 a
sealedclass can name exactly which classes are permitted to extend it, giving you a controlled hierarchy instead of an open one.
IS-A Relationship
The IS-A relationship is what inheritance and interface implementation express: Manager IS-A Employee. In Java it is written with the keywords extends and implements, and it is exactly what the instanceof operator tests.
Employee employee = new Manager("Olga");
System.out.println(employee instanceof Employee); // true - Manager IS-A Employee
System.out.println(employee instanceof Object); // true - everything IS-A Object HAS-A Relationship
The HAS-A relationship is about usage rather than type: class A HAS-A B when A holds a reference to an instance of B. This is composition, and it is usually the better default.
public class Department {
private final Manager head; // Department HAS-A Manager
public Department(Manager head) {
this.head = head;
}
} Inheritance or composition?
Use inheritance only when the IS-A statement is genuinely true (a Manager really is an Employee). If all you want is to reuse somebody else’s code, prefer composition: hold the other object in a field and delegate to it. Inheritance couples classes tightly, and a change in the superclass ripples through every subclass.
Polymorphism
Polymorphism literally means «many forms». It is the ability to work with objects through a single type without knowing their concrete class. The usual one-liner is «one interface, many implementations». In Java terms: if an object passes more than one IS-A test, it can be treated polymorphically.
Java gives you two flavours of it.
Runtime Polymorphism (Method Overriding)
Runtime polymorphism comes from method overriding. The compiler checks the call against the declared type, but the JVM picks the implementation at run time based on the object’s actual class. This is also called dynamic method dispatch or late binding.
List<Employee> staff = List.of(
new Employee("Ivan"),
new Manager("Olga"));
for (Employee employee : staff) {
// the variable's type is Employee, but the actual class decides
System.out.println(employee.salary());
}
// 1000.0
// 1500.0 Compile-Time Polymorphism (Method Overloading)
Compile-time polymorphism comes from method overloading: several methods share a name but differ in their parameter lists, and the compiler picks one from the static types of the arguments.
public class Printer {
public void print(int value) {
System.out.println("int: " + value);
}
public void print(String value) {
System.out.println("String: " + value);
}
} A few rules about references that follow directly from this and show up constantly in Java certification questions:
- A reference variable has exactly one declared type and that type never changes — only the object it points to can change.
- A reference is still a variable, so it can be reassigned to another object.
- The declared type of the reference decides which methods you may call; the object’s real class decides which implementation runs.
- A reference can point to an object of its declared type or of any subtype of it.
- A reference can be declared as a class type or an interface type; an interface-typed reference can hold any object whose class implements that interface.
- Polymorphic dispatch applies to instance methods only — not to static methods and not to fields.
Classic interview question
Overloading is resolved by the compiler from the declared argument types; overriding is resolved by the JVM from the real object type. That is why static methods are never overridden — they are hidden, and which one runs depends on the reference type, not on the object. The same applies to fields: they are never polymorphic.
For the language-agnostic definition of the term, see Polymorphism (computer science), and for the official Java walkthrough, Polymorphism in the Java Tutorials.
Abstraction: The Fourth Pillar
Abstraction is the act of keeping the characteristics that matter for the problem you are solving and throwing away the ones that do not. The resulting set of characteristics is the abstraction.
The same real-world entity is abstracted differently depending on context: a payment service cares about a customer’s id and payment method, while an HR system describes the very same person by job title and years of service.
In Java, abstraction is expressed by interfaces and abstract classes: they state what an object can do while saying nothing about how it does it.
public interface Payment {
void pay(long amount); // what happens is part of the contract
}
public class CardPayment implements Payment {
@Override
public void pay(long amount) { // how it happens is an implementation detail
System.out.println("Paid by card: " + amount);
}
}
public class CashPayment implements Payment {
@Override
public void pay(long amount) {
System.out.println("Paid in cash: " + amount);
}
} Payment payment = new CardPayment(); // the code depends on the abstraction
payment.pay(2500); // Paid by card: 2500 Notice that this snippet demonstrates abstraction (the Payment interface) and polymorphism (the call lands in the right implementation) at the same time. The principles are not independent boxes — they work together.
The Four Principles Side by Side
| Principle | What it means | How Java implements it | What you gain |
|---|---|---|---|
| Encapsulation | State and behaviour live together; details are hidden | private, protected, accessor methods, record | Outside code cannot put the object into an invalid state |
| Inheritance | A new class is built on top of an existing one | extends, super, Object as the common root | Reuse and extension of existing behaviour |
| Polymorphism | One type, many implementations | Overriding, overloading, interfaces, generics | Calling code does not depend on concrete classes |
| Abstraction | Keep what matters, drop the rest | interface, abstract class | A domain model free of irrelevant detail |
OOP Principles vs SOLID
These two lists get mixed up in interviews all the time. The OOP principles describe what an object model is made of. SOLID is a set of five design guidelines that tell you how to use that model so the code stays maintainable. One is vocabulary, the other is advice.
| SOLID principle | Statement | Java example |
|---|---|---|
| S — Single Responsibility | A class should have one reason to change | One class calculates salary, another renders the report — never both |
| O — Open/Closed | Open for extension, closed for modification | A new payment method is a new Payment implementation; existing code is untouched |
| L — Liskov Substitution | A subtype must be usable wherever its supertype is expected | Manager works everywhere an Employee is accepted |
| I — Interface Segregation | Many narrow interfaces beat one fat interface | Readable and Writable instead of a single Storage |
| D — Dependency Inversion | Depend on abstractions, not on implementations | A field typed Payment, not CardPayment |
Where Developers Get Tripped Up
- Treating encapsulation as a synonym for data hiding. Hiding is one half of it; the other half is that state and the operations on it belong to the same class.
- Inheriting purely to reuse code. Without a true IS-A relationship you end up with a brittle hierarchy. Composition is the safer default.
- Confusing overloading with overriding. Overloading means same name, different parameters, usually in the same class. Overriding means the same signature reimplemented in a subclass.
- Skipping
@Override. The annotation is optional, but without it a typo in the method name silently creates a brand new method instead of overriding anything — and the compiler says nothing. - Listing composition and aggregation as OOP principles. They are kinds of relationships between objects, not pillars. In an interview that answer counts against you.
- Expecting fields and static methods to be polymorphic. Only instance methods are dispatched on the runtime type; fields and statics are resolved from the reference type.
- Claiming Java is a pure object-oriented language. Primitives such as
int,doubleandbooleanare not objects, which makes Java a hybrid language.
The official introduction to classes and objects lives in the Object-Oriented Programming Concepts section of the Oracle Java Tutorials.
Frequently Asked Questions
Are there three or four pillars of OOP?
The classic trio is encapsulation, inheritance and polymorphism. Most textbooks and most interviewers add abstraction as a fourth pillar. The safe answer is to name all four and add that some authors treat abstraction as part of encapsulation rather than a separate principle.
What is the difference between abstraction and encapsulation?
Abstraction is a design decision: you choose which characteristics of an entity matter and expose only those, usually through an interface or an abstract class. Encapsulation is the implementation mechanism that protects those decisions: access modifiers and accessor methods stop outside code from touching internal state directly. Abstraction hides complexity, encapsulation hides data.
How is polymorphism different from inheritance?
Inheritance is a relationship between classes: one class is defined on top of another. Polymorphism is a capability: the same call produces different behaviour depending on the actual object. Inheritance is one way to get polymorphism in Java, but interfaces give you polymorphism without any shared parent class.
Is Java a pure object-oriented language?
No, Java is usually called a hybrid language. All code lives inside classes, but the primitive types int, double, char and boolean are not objects, and static members are used without creating an instance. Smalltalk and Ruby are the usual examples of fully object-oriented languages.
How do OOP principles differ from SOLID?
The OOP principles describe what an object model is made of: encapsulation, inheritance, polymorphism and abstraction. SOLID is five design guidelines for arranging classes so the code survives change. OOP answers what the building blocks are, SOLID answers how to assemble them without creating a mess.
Comments