Back to blog
Backend Systemsbeginner

Object-Oriented Programming in Java

Master Java OOP: classes, constructors, inheritance, interfaces, abstract classes, polymorphism, and encapsulation with practical examples.

Asma HafeezApril 17, 20266 min read
javaoopclassesinheritanceinterfaces
Share:𝕏

Object-Oriented Programming in Java

Java is built around objects. Understanding OOP well means you can design code that scales without becoming a mess.


Classes and Objects

JAVA
public class Person {
    // Fields (instance variables)
    String name;
    int age;

    // Constructor
    public Person(String name, int age) {
        this.name = name;  // 'this' disambiguates field vs parameter
        this.age = age;
    }

    // Method
    public void greet() {
        System.out.println("Hi, I'm " + name + " and I'm " + age + " years old.");
    }
}

// Using the class
Person alice = new Person("Alice", 30);
alice.greet();  // Hi, I'm Alice and I'm 30 years old.

Encapsulation — Private Fields + Getters/Setters

JAVA
public class BankAccount {
    private String owner;
    private double balance;  // private — not accessible directly

    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance;
    }

    // Getter
    public double getBalance() {
        return balance;
    }

    // Setter with validation
    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }

    @Override
    public String toString() {
        return String.format("BankAccount[%s, balance=%.2f]", owner, balance);
    }
}

BankAccount account = new BankAccount("Alice", 1000.0);
account.deposit(500.0);
System.out.println(account.getBalance()); // 1500.0
// account.balance = 9999; // compile error — private!

Inheritance

JAVA
// Base class
public class Animal {
    protected String name;
    protected String sound;

    public Animal(String name, String sound) {
        this.name = name;
        this.sound = sound;
    }

    public void makeSound() {
        System.out.println(name + " says " + sound);
    }

    public String getName() { return name; }
}

// Subclass
public class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name, "Woof");  // call parent constructor
        this.breed = breed;
    }

    @Override  // overrides parent method
    public void makeSound() {
        System.out.println(name + " barks: WOOF WOOF!");
    }

    public void fetch() {
        System.out.println(name + " fetches the ball!");
    }

    public String getBreed() { return breed; }
}

Dog rex = new Dog("Rex", "Labrador");
rex.makeSound();  // Rex barks: WOOF WOOF!
rex.fetch();      // Rex fetches the ball!
System.out.println(rex.getName()); // Rex (inherited from Animal)

Polymorphism

JAVA
Animal animal1 = new Dog("Rex", "Lab");
Animal animal2 = new Cat("Whiskers");  // assuming Cat extends Animal

animal1.makeSound();  // Dog's version
animal2.makeSound();  // Cat's version

// Arrays of the base type
Animal[] animals = { new Dog("Rex", "Lab"), new Cat("Kitty") };
for (Animal a : animals) {
    a.makeSound();  // polymorphic dispatch
}

// instanceof check before casting
if (animal1 instanceof Dog dog) {    // Java 16+ pattern matching
    dog.fetch();  // safe — we know it's a Dog
}

Abstract Classes

JAVA
// Can't instantiate — must be subclassed
public abstract class Shape {
    private String color;

    public Shape(String color) {
        this.color = color;
    }

    // Abstract method — no body, must be implemented by subclasses
    public abstract double area();
    public abstract double perimeter();

    // Concrete method — shared by all shapes
    public void describe() {
        System.out.printf("%s %s: area=%.2f%n", color, getClass().getSimpleName(), area());
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() { return Math.PI * radius * radius; }

    @Override
    public double perimeter() { return 2 * Math.PI * radius; }
}

public class Rectangle extends Shape {
    private double width, height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() { return width * height; }

    @Override
    public double perimeter() { return 2 * (width + height); }
}

// Usage
Shape[] shapes = { new Circle("red", 5), new Rectangle("blue", 3, 4) };
for (Shape s : shapes) {
    s.describe();  // polymorphic call to area()
}

Interfaces

JAVA
// Interface — defines a contract
public interface Printable {
    void print();  // implicitly public abstract
}

public interface Saveable {
    void save(String filename);

    // Default method (Java 8+) — optional override
    default void saveWithTimestamp(String filename) {
        save(filename + "_" + System.currentTimeMillis());
    }
}

// A class can implement multiple interfaces
public class Document implements Printable, Saveable {
    private String content;

    public Document(String content) {
        this.content = content;
    }

    @Override
    public void print() {
        System.out.println("Document: " + content);
    }

    @Override
    public void save(String filename) {
        System.out.println("Saving to " + filename);
    }
}

Document doc = new Document("Hello World");
doc.print();                        // Document: Hello World
doc.save("output.txt");             // Saving to output.txt
doc.saveWithTimestamp("output");    // Saving to output_1713369600000

Interface vs Abstract Class

| | Interface | Abstract Class | |---|---|---| | Multiple inheritance | Yes (implement many) | No (extend one) | | Fields | Constants only | Any | | Constructor | No | Yes | | Use when | Defining a capability | Sharing base implementation |


Records (Java 16+)

Records are immutable data classes with less boilerplate.

JAVA
// This single line generates: constructor, getters, equals, hashCode, toString
public record Point(double x, double y) {
    // Optional: custom method
    public double distanceTo(Point other) {
        double dx = this.x - other.x;
        double dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.x());             // 0.0 (getter)
System.out.println(p1.distanceTo(p2));  // 5.0
System.out.println(p1);                 // Point[x=0.0, y=0.0]

Enums

JAVA
public enum Priority {
    LOW, MEDIUM, HIGH;

    public boolean isUrgent() {
        return this == HIGH;
    }
}

Priority p = Priority.HIGH;
System.out.println(p.isUrgent()); // true
System.out.println(p.name());     // "HIGH"
System.out.println(p.ordinal());  // 2

// switch with enum
String label = switch (p) {
    case LOW    -> "Take your time";
    case MEDIUM -> "Normal pace";
    case HIGH   -> "Do it now!";
};

Static Members

JAVA
public class Counter {
    private static int total = 0;  // shared across all instances
    private int id;

    public Counter() {
        total++;
        this.id = total;
    }

    public int getId() { return id; }
    public static int getTotal() { return total; }
}

Counter a = new Counter();
Counter b = new Counter();
Counter c = new Counter();

System.out.println(Counter.getTotal());  // 3
System.out.println(a.getId());           // 1
System.out.println(c.getId());           // 3

Key Takeaways

  1. Encapsulate fields with private and expose behavior through methods
  2. Prefer interfaces over abstract classes when sharing only a contract (not state)
  3. Use @Override to make overrides explicit and catch typos at compile time
  4. Records are the modern way to write immutable data classes — less boilerplate
  5. Polymorphism via interfaces is what makes dependency injection work — depend on abstractions, not concrete types

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.