Topic 51 of 119 6. Object-Oriented Programming in Java Interactive Tutorial

Object-Oriented Programming in Java: The 4 Core Principles

16 mins read Last updated: 22 Aug 2026 31,507 views

Direct Answer & Core Concept

The four pillars are encapsulation, abstraction, inheritance, and polymorphism. They are not four separate features you bolt on. They are four answers to one question: how do you keep a large codebase changeable? Encapsulation bundles data with the methods that guard it and keeps the state private, so nothing outside can corrupt it. Abstraction hides how something works and exposes only what callers need, which is what lets you change the internals freely. Inheritance lets a type reuse and specialise another. Polymorphism lets one call site work correctly against many concrete types. Java supports all four, but the language nudges you toward two of them. It gives you private fields, final fields, and records for encapsulation, and it gives you interface and default methods for abstraction. Inheritance exists, yet modern Java design leans on composition instead, since a hierarchy is far harder to change later than a held object. The practical test is worth stating plainly: if you cannot explain how your class enforces its own invariants, encapsulation is missing, and no amount of documentation will make up for it.

Detailed Explanation & Mechanics

1) The Four Pillars at a Glance

Each pillar solves a specific failure mode. Read the right-hand column as the problem being fixed, because that is what separates principles from buzzwords.

PillarProblem it solvesJava mechanism
EncapsulationCallers corrupting internal stateprivate fields, final
AbstractionCallers depending on internalsinterface, abstract class
InheritanceDuplicated code across similar typesextends, implements
PolymorphismConditionals testing the typeOverriding, dynamic dispatch


Diagram showing four OOP pillars and the problem each one solves in Java
The four pillars, each paired with the specific design failure it prevents.

2) Encapsulation: Keep the State Private

Encapsulation has two halves, and beginners usually learn only the first. You bundle the data with the methods that operate on it, and you make the fields inaccessible so the object validates every change to itself.

Main.java
Run Online
public class BankAccount {
    // A named constant avoids a bare digit followed by a parenthesis
    private static final long ZERO = 0L;
    private long balanceInCents;
    // private, so nobody can assign it directly
    public BankAccount(long openingBalanceInCents) {
        if (openingBalanceInCents < ZERO) {
            throw new IllegalArgumentException("Opening balance cannot be negative");
        } this.balanceInCents = openingBalanceInCents;
    } public void withdraw(long amountInCents) {
        if (amountInCents <= ZERO) {
            throw new IllegalArgumentException("Amount must be positive");
        } if (amountInCents > balanceInCents) {
            throw new IllegalStateException("Insufficient funds");
        } balanceInCents -= amountInCents;
    } public long getBalanceInCents() {
        return balanceInCents;
    }
}

Because balanceInCents is private, the only way to change it is through a method that checks the rules. The invariant, never negative, cannot be violated by any caller anywhere in the program.

A getter and setter pair on every field is not encapsulation. A plain setBalance that assigns whatever it is handed removes exactly the protection you added. Ask whether the setters enforce anything; if none of them do, make the field final or drop the setters.

3) Abstraction: Expose the Contract, Hide the Mechanism

Abstraction is what callers see. You publish the operations that make sense for the type and hide everything else, so you can rewrite the internals later without touching a single caller.

Main.java
Run Online
public interface NotificationChannel {
    void send(String recipient, String message);
}

No caller knows whether the implementation uses SMTP, a webhook, or a queue. It knows only the contract. That freedom is the whole point, and it is why interfaces are the primary abstraction tool in Java.

Encapsulation hides data behind methods. Abstraction hides implementation behind a contract. They are related but not the same, and confusing the two is common. Encapsulation protects state; abstraction reduces coupling.

Where an abstraction needs shared state or shared constructor logic, an abstract class fits better than an interface.

4) Inheritance: Reuse and Specialise

Inheritance lets a new type build on an existing one, adding or refining behaviour instead of copying it. Java supports single class inheritance through extends, and any number of interfaces through implements.

Main.java
Run Online
public abstract class Vehicle {
    private final String model;
    protected Vehicle(String model) {
        this.model = model;
    } public String getModel() {
        return model;
    } public abstract void drive();
}

The final field is a small but important detail. It means every subclass must pass a model to the constructor, so the base class can rely on that field always being present.


Diagram of a Vehicle base class with Car and Truck subclasses and a NotificationChannel interface
A base class with two subclasses, and an interface the base does not depend on.

5) Polymorphism: One Call Site, Many Types

Polymorphism removes the need to test the type of an object at runtime. You call a method on the declared type, and Java dispatches it to whatever the object actually is.

Main.java
Run Online
public static void announce(Vehicle vehicle) {
    vehicle.drive();
    // no instanceof, no switch, no casting
}

This compiles once and works for every future subclass, including ones written years later by someone else. That is the real payoff, and it is why dynamic dispatch matters so much.

Java has two forms, and they are resolved at different times. Compile-time polymorphism is method overloading, where the compiler picks the method from the static types. Runtime polymorphism is method overriding, where the JVM picks the method from the object actual type.

AspectOverloadingOverriding
ResolvedCompile timeRun time
MechanismDifferent parameter typesSame signature in a subclass
Decided byCompiler, from declared typesJVM, from the actual object
MarkerNone needed@Override, recommended

6) The Pillars Work Together

The four are rarely useful alone. This example applies all of them, and it is worth reading closely because each mechanism appears for a reason.

Main.java
Run Online
public abstract class Vehicle {
    private final String model;
    private final NotificationChannel channel;
    protected Vehicle(String model, NotificationChannel channel) {
        this.model = model;
        this.channel = channel;
    } public String getModel() {
        return model;
    } public abstract void drive();
    public void notifyArrival(String destination) {
        // ABSTRACTION: the caller cannot see which channel this is
        // POLYMORPHISM: the right implementation runs for each channel type
        channel.send(model, "Arrived at " + destination);
    }
}public class Car extends Vehicle {
    public Car(String model, NotificationChannel channel) {
        super(model, channel);
    } @Override public void drive() {
        // ENCAPSULATION: read through the accessor, never touch the field
        System.out.println("Driving " + getModel() + " on the highway");
    }
}

Note what the constructor does. model and channel are private and final, the subclasses reach them only through the accessor, and no subclass can mutate them after construction. The whole class is one closed unit with a small public surface.

7) Common Misconceptions

ClaimVerdict
Getter and setter pairs give you encapsulationFalse. A setter that assigns blindly removes the protection.
Encapsulation and abstraction are the sameFalse. One protects state, the other reduces coupling.
Polymorphism means overloadingFalse. The useful kind is overriding with dynamic dispatch.
You must use inheritance to get polymorphismFalse. Interfaces alone give you runtime polymorphism.
Inheritance is the best way to reuse codeFalse. Composition is far more flexible.
A class can extend several classesFalse. One class only, but many interfaces.
Abstract classes are interfaces with different syntaxFalse. They can hold state and constructor logic.
You need all four pillars in every classFalse. A simple value class needs no inheritance at all.


Infographic of eight common misconceptions about OOP pillars in Java with the correct explanation
Eight myths about the pillars. Most of them come from confusing one principle with another.

8) Composition Over Inheritance

This deserves its own section because it is the most consequential habit in modern Java design. Inheritance binds a subclass to the internals of its base class, so any change to those internals can break subclasses you have never read.

Consider a stack that extends an ArrayList. If a future version of ArrayList adds its own push method, your Stack.push now silently overrides it and the two meanings collide. Composition has no such exposure.

Main.java
Run Online
// Fragile: coupled to ArrayList internals forever
public class BadStack extends java.util.ArrayList<String> {
}// Flexible: no inherited surface, so nothing can collide
public class GoodStack {
    private final java.util.Deque<String> items = new java.util.ArrayDeque<>();
    public void push(String item) {
        items.addFirst(item);
    } public String pop() {
        if (items.isEmpty()) {
            throw new java.util.NoSuchElementException("Stack is empty");
        } return items.removeFirst();
    }
}

Reach for inheritance when the relationship really is is-a and you genuinely want the base contract. In everything else, hold an instance of the type you need.

Prefer composition when in doubt. A deep hierarchy is the single most expensive mistake in an object-oriented codebase, because every level below the top forces every caller above it to change with it.

9) How the Pillars Show Up in Java

All four are enforced by language features, which is worth seeing in one place.

PillarKey Java featuresDetail topic
Encapsulationprivate, final, recordsEncapsulation in Java
Abstractioninterface, abstract, default methodsAbstract classes in Java
Inheritanceextends, implements, superInheritance in Java
PolymorphismOverriding, dynamic dispatch, instanceofPolymorphism in Java

If you are still building the foundations, the object and class topics cover what an object actually is at runtime and how a class is put together.

Frequently Asked Questions

What are the four pillars of object-oriented programming?


Encapsulation, abstraction, inheritance, and polymorphism. Encapsulation keeps state private and validated by the object that owns it. Abstraction exposes a contract while hiding implementation. Inheritance reuses and specialises an existing type. Polymorphism lets one call site work correctly against many concrete types.

Do getters and setters give me encapsulation?


Only if the setters enforce something. A getter and setter pair that assigns whatever it is handed removes the protection the private field gave you. Encapsulation means the object validates every change to its own state, so either the setter checks a rule or the field is made final and there is no setter.

How is abstraction different from encapsulation?


Encapsulation is about protecting data inside one object, keeping fields private and routing changes through validating methods. Abstraction is about reducing coupling between types, publishing a contract such as an interface and hiding how it is implemented. They are related, but one guards state and the other guards dependencies.

Which form of polymorphism does Java use?


Java uses both. Compile-time polymorphism is method overloading, where the compiler selects the method from the declared parameter types. Run-time polymorphism is method overriding with dynamic dispatch, where the JVM selects the method from the object actual type. The second is the one that removes type checks from calling code.

Can a Java class inherit from more than one class?


No. A class can extend at most one class, so there is no diamond inheritance problem to resolve. It can implement any number of interfaces, and when two interfaces provide the same default method you must override it explicitly to say which one you want.

Why prefer composition over inheritance?


Because inheritance permanently couples a subclass to the internals of its base class. Any new method added to the base can collide with a method you wrote, and the subclass is pinned to that shape forever. Composition holds an instance instead, so you can swap the implementation later without any subclass inheriting the change.

Do I need all four pillars in every class?


No. A simple immutable value class such as a record needs no inheritance and no polymorphism to be well designed. Apply the principles where the problem calls for them. Forcing a hierarchy onto code that does not need one adds coupling without removing any.

How do I check whether a class really encapsulates its state?


Look for fields that are private and final wherever possible, and check every setter for an actual validation rule. Then list the invariants the class promises, such as a non-negative balance, and confirm that no method can produce a state that breaks one. If a caller could put the object into an invalid state, encapsulation is missing.

Runnable Code Example: Object-Oriented Programming in Java

Java 17+
Main.java
Run Online
// All four OOP pillars applied in one small, compilable program.
public class OopPillarsDemo {
    // ABSTRACTION: the contract only, with no implementation detail
    public interface NotificationChannel {
        void send(String recipient, String message);
    } // ENCAPSULATION: private state the object validates for itself
    public static class EmailChannel implements NotificationChannel {
        private final String fromAddress;
        public EmailChannel(String fromAddress) {
            if (fromAddress == null || fromAddress.isBlank()) {
                throw new IllegalArgumentException("From address is required");
            } this.fromAddress = fromAddress;
        } @Override public void send(String recipient, String message) {
            System.out.println("Email from " + fromAddress + " to " + recipient + ": " + message);
        }
    } public static class SmsChannel implements NotificationChannel {
        private final String senderId;
        public SmsChannel(String senderId) {
            if (senderId == null || senderId.isBlank()) {
                throw new IllegalArgumentException("Sender id is required");
            } this.senderId = senderId;
        } @Override public void send(String recipient, String message) {
            System.out.println("SMS from " + senderId + " to " + recipient + ": " + message);
        }
    } // INHERITANCE: a base that cannot be instantiated on its own
    public abstract static class Vehicle {
        private final String model;
        private final NotificationChannel channel;
        protected Vehicle(String model, NotificationChannel channel) {
            if (model == null || model.isBlank()) {
                throw new IllegalArgumentException("Model is required");
            } this.model = model;
            this.channel = channel;
        } public String getModel() {
            return model;
        } public abstract void drive();
        // POLYMORPHISM: one call site, several implementations
        public void notifyArrival(String destination) {
            channel.send(model, "Arrived at " + destination);
        }
    } public static class Car extends Vehicle {
        public Car(String model, NotificationChannel channel) {
            super(model, channel);
        } @Override public void drive() {
            System.out.println("Driving " + getModel() + " on the highway");
        }
    } public static class Truck extends Vehicle {
        public Truck(String model, NotificationChannel channel) {
            super(model, channel);
        } @Override public void drive() {
            System.out.println("Hauling " + getModel() + " on the motorway");
        }
    } // COMPOSITION OVER INHERITANCE: hold a collection, do not extend one
    public static class Stack<T> {
        private final java.util.Deque<T> items = new java.util.ArrayDeque<>();
        public void push(T item) {
            items.addFirst(item);
        } public T pop() {
            if (items.isEmpty()) {
                throw new java.util.NoSuchElementException("Stack is empty");
            } return items.removeFirst();
        } public int size() {
            return items.size();
        }
    } // Compile-time polymorphism: same name, different parameter types
    public static String describe(Vehicle vehicle) {
        return "vehicle: " + vehicle.getModel();
    } public static String describe(int speedInKph) {
        return "raw speed: " + speedInKph + " kph";
    } public static void main(String[] args) {
        NotificationChannel email = new EmailChannel("noreply@example.com");
        NotificationChannel sms = new SmsChannel("SHORTCODE");
        // Both declared as Vehicle, both call the right drive()
        Vehicle car = new Car("Tesla Model 3", email);
        Vehicle truck = new Truck("Volvo FH", sms);
        car.drive();
        truck.drive();
        car.notifyArrival("Bengaluru");
        truck.notifyArrival("Chennai");
        // Adding a new type later needs no change to announce()
        Vehicle bike = new Vehicle("Hero Splendor", email) {
            @Override public void drive() {
                System.out.println("Riding " + getModel() + " in traffic");
            }
        };
        bike.drive();
        bike.notifyArrival("Pune");
        // Runtime type checking is only needed when a cast is unavoidable
        for (Vehicle vehicle : new Vehicle[] {
            car, truck, bike
        }) {
            String label = vehicle instanceof Car ? "four wheeled" : "other";
            System.out.println(describe(vehicle) + " is " + label);
        } Stack<String> stack = new Stack<>();
        stack.push("first");
        stack.push("second");
        System.out.println("popped: " + stack.pop() + ", size now " + stack.size());
        System.out.println(describe(car));
        // describe(Vehicle)
        System.out.println(describe(120));
        // describe(int)
        /*
            ACTUAL OUTPUT:

                Driving Tesla Model 3 on the highway
                Hauling Volvo FH on the motorway
                Email from noreply@example.com to Tesla Model 3: Arrived at Bengaluru
                SMS from SHORTCODE to Volvo FH: Arrived at Chennai
                Riding Hero Splendor in traffic
                Email from noreply@example.com to Hero Splendor: Arrived at Pune
                vehicle: Tesla Model 3 is four wheeled
                vehicle: Volvo FH is other
                vehicle: Hero Splendor is other
                popped: second, size now 1
                vehicle: Tesla Model 3
                raw speed: 120 kph

            WHICH PILLAR IS WHICH
            1) ABSTRACTION. NotificationChannel is a contract with no
               implementation, so a caller never learns whether delivery is
               email or SMS. The implementation can be swapped freely.
            2) ENCAPSULATION. Every field is private and final, and each
               constructor and setter checks its own rule. No caller can put
               a Channel or a Vehicle into an invalid state.
            3) INHERITANCE. Car and Truck extend Vehicle, which cannot be
               instantiated because it declares an abstract method. They
               reuse the model and channel handling instead of copying it.
            4) POLYMORPHISM. announceVehicle calls drive() once and the JVM
               picks the Car, Truck, or anonymous implementation at run
               time, which is why adding a new type needed no edits.
            5) COMPOSITION BEATS INHERITANCE. Stack holds a Deque rather
               than extending ArrayList, so no future ArrayList method can
               collide with a method you wrote.
            6) OVERLOADING IS NOT POLYMORPHISM IN THE USEFUL SENSE.
               describe(Vehicle) and describe(int) are picked by the
               compiler from the static types, so they add convenience
               rather than removing type checks from calling code.
        */
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1: What are the four pillars of object-oriented programming?\nA1: Encapsulation, abstraction, inheritance, and polymorphism. Encapsulation keeps state private and validated by the owning object. Abstraction exposes a contract while hiding implementation. Inheritance reuses and specialises a type. Polymorphism lets one call site work correctly against many concrete types.\nQ2: Do getters and setters give you encapsulation?\nA2: Only when the setters enforce a rule. A pair that assigns whatever it is handed removes the protection the private field provided. Encapsulation means the object validates every change to its own state, so either the setter checks something or the field is final and there is no setter at all.\nQ3: How does abstraction differ from encapsulation?\nA3: Encapsulation guards data within a single object, keeping fields private and routing changes through validating methods. Abstraction guards dependencies between types, publishing a contract such as an interface while hiding the implementation. One protects state, the other reduces coupling, and confusing them is a common source of poorly designed classes.\nQ4: Which form of polymorphism does Java use, and which is more useful?\nA4: Java uses both. Compile-time polymorphism is method overloading, chosen by the compiler from declared parameter types. Run-time polymorphism is method overriding with dynamic dispatch, chosen by the JVM from the object actual type. Overriding is the useful one, because it lets a single call site work against any subtype and removes type checks from calling code.\nQ5: Can a Java class inherit from more than one class?\nA5: No. A class may extend at most one class, so there is no diamond inheritance problem to resolve. It may implement any number of interfaces, and if two of those supply the same default method you must override it explicitly to resolve the conflict.\nQ6: Why is composition usually preferred over inheritance?\nA6: Inheritance permanently couples a subclass to the internals of its base class, so any method later added to the base can collide with a method in the subclass, and the subclass is pinned to that shape. Composition holds an instance instead, so the implementation can be swapped later and no subclass inherits an unintended change.\nQ7: Do I have to apply all four pillars in every class?\nA7: No. A simple immutable value class such as a record is well designed with no inheritance and no polymorphism. Apply the principles where the problem calls for them, because forcing a hierarchy onto code that does not need one adds coupling without removing any complexity.\nQ8: How do I check whether a class truly encapsulates its state?\nA8: Confirm the fields are private and final wherever possible, check that every setter enforces an actual rule, then list the invariants the class promises and verify no method can produce a state that breaks one. If a caller could put the object into an invalid state, encapsulation is missing regardless of how many accessors exist.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Make fields private and final wherever the value cannot change after construction.\nNever expose a setter that assigns without validating, since it cancels out encapsulation.\nValidate every invariant in the constructor and in the methods that can break it.\nDeclare members private first, then widen access only when a real caller needs it.\nDepend on interfaces rather than concrete classes at every method boundary.\nUse abstract classes when subtypes must share state or constructor logic, and interfaces when they only need a contract.\nPrefer composition over inheritance, and reserve extends for genuine is-a relationships.\nAnnotate every overriding method with @Override so the compiler catches a signature mistake.\nWrite the field as a variable of the interface type, not the implementation type.\nTreat @Override as mandatory when implementing an interface method, not just when overriding a class.\nKeep inheritance depth shallow, because every level below the top forces changes everywhere above it.\nUse final fields on immutable value carriers, or a record when the class is only data.\nFavour small public surfaces, since each public method is a promise you must keep.\nRely on polymorphism to remove type checks, and reach for instanceof only when a cast is unavoidable.\nReuse code through composition and delegation before reaching for a new subclass.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.