Java OOP Concepts: The 4 Pillars with Examples

Java OOP concepts explained with runnable Java 25 examples of abstraction, encapsulation, inheritance and polymorphism, plus records and sealed types.

Diagram of the four pillars of OOP in Java with the Java feature and a library catalog example for abstraction, encapsulation, inheritance and polymorphism

Object-oriented programming (OOP) in Java is a way of building programs from objects, where each object keeps its own data in fields and offers behavior through methods. The core Java OOP concepts are four principles, namely abstraction, encapsulation, inheritance and polymorphism, which are often called the four pillars of OOP.

We use OOP concepts to model the things an application deals with, such as books, members, loans and fees in a library app, so that each class owns its data and rules and the rest of the code works with clear contracts instead of raw data.

The following example shows all four pillars in a few lines of a library catalog.

interface Borrowable {                           // abstraction
    int loanDays();
}

abstract class LibraryItem implements Borrowable {
    private final String title;                   // encapsulation

    LibraryItem(String title) {
        this.title = Objects.requireNonNull(title, "title");
    }

    String title() {
        return title;
    }
}

class Book extends LibraryItem {                  // inheritance
    Book(String title) {
        super(title);
    }

    public int loanDays() {
        return 21;
    }
}

class Magazine extends LibraryItem {
    Magazine(String title) {
        super(title);
    }

    public int loanDays() {
        return 7;
    }
}
List<Borrowable> items = List.of(new Book("Dune"), new Magazine("Wired"));
int bookDays = items.get(0).loanDays();           // 21, polymorphism
int magazineDays = items.get(1).loanDays();       // 7

The list holds both items through the Borrowable interface, and each object answers loanDays() in its own way. We start with classes and constructors, go through each pillar with a runnable example, explain object relationships and coupling, show how records and sealed types fit in, and end with a learning path through the detailed OOP tutorials.

1. What Is Object-Oriented Programming?

In procedural code, data and the functions that change it are separate, so any function can change any data. In object-oriented code, an object bundles its data with the methods allowed to change it. Each object has state, which is the values of its fields, and behavior, which is its methods. It also has an identity, so two objects with the same field values are still two different objects.

Java is an object-oriented language, but not a pure one. Primitive types such as int and double are not objects, and static methods belong to a class rather than to an object. Java keeps them for performance and convenience, and the classes and objects tutorial covers the basics of declaring both.

1.1. Classes and Objects

A class is a template that declares fields and methods, and an object is one instance of that class created with new. The library app has one Book class and thousands of Book objects, one per copy on the shelves.

record Member(String name, String email) {
}
Book first = new Book("Dune");
Book second = new Book("Dune");
boolean sameObject = first == second;             // false
boolean sameTitle = first.title().equals(second.title());  // true
Member ana = new Member("Ana", "ana@example.com");
boolean equalMembers = ana.equals(new Member("Ana", "ana@example.com"));  // true

The == operator compares identity, so two copies of the same book are different objects. The equals() method compares content when the class defines it, and a record such as Member gets equals() and hashCode() from its fields. The JVM does not use hashCode() as an identity, because two different objects may return the same hash code, as the hashCode and equals guide explains.

1.2. Constructors

A constructor sets up a new object before anyone can use it. It has the class name and no return type, and it is the right place to reject invalid input, as LibraryItem does with Objects.requireNonNull(). If a class declares no constructor, the compiler adds a default constructor without parameters. As soon as we declare any constructor, the compiler stops adding it.

The Java constructors guide covers default and parameterized constructors and constructor chaining with this(…). Code that must run for every constructor can also go into an instance initializer block, although a shared private method or constructor chaining is easier to read.

2. The Four Pillars of OOP in Java

The four pillars are design ideas, and Java gives each one a language feature. They work together, and most classes we write use all four at once, as the intro example did.

Diagram of the four pillars of OOP in Java with the Java feature and a library catalog example for abstraction, encapsulation, inheritance and polymorphism
Each OOP pillar maps to a Java feature; the library example uses all four
PillarIdeaJava featureLibrary example
AbstractionShow what an object does, hide howInterfaces, abstract classesBorrowable.loanDays()
EncapsulationKeep data private and guardedprivate fields, access modifiersLoan with a private due date
InheritanceReuse and specialize a typeextends, implementsBook extends LibraryItem
PolymorphismOne call, many implementationsOverriding, overloadingloanDays() on a List<Borrowable>

2.1. Abstraction

Abstraction means designing a type around the operations its callers need and leaving out everything else. The checkout desk in the library app needs to know how long an item can be borrowed, so Borrowable offers loanDays() and nothing about shelves or barcodes.

In Java, we write abstractions as interfaces and abstract classes. An interface is a pure contract, and an abstract class can also hold shared fields and code, which is why LibraryItem is abstract. The abstraction in Java guide goes deeper, and the interface vs abstract class article helps choose between the two.

2.2. Encapsulation

Encapsulation means hiding an object’s fields and letting other code change them only through methods that enforce the rules. Without it, any class could set a due date in the past or renew a loan a hundred times.

class Loan {
    private LocalDate dueDate;
    private int renewals;

    Loan(Borrowable item, LocalDate start) {
        this.dueDate = start.plusDays(item.loanDays());
    }

    void renew() {
        if (renewals == 2) {
            throw new IllegalStateException("Renewal limit reached");
        }
        renewals++;
        dueDate = dueDate.plusDays(14);
    }

    LocalDate dueDate() {
        return dueDate;
    }
}
Loan loan = new Loan(new Book("Dune"), LocalDate.of(2026, 1, 5));
LocalDate due = loan.dueDate();                   // 2026-01-26
loan.renew();
loan.renew();
LocalDate renewed = loan.dueDate();               // 2026-02-23
loan.renew();                                     // IllegalStateException: Renewal limit reached

The renewal limit lives in one method, so a rule change touches one class. Java controls visibility with four access modifiers, and the encapsulation vs abstraction article explains why the two pillars are often confused.

2.3. Inheritance

Inheritance lets a class reuse the fields and methods of another class and add or change behavior. A Book is a LibraryItem, so it inherits title() and reuses the null check of the parent constructor through super(title). Private members and constructors are not inherited, and every class without extends inherits from java.lang.Object.

A class extends only one class but can implement many interfaces. The Java inheritance guide covers single, multilevel and hierarchical inheritance with constructor chaining, the extends vs implements article compares the two keywords, and multiple inheritance in Java explains how default methods and the diamond rules work.

2.4. Polymorphism

Polymorphism means that one method call runs different code depending on the object or the arguments. With overriding, the JVM picks the method from the class of the object at runtime, which is how items.get(1).loanDays() returned 7. With overloading, the compiler picks one of several methods with the same name from the argument types.

static int lateFee(int daysLate) {
    return lateFee(daysLate, 25);
}

static int lateFee(int daysLate, int centsPerDay) {
    return Math.max(0, daysLate) * centsPerDay;
}
int standardFee = lateFee(4);                     // 100
int reducedFee = lateFee(4, 10);                  // 40

The polymorphism in Java article shows both kinds in detail, and overloading vs overriding compares the rules. Static methods do not take part in runtime dispatch, which the method hiding article explains. When code must branch on the class of an object, the instanceof operator with a pattern variable is the modern way to test and cast in one step.

3. Relationships Between Objects

Objects rarely work alone. Besides inheritance, which is an is-a relationship, classes connect through has-a and uses-a relationships, and the difference matters when we decide who creates and deletes which object.

RelationshipMeaningLibrary example
AssociationTwo objects work together, and each has its own lifecycleA Member borrows a Book; deleting the member keeps the book
AggregationOne object holds others that can exist without itA Shelf holds books; books move to another shelf when it is removed
CompositionOne object owns parts that cannot exist without itA Library owns its Shelf objects; closing the library removes its shelves

In code, all three look like fields, so the difference is in who creates the object and who may keep a reference to it. The association, aggregation and composition article shows each relationship with code and lifecycle rules.

4. Coupling and Cohesion

Coupling measures how much one class depends on the details of another. Cohesion measures how closely the methods of one class belong together. Good OOP design aims for low coupling and high cohesion, so a change in one class rarely forces changes in others, and each class has one clear job.

Say the library sends overdue reminders by email today and by SMS next year. If the reminder class creates an email client itself, it is tightly coupled to email. If it depends on a small Notifier interface, the channel can change without touching the reminder logic.

interface Notifier {
    String send(String to, String text);
}

class OverdueReminder {
    private final Notifier notifier;

    OverdueReminder(Notifier notifier) {
        this.notifier = notifier;
    }

    String remind(Member member, String title) {
        return notifier.send(member.email(), "Please return " + title);
    }
}
Notifier fakeMail = (to, text) -> to + " <- " + text;
OverdueReminder reminder = new OverdueReminder(fakeMail);
String sent = reminder.remind(new Member("Ana", "ana@example.com"), "Dune");  // "ana@example.com <- Please return Dune"

OverdueReminder is also cohesive, because it only builds and sends reminders. Sending and retrying belong to the Notifier implementations, and the test above uses a lambda as a fake notifier without any mocking library.

5. Records, Sealed Types and Pattern Matching

Modern Java added three features that change how we write object-oriented code. Records (Java 16) are classes for plain data with generated constructors, accessors, equals() and hashCode(). Sealed classes and interfaces (Java 17) fix the list of allowed subtypes. Record patterns and pattern matching for switch (Java 21) let us take records apart inside a switch.

sealed interface Fee permits LateFee, LostItemFee {
}

record LateFee(int days) implements Fee {
}

record LostItemFee(int priceCents) implements Fee {
}
static int amount(Fee fee) {
    return switch (fee) {
        case LateFee(int days) -> days * 25;
        case LostItemFee(int priceCents) -> priceCents + 500;
    };
}
int late = amount(new LateFee(6));                // 150
int lost = amount(new LostItemFee(1999));         // 2499

Records do not replace classes with behavior, such as Loan, which needs mutable private state. They replace the long classes with fields, getters, equals() and hashCode() that older Java code used for data. The switch needs no default branch, because the compiler knows both permitted fee types.

6. OOP Design Principles

The four pillars describe what Java offers, and design principles describe how to use it well. A handful of them come up in almost every code review.

  • Favor composition over inheritance. Inherit only when the subclass is a true specialization, and otherwise keep the other object in a private field, as OverdueReminder does with Notifier.
  • Program to an interface. Declare fields, variables, parameters and return types with interface types such as List and Notifier, so implementations can change.
  • Give each class a single responsibility, as described in the single responsibility principle.
  • Keep classes open for extension and closed for modification. A new fee type should mean a new record, not edits across the codebase. The SOLID principles article covers this and the other three principles.
  • Encapsulate what changes. Hide the parts likely to change, such as the reminder channel, behind an interface or a private method.

7. Learning Path for Java OOP

The detailed OOP tutorials build on each other, so the order matters for someone learning the topic. Each step in this learning path uses ideas from the step before it.

  • Start with the building blocks in section 1, namely classes and constructors, and how Java uses the static keyword for class-level members.
  • Learn the four pillars from section 2 in order, because polymorphism depends on inheritance and encapsulation depends on access modifiers.
  • Study the keywords that control inheritance, such as extends and implements, this and super, and the final keyword that blocks overriding and subclassing.
  • Compare the pairs that interviewers ask about most, namely overloading vs overriding, encapsulation vs abstraction, interfaces vs abstract classes and extends vs implements, which are all linked in section 2.
  • Finish with object relationships and design in section 3 and section 6, and practice them with the strategy pattern, which uses every pillar.

8. Java OOP Concepts FAQs

OOP questions from beginners and interview candidates focus on definitions and on what Java does not allow.

8.1. Is Java a pure object-oriented language?

No. Java has primitive types such as int and boolean that are not objects, and static members that belong to a class rather than an object. Everything else, including arrays and strings, is an object.

8.2. What are the four pillars of OOP in Java?

Abstraction, encapsulation, inheritance and polymorphism. Java supports them with interfaces and abstract classes, access modifiers, the extends and implements keywords, and method overriding and overloading.

8.3. Can a Java class have two parent classes?

Not two parent classes. A class can extend only one class, so there is no multiple inheritance of state. A class can implement many interfaces, and since Java 8 it can inherit method bodies from them as default methods.

8.4. How is OOP different from functional programming in Java?

OOP organizes code around objects that hold state and behavior together, while functional programming organizes it around functions that transform immutable data. Java supports both, and modern code mixes them, for example a record passed through a stream pipeline of lambdas.

8.5. Are records object-oriented?

Yes. A record is a class with its own constructor and methods, and it can implement interfaces and declare its own methods. It cannot extend another class and its fields are final, so it fits data that does not change.

9. Conclusion

Object-oriented programming in Java builds programs from objects that combine data and behavior. Abstraction defines what a type offers, encapsulation protects its data, inheritance reuses and specializes types, and polymorphism lets one call work with many implementations.

Classes and constructors give objects a valid start, relationships and low coupling keep the design flexible, and records, sealed types and pattern matching cover data-centric code with less boilerplate. Each pillar has its own detailed tutorial linked in its section, and the learning path in section 7 gives a reading order.

10. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. 4 pillers of oop are significant , how , and if we omit any 2 of the pillers what effect will it have on oop ?
    pls answer

  2. Hi Lokesh,

    Manager can not reuse department, address and education from Employee because these attributes are private.
    Manager can reuse them if they are protected.

    • Yes, he can resuse the. For brevity Lokesh has omitted the getters and setters I believe. You can access those data using the getters and setters of the Parent Class and initialize them using setters/chaining of constructors.

      Child class {
      @Override
      public getSuperClassProperty() {
      super.getProperty(); // This is defined in the parent class.
      }

  3. Wonderful points you have mentioned here, Its actually a great and helpful piece of information. I am satisfied that you simply shared this helpful info with us. Please stay us informed like this.
    Thank you for sharing.

  4. Informative and Insightful blog!
    All i was looking for the OOPs concept in java in detail and i have found it very well here, OOPs concepts you have defined clearly and precise which is understandable for us. As beginner i have enjoyed your article and will look forward to read more from you. Thanks A lot for this meaningful article!

  5. Hi Lokesh,
    Can you please point out some differences between inheritance and composition. And as they say we should prefer composition over inheritance, but what is the reason behind it.

    • I will list out differences between inheritance and composition some other time. Main reason to prefer composition over inheritance is “loose coupling”. Inheritance introduces tight coupling which shall be avoided in most of the cases.

      • public class Person {

        //composition has-a relationship
        private Job job;

        public Person(){
        this.job=new Job();
        job.setSalary(1000L);
        }
        public long getSalary() {
        return job.getSalary();
        }

        }

        The above example is for Composition in which we are creating object of dependent object inside constructor so i think it is tight coupling? right ?

  6. Hi Lokesh,

    please clarify my doubt in spring.

    Can we inject Service Class into Controller in spring framework?. this is one of the interview question i was faced in CTS Pune.

    please provide me the answer with simple example. thanks in advance.

    if any tutorials are there please send it to my mail: pradepp.tanugula@gmail.com

  7. Sir, by mistake chenage word is written in place of change inside Control abstraction section of blog.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.