Java · lesson 11 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Java OOP — Complete Guide
A map of Java OOP from classes and objects through the four pillars to SOLID and design patterns, with one runnable program showing every pillar.
By SkillByExample · Updated
Java is a class-based, object-oriented language. Every piece of running logic lives inside a class, and every value you work with (except the 8 primitives) is an object. Understanding OOP is not optional in Java — it is the language. The sooner you build a solid mental model of objects, classes, and the four pillars, the easier every other topic becomes.
What Is OOP?
Object-Oriented Programming organises code around objects — self-contained units that bundle data (fields) and behaviour (methods). This mirrors how we think about the real world: a BankAccount knows its own balance and knows how to deposit or withdraw. A Car knows its own speed and knows how to accelerate. This bundling of state and behaviour in one place is what makes large codebases manageable — each object is responsible for itself.
The alternative — procedural code — passes data between separate functions. OOP keeps data and the functions that operate on it together, making large codebases dramatically easier to manage.
The Four Pillars
Each pillar solves a specific design problem. Encapsulation prevents accidental state corruption. Inheritance enables code reuse across related types. Polymorphism lets one piece of code work with many different types. Abstraction lets you work at a higher level without being tangled in implementation details.
| Pillar | What it means | Java mechanism |
|---|---|---|
| Encapsulation | Hide internal state, expose a clean interface | private fields + getters/setters |
| Inheritance | Child classes reuse and extend parent classes | extends keyword |
| Polymorphism | One interface, many implementations | Method overriding + @Override |
| Abstraction | Work with concepts, not implementation details | abstract classes + interface |
Learning Path
Work through these in order. Each page builds on the previous one.
Beginner
- Introduction to OOP in Java — classes, objects, constructors,
this - Encapsulation — access modifiers, getters/setters, immutability
- Inheritance —
extends,super, method overriding,final - Polymorphism — runtime dispatch,
@Override, interface polymorphism - Abstraction —
abstractclasses,interface, default methods
Intermediate
- Interfaces — Deep Dive — segregation, default methods, functional interfaces
- Abstract Classes — template method pattern, partial implementations
- Method Overloading — compile-time polymorphism, varargs, autoboxing
- Composition vs Inheritance — when to favour composition, Decorator pattern
Advanced
- SOLID Principles — five design principles every Java engineer must know
- Design Patterns — Singleton, Factory, Observer, Strategy, Builder
- Generics and OOP — type-safe collections, bounded wildcards, generic methods
The four pillars in one program
Here is a small but complete program that uses all four pillars at once. Each comment names the pillar at work; the lessons above cover each in depth.
import java.util.List;
public class Pillars {
interface Shape { // abstraction: what, not how
double area();
default String describe() { return getClass().getSimpleName() + " " + String.format("%.1f", area()); }
}
static class Rectangle implements Shape {
private final double w, h; // encapsulation: private state
Rectangle(double w, double h) {
if (w <= 0 || h <= 0) throw new IllegalArgumentException("sides must be positive");
this.w = w; this.h = h;
}
public double area() { return w * h; }
}
static class Square extends Rectangle { // inheritance: a Square is a Rectangle
Square(double side) { super(side, side); }
}
record Circle(double r) implements Shape {
public double area() { return Math.PI * r * r; }
}
public static void main(String[] args) {
List<Shape> shapes = List.of(new Rectangle(2, 3), new Square(4), new Circle(1));
for (Shape s : shapes) System.out.println(s.describe()); // polymorphism
double total = shapes.stream().mapToDouble(Shape::area).sum();
System.out.printf("total %.2f%n", total);
}
}
$ java Pillars.java
Rectangle 6.0
Square 16.0
Circle 3.1
total 25.14
The loop never asks which kind of shape it has: each object answers area() its own way
(polymorphism) behind the Shape interface (abstraction). Square reuses Rectangle’s fields and
validation through super (inheritance), and nothing outside Rectangle can change w or h
(encapsulation).
Practice
- Add a
Triangle(double base, double height)record implementingShapeand include it in the list. Which existing code had to change?
Solution
import java.util.List;
public class AddTriangle {
interface Shape {
double area();
default String describe() { return getClass().getSimpleName() + " " + String.format("%.1f", area()); }
}
record Circle(double r) implements Shape {
public double area() { return Math.PI * r * r; }
}
record Triangle(double base, double height) implements Shape {
public double area() { return 0.5 * base * height; }
}
public static void main(String[] args) {
List<Shape> shapes = List.of(new Circle(1), new Triangle(3, 4));
for (Shape s : shapes) System.out.println(s.describe());
}
}
$ java AddTriangle.java
Circle 3.1
Triangle 6.0
Only the list changed. The loop and describe work with any Shape, which is the payoff of
polymorphism: new types plug in without editing the code that uses them.
- Try to construct
new Rectangle(-1, 2), catch the exception, and print its message. Which pillar protects the object here?
Solution
public class Guarded {
static class Rectangle {
private final double w, h;
Rectangle(double w, double h) {
if (w <= 0 || h <= 0) throw new IllegalArgumentException("sides must be positive");
this.w = w; this.h = h;
}
}
public static void main(String[] args) {
try {
new Rectangle(-1, 2);
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}
$ java Guarded.java
rejected: sides must be positive
Encapsulation: with private final fields and a validating constructor, no invalid Rectangle can
ever exist.
- Override
describe()inSquareso it printsSquare of side 4.0instead of the default, and check that the loop picks it up without changes.
Solution
import java.util.List;
public class OverrideDescribe {
interface Shape {
double area();
default String describe() { return getClass().getSimpleName() + " " + area(); }
}
static class Rectangle implements Shape {
protected final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
public double area() { return w * h; }
}
static class Square extends Rectangle {
Square(double side) { super(side, side); }
@Override public String describe() { return "Square of side " + w; }
}
public static void main(String[] args) {
for (Shape s : List.of(new Rectangle(2, 3), new Square(4))) System.out.println(s.describe());
}
}
$ java OverrideDescribe.java
Rectangle 6.0
Square of side 4.0
The call site is unchanged; the JVM picks Square’s override at runtime because that is the
object’s actual class.