Object-Oriented Programming
Master the pillars of OOP in Java
Object-Oriented Programming in Java
Master the fundamentals of Object-Oriented Programming (OOP) in Java with free flashcards and interactive practice. This lesson covers classes, objects, inheritance, polymorphism, encapsulation, and abstractionโessential concepts for building robust, maintainable Java applications and acing technical interviews.
Welcome to Object-Oriented Programming! ๐ป
Object-Oriented Programming is the cornerstone of modern Java development. Instead of thinking in terms of procedures and functions, OOP allows you to model real-world entities as objects that contain both data (fields) and behavior (methods). This paradigm revolutionized software engineering by making code more modular, reusable, and easier to understand.
Think of OOP like building with LEGO blocks ๐งฑ: each piece (object) has its own shape and purpose, and you can combine them in countless ways to create complex structures. Once you understand the four pillars of OOPโencapsulation, inheritance, polymorphism, and abstractionโyou'll be able to design elegant solutions to complex problems.
Core Concepts of OOP ๐ฏ
1. Classes and Objects
A class is a blueprint or template that defines the structure and behavior of objects. An object is a specific instance of a classโit's the actual entity that exists in memory during program execution.
Real-world analogy ๐: Think of a class as an architectural blueprint for a house. The blueprint defines rooms, doors, and windows, but it's not a house itself. When builders construct actual houses from that blueprint, each house is an objectโa concrete instance with its own address, color, and occupants.
// Class definition (the blueprint)
public class Dog {
// Fields (data/state)
String name;
int age;
String breed;
// Constructor (creates objects)
public Dog(String name, int age, String breed) {
this.name = name;
this.age = age;
this.breed = breed;
}
// Methods (behavior)
public void bark() {
System.out.println(name + " says: Woof!");
}
public void sleep() {
System.out.println(name + " is sleeping...");
}
}
// Creating objects (instances)
Dog myDog = new Dog("Buddy", 3, "Golden Retriever");
Dog yourDog = new Dog("Max", 5, "Labrador");
myDog.bark(); // Output: Buddy says: Woof!
yourDog.bark(); // Output: Max says: Woof!
๐ก Tip: The this keyword refers to the current object instance. It's especially useful when parameter names match field names, helping distinguish between them.
2. Encapsulation ๐
Encapsulation is the practice of bundling data (fields) and methods that operate on that data within a single unit (class), while restricting direct access to some components. This is achieved using access modifiers and getter/setter methods.
Why encapsulation matters: It protects object integrity by preventing external code from making invalid changes. Imagine a bank accountโyou shouldn't be able to directly set your balance to any value; instead, you should deposit or withdraw through controlled methods.
| Access Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
| private | โ | โ | โ | โ |
| default (no modifier) | โ | โ | โ | โ |
| protected | โ | โ | โ | โ |
| public | โ | โ | โ | โ |
public class BankAccount {
// Private fields - cannot be accessed directly from outside
private String accountNumber;
private double balance;
private String ownerName;
public BankAccount(String accountNumber, String ownerName) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
this.balance = 0.0;
}
// Getter method (read-only access)
public double getBalance() {
return balance;
}
// Controlled methods to modify balance
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: $" + amount);
} else {
System.out.println("Invalid deposit amount");
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("Withdrawn: $" + amount);
} else {
System.out.println("Invalid or insufficient funds");
}
}
}
๐ง Memory device: PPPP = Private Protects, Public Permits (Private keeps things safe inside, Public allows access from anywhere)
3. Inheritance ๐จโ๐ฉโ๐งโ๐ฆ
Inheritance allows a class to inherit fields and methods from another class, promoting code reuse and establishing hierarchical relationships. The class being inherited from is the parent/superclass, and the class inheriting is the child/subclass.
Real-world analogy ๐: Think of biological inheritance. A "Mammal" class might have properties like "warm-blooded" and "has hair." Dogs, cats, and humans are all specific types of mammals that inherit these characteristics but add their own unique features.
INHERITANCE HIERARCHY
โโโโโโโโโโโโ
โ Animal โ
โ โ
โ +eat() โ
โ +sleep() โ
โโโโโโโฌโโโโโ
โ
โโโโโโโโโโดโโโโโโโโโ
โ โ
โโโโโโดโโโโโ โโโโโโโดโโโโโโ
โ Dog โ โ Cat โ
โ โ โ โ
โ +bark() โ โ +meow() โ
โโโโโโโโโโโ โโโโโโโโโโโโโ
// Superclass (parent)
public class Animal {
protected String name;
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.");
}
}
// Subclass (child) - inherits from Animal
public class Dog extends Animal {
private String breed;
// Constructor must call parent constructor
public Dog(String name, int age, String breed) {
super(name, age); // Calls Animal constructor
this.breed = breed;
}
// Dog-specific method
public void bark() {
System.out.println(name + " barks: Woof!");
}
// Method overriding
@Override
public void eat() {
System.out.println(name + " is eating dog food.");
}
}
// Usage
Dog myDog = new Dog("Buddy", 3, "Beagle");
myDog.eat(); // Output: Buddy is eating dog food.
myDog.sleep(); // Output: Buddy is sleeping. (inherited)
myDog.bark(); // Output: Buddy barks: Woof!
๐ก Tip: Use the super keyword to access parent class constructors and methods. The @Override annotation helps catch errors by ensuring you're actually overriding a parent method.
๐ค Did you know? Java doesn't support multiple inheritance (a class can't extend multiple classes) to avoid the "diamond problem." However, Java allows implementing multiple interfaces, which we'll discuss later!
4. Polymorphism ๐ญ
Polymorphism (Greek for "many forms") allows objects of different classes to be treated as objects of a common superclass. It's the ability of a single interface to represent different underlying forms (data types).
There are two types:
- Compile-time polymorphism (Method Overloading): Multiple methods with the same name but different parameters
- Runtime polymorphism (Method Overriding): Subclass provides specific implementation of a method already defined in its superclass
Real-world analogy ๐: Think of a universal remote control. The "power" button works on your TV, stereo, and DVD player, but each device responds differently. Same button (interface), different behaviors (implementations).
// Method Overloading (compile-time polymorphism)
public class Calculator {
// Same method name, different parameters
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
// Method Overriding (runtime polymorphism)
public class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
}
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle โญ");
}
}
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle โญ");
}
}
// Polymorphic behavior
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
Shape shape3 = new Shape();
shape1.draw(); // Output: Drawing a circle โญ
shape2.draw(); // Output: Drawing a rectangle โญ
shape3.draw(); // Output: Drawing a shape
๐ก Tip: Polymorphism enables you to write flexible, extensible code. You can add new Shape subclasses without modifying code that uses the Shape interface.
5. Abstraction ๐จ
Abstraction means hiding complex implementation details and showing only essential features. It's achieved through abstract classes and interfaces.
Abstract classes:
- Cannot be instantiated directly
- Can have both abstract methods (no implementation) and concrete methods (with implementation)
- Use
abstractkeyword
Interfaces:
- Pure abstraction (traditionally all methods were abstract)
- A class can implement multiple interfaces
- Use
implementskeyword - Since Java 8, interfaces can have default and static methods
Real-world analogy ๐: Think of a car dashboard. You see the speedometer, steering wheel, and pedals (abstraction), but you don't see the complex engine mechanics, fuel injection systems, or transmission details (hidden implementation).
// Abstract class
public abstract class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
// Abstract method (no implementation)
public abstract void start();
// Concrete method (with implementation)
public void stop() {
System.out.println(brand + " vehicle stopped.");
}
}
public class Car extends Vehicle {
public Car(String brand) {
super(brand);
}
@Override
public void start() {
System.out.println(brand + " car started with ignition key.");
}
}
// Interface
public interface Flyable {
void fly(); // Implicitly public and abstract
void land();
}
public class Airplane extends Vehicle implements Flyable {
public Airplane(String brand) {
super(brand);
}
@Override
public void start() {
System.out.println(brand + " airplane engines started.");
}
@Override
public void fly() {
System.out.println(brand + " airplane is flying โ๏ธ");
}
@Override
public void land() {
System.out.println(brand + " airplane is landing.");
}
}
| Feature | Abstract Class | Interface |
|---|---|---|
| Methods | Can have both abstract and concrete methods | Abstract by default (can have default/static since Java 8) |
| Fields | Can have any type of fields | Only public static final constants |
| Constructor | Can have constructors | Cannot have constructors |
| Multiple | A class can extend only one abstract class | A class can implement multiple interfaces |
| Access Modifiers | Can use any access modifier | Methods are public by default |
Detailed Examples with Explanations ๐
Example 1: Building a Library Management System
Let's create a practical system that demonstrates all OOP principles:
// Abstract base class
public abstract class LibraryItem {
private String id;
private String title;
protected boolean isCheckedOut;
public LibraryItem(String id, String title) {
this.id = id;
this.title = title;
this.isCheckedOut = false;
}
// Abstract method - each item type implements differently
public abstract void displayInfo();
// Concrete methods - common to all items
public void checkOut() {
if (!isCheckedOut) {
isCheckedOut = true;
System.out.println(title + " checked out successfully.");
} else {
System.out.println(title + " is already checked out.");
}
}
public void returnItem() {
if (isCheckedOut) {
isCheckedOut = false;
System.out.println(title + " returned successfully.");
}
}
public String getTitle() {
return title;
}
}
// Concrete subclass - Book
public class Book extends LibraryItem {
private String author;
private int pages;
public Book(String id, String title, String author, int pages) {
super(id, title);
this.author = author;
this.pages = pages;
}
@Override
public void displayInfo() {
System.out.println("๐ Book: " + getTitle());
System.out.println(" Author: " + author);
System.out.println(" Pages: " + pages);
System.out.println(" Status: " + (isCheckedOut ? "Checked Out" : "Available"));
}
}
// Concrete subclass - DVD
public class DVD extends LibraryItem {
private String director;
private int runtime; // in minutes
public DVD(String id, String title, String director, int runtime) {
super(id, title);
this.director = director;
this.runtime = runtime;
}
@Override
public void displayInfo() {
System.out.println("๐ DVD: " + getTitle());
System.out.println(" Director: " + director);
System.out.println(" Runtime: " + runtime + " minutes");
System.out.println(" Status: " + (isCheckedOut ? "Checked Out" : "Available"));
}
}
// Usage demonstration
public class LibraryDemo {
public static void main(String[] args) {
// Polymorphism - array of parent type holds different child objects
LibraryItem[] items = new LibraryItem[3];
items[0] = new Book("B001", "Clean Code", "Robert Martin", 464);
items[1] = new DVD("D001", "The Matrix", "Wachowski", 136);
items[2] = new Book("B002", "Effective Java", "Joshua Bloch", 416);
// Display all items
for (LibraryItem item : items) {
item.displayInfo(); // Polymorphic method call
System.out.println();
}
// Check out an item
items[0].checkOut();
items[0].displayInfo();
}
}
Key takeaways from this example:
- Encapsulation: Private fields with controlled access
- Inheritance: Book and DVD inherit from LibraryItem
- Polymorphism: Array of LibraryItem can hold different types
- Abstraction: Abstract displayInfo() method forces subclasses to provide implementations
Example 2: Payment Processing System
This example shows how interfaces enable multiple inheritance of behavior:
// Interface for payment processing
public interface Payable {
void processPayment(double amount);
boolean refund(double amount);
}
// Interface for transactions
public interface Transactional {
String getTransactionId();
void logTransaction(String details);
}
// Base class for payment methods
public abstract class PaymentMethod {
protected String accountHolder;
protected double balance;
public PaymentMethod(String accountHolder, double balance) {
this.accountHolder = accountHolder;
this.balance = balance;
}
public double getBalance() {
return balance;
}
}
// CreditCard implements multiple interfaces
public class CreditCard extends PaymentMethod implements Payable, Transactional {
private String cardNumber;
private String transactionId;
public CreditCard(String accountHolder, String cardNumber, double creditLimit) {
super(accountHolder, creditLimit);
this.cardNumber = maskCardNumber(cardNumber);
this.transactionId = "";
}
@Override
public void processPayment(double amount) {
if (amount <= balance) {
balance -= amount;
transactionId = generateTransactionId();
System.out.println("๐ณ Charged $" + amount + " to card ending in " + cardNumber);
logTransaction("Payment: $" + amount);
} else {
System.out.println("โ Insufficient credit limit");
}
}
@Override
public boolean refund(double amount) {
balance += amount;
transactionId = generateTransactionId();
logTransaction("Refund: $" + amount);
System.out.println("โ
Refunded $" + amount + " to card");
return true;
}
@Override
public String getTransactionId() {
return transactionId;
}
@Override
public void logTransaction(String details) {
System.out.println("[LOG] " + details + " | TxnID: " + transactionId);
}
private String maskCardNumber(String number) {
return "****" + number.substring(number.length() - 4);
}
private String generateTransactionId() {
return "TXN" + System.currentTimeMillis();
}
}
// PayPal - different implementation of same interfaces
public class PayPal extends PaymentMethod implements Payable, Transactional {
private String email;
private String transactionId;
public PayPal(String accountHolder, String email, double balance) {
super(accountHolder, balance);
this.email = email;
this.transactionId = "";
}
@Override
public void processPayment(double amount) {
if (amount <= balance) {
balance -= amount;
transactionId = "PP" + System.currentTimeMillis();
System.out.println("๐ฐ Paid $" + amount + " via PayPal (" + email + ")");
logTransaction("Payment: $" + amount);
} else {
System.out.println("โ Insufficient PayPal balance");
}
}
@Override
public boolean refund(double amount) {
balance += amount;
transactionId = "PP" + System.currentTimeMillis();
logTransaction("Refund: $" + amount);
System.out.println("โ
Refunded $" + amount + " to PayPal");
return true;
}
@Override
public String getTransactionId() {
return transactionId;
}
@Override
public void logTransaction(String details) {
System.out.println("[PAYPAL LOG] " + details + " | Email: " + email);
}
}
Why this design is powerful:
- Different payment methods share common interfaces
- New payment types can be added without changing existing code
- Code that uses
Payableinterface works with any payment method - Multiple interfaces allow mixing different capabilities
Example 3: Game Character System
This example demonstrates constructor chaining and method overriding:
public class Character {
private String name;
private int health;
private int level;
// Default constructor
public Character() {
this("Unknown", 100, 1);
}
// Constructor with name only
public Character(String name) {
this(name, 100, 1);
}
// Full constructor (called by others)
public Character(String name, int health, int level) {
this.name = name;
this.health = health;
this.level = level;
}
public void attack() {
System.out.println(name + " performs a basic attack!");
}
public void takeDamage(int damage) {
health -= damage;
System.out.println(name + " took " + damage + " damage. Health: " + health);
}
public boolean isAlive() {
return health > 0;
}
}
public class Warrior extends Character {
private int armor;
public Warrior(String name) {
super(name, 150, 1); // Warriors start with more health
this.armor = 20;
}
@Override
public void attack() {
System.out.println("โ๏ธ " + super.toString() + " swings a mighty sword!");
}
// New method specific to Warrior
public void shieldBlock() {
System.out.println("๐ก๏ธ Warrior raises shield, blocking incoming damage!");
}
@Override
public void takeDamage(int damage) {
int reducedDamage = Math.max(0, damage - armor);
super.takeDamage(reducedDamage);
System.out.println("(Armor absorbed " + (damage - reducedDamage) + " damage)");
}
}
public class Mage extends Character {
private int mana;
public Mage(String name) {
super(name, 80, 1); // Mages have less health
this.mana = 100;
}
@Override
public void attack() {
if (mana >= 10) {
mana -= 10;
System.out.println("๐ฎ " + super.toString() + " casts a fireball! (Mana: " + mana + ")");
} else {
System.out.println("๐ซ Not enough mana for spell!");
}
}
public void meditate() {
mana = 100;
System.out.println("๐ง Mage meditates and restores mana to full.");
}
}
Example 4: Static Members and Utility Classes
Static members belong to the class itself, not to instances:
public class MathUtils {
// Static constant
public static final double PI = 3.14159265359;
// Static variable - shared by all instances
private static int calculationCount = 0;
// Static method - can be called without creating an object
public static double calculateCircleArea(double radius) {
calculationCount++;
return PI * radius * radius;
}
public static double calculateCircleCircumference(double radius) {
calculationCount++;
return 2 * PI * radius;
}
public static int getCalculationCount() {
return calculationCount;
}
// Private constructor prevents instantiation
private MathUtils() {
throw new UnsupportedOperationException("Utility class");
}
}
// Usage - no object creation needed
double area = MathUtils.calculateCircleArea(5.0);
double circumference = MathUtils.calculateCircleCircumference(5.0);
System.out.println("Calculations performed: " + MathUtils.getCalculationCount());
๐ก Tip: Use static methods for utility functions that don't need object state. Common examples: Math.sqrt(), Arrays.sort(), Collections.shuffle().
Common Mistakes to Avoid โ ๏ธ
1. Forgetting to Call super() in Constructors
โ Wrong:
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
// Missing super(name, age)!
this.breed = breed; // Compiler error!
}
}
โ Right:
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // Must be first line
this.breed = breed;
}
}
2. Breaking Encapsulation with Public Fields
โ Wrong:
public class Student {
public String name; // Anyone can modify!
public int grade; // No validation possible!
}
โ Right:
public class Student {
private String name;
private int grade;
public void setGrade(int grade) {
if (grade >= 0 && grade <= 100) {
this.grade = grade;
} else {
throw new IllegalArgumentException("Invalid grade");
}
}
}
3. Overriding vs Overloading Confusion
โ Wrong - thinking this is overriding:
public class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
public class Dog extends Animal {
// This is overLOADING, not overRIDING - different signature!
public void makeSound(String mood) {
System.out.println("Woof woof");
}
}
โ Right - proper overriding:
public class Dog extends Animal {
@Override // Same signature
public void makeSound() {
System.out.println("Woof woof");
}
}
4. Trying to Instantiate Abstract Classes or Interfaces
โ Wrong:
Animal animal = new Animal(); // Error if Animal is abstract!
Runnable task = new Runnable(); // Error - Runnable is an interface!
โ Right:
Animal animal = new Dog(); // Use concrete subclass
Runnable task = new Runnable() { // Anonymous class
@Override
public void run() {
System.out.println("Running task");
}
};
5. Misusing Static Members
โ Wrong:
public class Counter {
private static int count = 0; // Shared by ALL objects!
public void increment() {
count++; // All counters share same count!
}
}
โ Right (if you want separate counts):
public class Counter {
private int count = 0; // Each object has its own
public void increment() {
count++;
}
}
6. Violating the "Is-A" Relationship
โ Wrong - inheritance misuse:
// A car is NOT a wheel! Don't use inheritance here.
public class Car extends Wheel {
// Wrong relationship
}
โ Right - use composition:
public class Car {
private Wheel[] wheels = new Wheel[4]; // Car HAS wheels
private Engine engine; // Car HAS an engine
}
๐ง Memory device: HASI = Has-A use composition, Is-A use inheritance
๐ง Try This: Mini-Exercises
Exercise 1: Create a Rectangle class with private width and height fields. Add methods to calculate area and perimeter. Create a Square subclass that ensures width equals height.
Exercise 2: Design an interface Drawable with a draw() method. Create classes Circle, Triangle, and Line that implement this interface. Write a method that accepts an array of Drawable objects and calls draw() on each.
Exercise 3: Create an abstract class Employee with fields for name and salary. Create subclasses Manager and Developer with different bonus calculation methods. Use polymorphism to calculate total payroll.
Key Takeaways ๐ฏ
- Classes are blueprints, objects are instances created from those blueprints
- Encapsulation protects data integrity using private fields and public methods
- Inheritance enables code reuse through parent-child relationships (use
extends) - Polymorphism allows treating objects of different types uniformly through a common interface
- Abstraction hides complexity using abstract classes and interfaces
- Use access modifiers strategically: private for fields, public for interfaces
- Constructor chaining with
super()initializes parent class properly - Method overriding changes behavior; method overloading provides multiple versions
- Static members belong to the class, not instances
- Prefer composition over inheritance when modeling "has-a" relationships
๐ Quick Reference Card
| Concept | Keyword | Purpose |
|---|---|---|
| Class Definition | class | Blueprint for objects |
| Object Creation | new | Instantiate a class |
| Inheritance | extends | Child inherits from parent |
| Interface Implementation | implements | Class fulfills interface contract |
| Method Override | @Override | Replace parent method |
| Parent Reference | super | Access parent class members |
| Current Object | this | Reference to current instance |
| Abstract Class | abstract class | Cannot instantiate, can have abstract methods |
| Interface | interface | Pure contract (all methods abstract) |
| Class Member | static | Belongs to class, not instances |
| Constant | final | Cannot be changed after initialization |
| Private Access | private | Only within same class |
| Protected Access | protected | Same package + subclasses |
| Public Access | public | Accessible from anywhere |
๐ Further Study
- Oracle Java Tutorials - Object-Oriented Programming Concepts: https://docs.oracle.com/javase/tutorial/java/concepts/
- GeeksforGeeks - Object Oriented Programming in Java: https://www.geeksforgeeks.org/object-oriented-programming-oops-concept-in-java/
- Baeldung - OOP Concepts in Java: https://www.baeldung.com/java-oop
Master these OOP fundamentals, and you'll have a solid foundation for building sophisticated Java applications! Remember: good OOP design makes code more maintainable, testable, and scalable. ๐