Java is fundamentally an object-oriented language. Everything in Java (except primitives) is an object. OOP organizes code around objects — bundles of state (fields) and behavior (methods). The four pillars of OOP are Encapsulation, Inheritance, Polymorphism, and Abstraction. Mastering these makes you a professional Java developer.
Step 1 — Classes and Objects
A class is a blueprint. An object is an instance of that blueprint. A class defines what properties (fields) and behaviors (methods) its objects will have.
// Class = Blueprint
public class BankAccount {
// --- FIELDS (state/attributes) ---
// private: only accessible within this class (encapsulation)
private String owner;
private double balance;
private String accountNumber;
private static int totalAccounts = 0; // static field: shared across ALL instances
// --- CONSTRUCTOR: called when 'new BankAccount(...)' is executed ---
// Same name as the class. No return type (not even void).
public BankAccount(String owner, double initialBalance) {
this.owner = owner; // 'this' refers to the current object
this.balance = initialBalance;
this.accountNumber = "ACC-" + (++totalAccounts); // auto-generate
}
// --- METHODS (behavior) ---
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
this.balance += amount;
}
public void withdraw(double amount) {
if (amount > this.balance) throw new IllegalStateException("Insufficient funds");
this.balance -= amount;
}
// --- GETTERS: controlled read access to private fields ---
public double getBalance() { return balance; }
public String getOwner() { return owner; }
public String getAccountNumber(){ return accountNumber; }
// --- Static method: belongs to the class, not an instance ---
public static int getTotalAccounts() { return totalAccounts; }
// --- toString: called automatically in String context ---
@Override
public String toString() {
return String.format("BankAccount[%s, owner=%s, balance=%.2f]",
accountNumber, owner, balance);
}
}
// --- Main class to use BankAccount ---
class Main {
public static void main(String[] args) {
// 'new' allocates object on heap, calls constructor
BankAccount acc1 = new BankAccount("Kuldeep", 1000.0);
BankAccount acc2 = new BankAccount("Priya", 500.0);
acc1.deposit(500.0);
acc1.withdraw(200.0);
System.out.println(acc1); // BankAccount[ACC-1, owner=Kuldeep, balance=1300.00]
System.out.println(acc2); // BankAccount[ACC-2, owner=Priya, balance=500.00]
System.out.println("Total accounts: " + BankAccount.getTotalAccounts()); // 2
// acc1.balance = 999; // COMPILE ERROR: balance is private
}
}Step 2 — Encapsulation
Encapsulation means hiding internal state and requiring all interaction to go through well-defined methods (getters/setters). It prevents external code from putting an object into an invalid state.
public class Person {
private String name;
private int age;
private String email;
public Person(String name, int age, String email) {
setName(name); // use setters in constructor for validation
setAge(age);
setEmail(email);
}
public String getName() { return name; }
public void setName(String name) {
if (name == null || name.isBlank())
throw new IllegalArgumentException("Name cannot be empty");
this.name = name.trim();
}
public int getAge() { return age; }
public void setAge(int age) {
if (age < 0 || age > 150)
throw new IllegalArgumentException("Invalid age: " + age);
this.age = age;
}
public String getEmail() { return email; }
public void setEmail(String email) {
if (email == null || !email.contains("@"))
throw new IllegalArgumentException("Invalid email");
this.email = email.toLowerCase();
}
// Read-only computed property
public boolean isAdult() { return age >= 18; }
}Step 3 — Constructors: Default, Parameterized, and this()
public class Product {
private String name;
private double price;
private int stock;
// No-arg constructor (also called default constructor)
public Product() {
this("Unknown", 0.0, 0); // calls the 3-arg constructor — this() must be FIRST line
}
// 2-arg constructor
public Product(String name, double price) {
this(name, price, 100); // chain to 3-arg constructor
}
// The "master" constructor — all others delegate here
public Product(String name, double price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
@Override
public String toString() {
return name + " ($" + price + ", stock: " + stock + ")";
}
public static void main(String[] args) {
Product p1 = new Product(); // Unknown ($0.0, stock: 0)
Product p2 = new Product("Laptop", 999.99); // Laptop ($999.99, stock: 100)
Product p3 = new Product("Phone", 499.99, 50); // Phone ($499.99, stock: 50)
System.out.println(p1);
System.out.println(p2);
System.out.println(p3);
}
}Step 4 — Inheritance
Inheritance allows a child class (subclass) to inherit fields and methods from a parent class (superclass). Java supports single inheritance for classes (one parent only) but multiple inheritance through interfaces.
// --- Parent class (Superclass) ---
public class Animal {
protected String name; // protected: accessible in this class AND subclasses
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
public String describe() {
return "Animal: " + name + ", Age: " + age;
}
}
// --- Child class: Dog extends Animal ---
// Dog INHERITS all non-private fields and methods of Animal
class Dog extends Animal {
private String breed;
// super(...) calls the parent constructor — MUST be first line
public Dog(String name, int age, String breed) {
super(name, age); // initialize the Animal part
this.breed = breed;
}
// Dog adds its OWN behavior
public void bark() {
System.out.println(name + " says: Woof!");
}
// @Override: overrides the parent's describe() method
@Override
public String describe() {
return super.describe() + ", Breed: " + breed; // reuse parent's output
}
}
// --- Another child: Cat extends Animal ---
class Cat extends Animal {
private boolean isIndoor;
public Cat(String name, int age, boolean isIndoor) {
super(name, age);
this.isIndoor = isIndoor;
}
public void purr() {
System.out.println(name + " purrs...");
}
@Override
public String describe() {
return super.describe() + (isIndoor ? " (indoor)" : " (outdoor)");
}
}
class InheritanceDemo {
public static void main(String[] args) {
Dog dog = new Dog("Rex", 3, "Labrador");
Cat cat = new Cat("Whiskers", 5, true);
dog.eat(); // inherited from Animal: "Rex is eating."
dog.bark(); // Dog's own method: "Rex says: Woof!"
dog.sleep(); // inherited: "Rex is sleeping."
System.out.println(dog.describe()); // Animal: Rex, Age: 3, Breed: Labrador
cat.eat(); // inherited: "Whiskers is eating."
cat.purr(); // Cat's own: "Whiskers purrs..."
System.out.println(cat.describe()); // Animal: Whiskers, Age: 5 (indoor)
// IS-A relationship: Dog IS-A Animal
Animal a = new Dog("Buddy", 2, "Poodle"); // Dog reference stored as Animal
a.eat(); // works: eat() is in Animal
// a.bark(); // COMPILE ERROR: Animal reference doesn't know about bark()
((Dog) a).bark(); // downcast to access Dog-specific method
}
}Step 5 — Polymorphism
Polymorphism means 'many forms'. In Java there are two types: Compile-time polymorphism (method overloading — decided at compile time) and Runtime polymorphism (method overriding — decided at runtime based on the actual object type).
// Runtime Polymorphism — the most powerful form
class Shape {
public double area() {
return 0; // base implementation
}
public String describe() {
return "Shape with area: " + area();
}
}
class Circle extends Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override
public double area() {
return width * height;
}
}
class Triangle extends Shape {
private double base, height;
public Triangle(double b, double h) { this.base = b; this.height = h; }
@Override
public double area() {
return 0.5 * base * height;
}
}
class PolymorphismDemo {
// This method works for ANY Shape — present or future!
static void printArea(Shape shape) {
// At RUNTIME, Java calls the ACTUAL object's area() method
System.out.printf("%s -> Area: %.2f%n",
shape.getClass().getSimpleName(), shape.area());
}
public static void main(String[] args) {
// All stored as 'Shape' references
Shape[] shapes = {
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
};
// Dynamic dispatch: the correct area() is called based on actual type
for (Shape s : shapes) {
printArea(s);
}
// Circle -> Area: 78.54
// Rectangle -> Area: 24.00
// Triangle -> Area: 12.00
// describe() calls area() which is polymorphic!
System.out.println(new Circle(5).describe());
// Shape with area: 78.53981633974483
}
}Step 6 — Abstraction: Abstract Classes and Interfaces
Abstraction means hiding implementation details and exposing only what is necessary. Java provides two mechanisms: abstract classes and interfaces.
// Abstract class: cannot be instantiated directly
// Can have both abstract methods AND concrete methods AND fields
public abstract class Vehicle {
protected String brand;
protected int year;
protected double fuelLevel;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
this.fuelLevel = 100.0;
}
// Abstract method: MUST be implemented by all subclasses
public abstract void startEngine();
public abstract double fuelConsumptionPerKm();
// Concrete method: shared logic for all vehicles
public void drive(double km) {
double fuelNeeded = km * fuelConsumptionPerKm();
if (fuelLevel < fuelNeeded)
throw new IllegalStateException("Not enough fuel!");
fuelLevel -= fuelNeeded;
System.out.printf("%s drove %.1f km. Fuel left: %.1f%%%n",
brand, km, fuelLevel);
}
public String getInfo() {
return brand + " (" + year + ")";
}
}
class Car extends Vehicle {
public Car(String brand, int year) { super(brand, year); }
@Override
public void startEngine() {
System.out.println(brand + ": Vroom! Engine started.");
}
@Override
public double fuelConsumptionPerKm() { return 0.08; } // 8L/100km
}
class Truck extends Vehicle {
private double cargoWeight;
public Truck(String brand, int year, double cargoWeight) {
super(brand, year);
this.cargoWeight = cargoWeight;
}
@Override
public void startEngine() {
System.out.println(brand + ": ROAR! Diesel engine started.");
}
@Override
public double fuelConsumptionPerKm() {
return 0.15 + (cargoWeight * 0.001); // heavier cargo = more fuel
}
}// Interface: a pure contract (Java 8+ can have default/static methods)
public interface Flyable {
// Constants in interfaces are implicitly public static final
int MAX_ALTITUDE = 40000;
// Abstract method: all implementing classes must implement this
void fly(int altitude);
void land();
// Default method (Java 8+): provides a default implementation
default void hover() {
System.out.println("Hovering in place...");
}
// Static method (Java 8+): utility method on the interface
static void printMaxAltitude() {
System.out.println("Max altitude: " + MAX_ALTITUDE + " ft");
}
}
interface Swimmable {
void swim(int depth);
default void float_() { System.out.println("Floating on surface..."); }
}
// Java supports MULTIPLE interface implementation (unlike extends)
class Duck extends Animal implements Flyable, Swimmable {
public Duck(String name) { super(name, 1); }
@Override
public void fly(int altitude) {
System.out.println(name + " is flying at " + altitude + " ft.");
}
@Override
public void land() {
System.out.println(name + " has landed.");
}
@Override
public void swim(int depth) {
System.out.println(name + " is swimming at " + depth + " m depth.");
}
}
class InterfaceDemo {
public static void main(String[] args) {
Duck duck = new Duck("Donald");
duck.fly(500);
duck.swim(2);
duck.hover(); // uses default method from Flyable
duck.float_(); // uses default method from Swimmable
// Interfaces as types (polymorphism!)
Flyable f = duck; // Duck IS-A Flyable
f.fly(1000);
// f.swim(5); // COMPILE ERROR: Flyable reference doesn't know swim()
Flyable.printMaxAltitude(); // static interface method
}
}Abstract Class vs Interface: When to Use Which
- Use abstract class when: classes share common STATE (fields) and behavior; you have an IS-A relationship; you need constructors or protected members.
- Use interface when: defining a CONTRACT that unrelated classes can implement; you need multiple 'types' for one class; you have no shared state.
- Rule of thumb: interfaces define WHAT an object can do; abstract classes define WHAT an object IS.
- Java 8+: interfaces can have default methods, blurring the line slightly. Prefer interfaces for most abstractions in modern Java.
Step 7 — equals(), hashCode(), and Comparable
These three methods are critical for using your objects correctly in collections (HashMap, HashSet, TreeSet, etc.).
import java.util.*;
public class Student implements Comparable<Student> {
private String id;
private String name;
private double gpa;
public Student(String id, String name, double gpa) {
this.id = id;
this.name = name;
this.gpa = gpa;
}
// equals() must be consistent with hashCode()!
// Two objects that are equal() MUST have the same hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true; // same reference
if (o == null || getClass() != o.getClass()) return false; // null/type check
Student student = (Student) o;
return Objects.equals(id, student.id); // equality based on ID
}
@Override
public int hashCode() {
return Objects.hash(id); // same field(s) as equals!
}
// Comparable: defines NATURAL ORDERING (e.g., for TreeSet, Collections.sort)
@Override
public int compareTo(Student other) {
// Sort by GPA descending (highest GPA first)
return Double.compare(other.gpa, this.gpa);
}
@Override
public String toString() {
return String.format("Student[%s, %s, GPA=%.2f]", id, name, gpa);
}
public static void main(String[] args) {
Student s1 = new Student("S001", "Kuldeep", 3.9);
Student s2 = new Student("S001", "Kuldeep", 3.9);
Student s3 = new Student("S002", "Priya", 3.7);
System.out.println(s1.equals(s2)); // true (same id)
System.out.println(s1 == s2); // false (different objects)
Set<Student> set = new HashSet<>();
set.add(s1);
set.add(s2); // NOT added — equals() and hashCode() say it's a duplicate
System.out.println(set.size()); // 1
List<Student> students = new ArrayList<>(Arrays.asList(s1, s3,
new Student("S003", "Amit", 3.95)));
Collections.sort(students); // uses compareTo — sorts by GPA desc
students.forEach(System.out::println);
// Student[S003, Amit, GPA=3.95]
// Student[S001, Kuldeep, GPA=3.90]
// Student[S002, Priya, GPA=3.70]
}
}Step 8 — Enums
// Enum: a special class for a fixed set of constants
// Enums can have fields, constructors, and methods!
public enum OrderStatus {
PENDING("Order placed, awaiting processing"),
PROCESSING("Order is being prepared"),
SHIPPED("Order has been shipped"),
DELIVERED("Order delivered successfully"),
CANCELLED("Order was cancelled");
private final String description; // each enum constant has a description
// Enum constructor (always private)
OrderStatus(String description) {
this.description = description;
}
public String getDescription() { return description; }
public boolean isTerminal() {
return this == DELIVERED || this == CANCELLED;
}
}
class EnumDemo {
public static void main(String[] args) {
OrderStatus status = OrderStatus.SHIPPED;
System.out.println(status); // SHIPPED
System.out.println(status.name()); // SHIPPED (string)
System.out.println(status.ordinal()); // 2 (0-indexed position)
System.out.println(status.getDescription()); // Order has been shipped
System.out.println(status.isTerminal()); // false
// Enums work beautifully with switch
switch (status) {
case PENDING -> System.out.println("Waiting...");
case SHIPPED -> System.out.println("On the way!");
case DELIVERED -> System.out.println("Enjoy!");
default -> System.out.println("Other: " + status);
}
// Iterate all enum values
for (OrderStatus s : OrderStatus.values()) {
System.out.printf("%-12s -> %s%n", s, s.getDescription());
}
// Convert string to enum
OrderStatus fromString = OrderStatus.valueOf("PENDING");
System.out.println(fromString.getDescription()); // Order placed...
}
}Step 9 — Inner Classes and Anonymous Classes
public class Outer {
private int value = 10;
// --- Regular inner class: has access to outer class members ---
class Inner {
void display() {
System.out.println("Outer.value = " + value); // accesses outer's private!
}
}
// --- Static nested class: does NOT hold a reference to outer instance ---
// Use this when the nested class doesn't need outer class members
static class StaticNested {
void display() {
// System.out.println(value); // COMPILE ERROR: no access to outer instance
System.out.println("I am a static nested class");
}
}
// --- Local class: defined inside a method ---
void methodWithLocalClass() {
class Local {
void greet() { System.out.println("Local class says hi!"); }
}
new Local().greet();
}
public static void main(String[] args) {
Outer outer = new Outer();
// Inner class requires outer instance
Outer.Inner inner = outer.new Inner();
inner.display(); // Outer.value = 10
// Static nested class: no outer instance needed
Outer.StaticNested nested = new Outer.StaticNested();
nested.display();
outer.methodWithLocalClass();
// --- Anonymous class: one-time implementation of interface/abstract class ---
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Running anonymously!");
}
};
r.run();
// Modern equivalent: lambda expression (Java 8+)
Runnable lambda = () -> System.out.println("Running with lambda!");
lambda.run();
}
}