Lesson 04: OOP Deep Dive
Go deeper into OOP pillars: encapsulation, inheritance, polymorphism, and interfaces. Build safer, reusable, and flexible designs.
Introduction
Now that you know the basics of classes and objects, let's explore the four core principles that make object-oriented programming powerful. Encapsulation keeps your data safe by controlling how others can access it. Inheritance lets you reuse code by building new classes on top of existing ones. Polymorphism allows different objects to respond to the same command in their own way. Interfaces create contracts that multiple classes can follow. These concepts help you write code that is safer, easier to maintain, and more flexible. Think of them as tools that help you organize your code like a well-structured building.
Encapsulation
Definition
Encapsulation means hiding the internal details of an object and only allowing controlled access through specific methods. You make fields private and create public methods (getters and setters) to read or modify them safely. This protects your data from being changed in ways that could break your program.
Analogy
Think of your bank account. You can't directly reach into the bank's computer and change your balance. Instead, you use an ATM or app that provides safe ways to deposit, withdraw, or check your balance. The bank protects your account details and only allows you to interact through secure, controlled methods. If there's not enough money, the withdrawal is denied. This prevents impossible situations like having a negative balance when that's not allowed.
Example
// BankAccount class with encapsulation
class BankAccount { // class definition
private double balance; // private field - hidden from outside
private String accountNumber; // private field - protected data
// Constructor to set up account
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber; // set account number
this.balance = initialBalance; // set starting balance
}
// Getter method - safe way to read balance
public double getBalance() { // public method to access private data
return balance; // return current balance
}
// Safe way to add money
public void deposit(double amount) { // public method to modify balance
if (amount > 0) { // validate input - no negative deposits
balance += amount; // safely update balance
System.out.println("Deposited: $" + amount);
} else {
System.out.println("Invalid deposit amount");
}
}
// Safe way to remove money
public boolean withdraw(double amount) { // returns success/failure
if (amount > 0 && amount <= balance) { // check if withdrawal is valid
balance -= amount; // safely reduce balance
System.out.println("Withdrew: $" + amount);
return true; // indicate success
} else {
System.out.println("Invalid withdrawal");
return false; // indicate failure
}
}
}
Inheritance
Definition
Inheritance allows you to create a new class based on an existing class. The new class (child) automatically gets all the fields and methods from the parent class, and can add its own features or modify existing ones. This helps you reuse code and create specialized versions of general concepts.
Analogy
Inheritance is like a child inheriting traits from their parents. A child might inherit their parent's eye color, height, and certain abilities, but they also develop their own unique skills and personality traits. In the same way, a child class inherits everything from its parent class but can add new features or change how certain things work. Just like how all family members share some common characteristics but each person is still unique.
Example
// Base class representing any vehicle
class Vehicle { // parent class
protected String brand; // field accessible to children
protected int maxSpeed; // field accessible to children
public Vehicle(String brand, int maxSpeed) { // constructor
this.brand = brand; // set brand
this.maxSpeed = maxSpeed; // set maximum speed
}
public void start() { // method inherited by children
System.out.println(brand + " is starting...");
}
public void move() { // method inherited by children
System.out.println("Moving at speed up to " + maxSpeed + " mph");
}
}
// Specialized class that inherits from Vehicle
class Car extends Vehicle { // child class inheriting from Vehicle
private int doors; // additional field specific to cars
public Car(String brand, int maxSpeed, int doors) { // constructor
super(brand, maxSpeed); // call parent constructor
this.doors = doors; // set car-specific field
}
public void honk() { // new method only cars have
System.out.println("Beep beep!"); // car-specific behavior
}
// Override parent method with car-specific version
@Override
public void move() { // customize inherited method
System.out.println("Driving on roads at " + maxSpeed + " mph");
}
}
// Another specialized class
class Motorcycle extends Vehicle { // another child class
public Motorcycle(String brand, int maxSpeed) {
super(brand, maxSpeed); // call parent constructor
}
public void wheelie() { // motorcycle-specific method
System.out.println("Doing a wheelie!");
}
@Override
public void move() { // customize inherited method
System.out.println("Riding on two wheels at " + maxSpeed + " mph");
}
}
Polymorphism
Definition
Polymorphism allows objects of different classes to be treated as objects of a common parent class, while each object behaves according to its own specific type. When you call a method on a parent reference, the actual method that runs depends on what type of object it really is. This lets you write flexible code that works with many different types.
Analogy
Polymorphism is like one person playing multiple roles. The same person might be a student at school, a child at home, and an employee at work. Depending on the situation, they behave differently, but they're still the same person. When someone says "student, please raise your hand," all students respond, but each in their own way - some are eager, others reluctant. The command is the same, but each person's response reflects their individual personality and circumstances.
Example
// Using the Vehicle hierarchy from inheritance example
class VehicleDemo { // demonstration class
// Method that works with any Vehicle type
public static void testVehicle(Vehicle v) { // accepts any Vehicle or its children
v.start(); // calls appropriate start method
v.move(); // calls the specific move() version
System.out.println("---");
}
public static void main(String[] args) {
// Create different types of vehicles
Vehicle car = new Car("Toyota", 120, 4); // Car object in Vehicle reference
Vehicle motorcycle = new Motorcycle("Honda", 180); // Motorcycle in Vehicle reference
Vehicle genericVehicle = new Vehicle("Generic", 50); // Regular Vehicle object
// All calls to testVehicle work the same way
testVehicle(car); // calls Car's move() method
testVehicle(motorcycle); // calls Motorcycle's move() method
testVehicle(genericVehicle); // calls Vehicle's move() method
// Array of different vehicle types
Vehicle[] fleet = { // array holding different types
new Car("BMW", 150, 2),
new Motorcycle("Yamaha", 200),
new Car("Ford", 110, 4)
};
// Process all vehicles the same way
for (Vehicle vehicle : fleet) { // iterate through mixed types
vehicle.start(); // correct start() method called
vehicle.move(); // correct move() method called
}
}
}
Interfaces
Definition
An interface is a contract that defines what methods a class must have, without specifying how those methods work. Any class that implements an interface must provide its own version of all the interface's methods. Interfaces allow different classes to be used interchangeably as long as they follow the same contract.
Analogy
Think of a driver's license test. Everyone who wants to drive must demonstrate they can start the car, steer, brake, and park. The test doesn't care what kind of car you use or exactly how you perform these actions - it just ensures you can do the required tasks. Once you pass, you can drive any car because all cars follow the same basic "driving interface." Different car manufacturers implement steering and braking differently, but they all respond to the same basic commands from the driver.
Example
// Interface defining what payment methods must do
interface PaymentProcessor { // contract definition
boolean processPayment(double amount); // required method signature
String getPaymentType(); // required method signature
}
// Credit card implementation
class CreditCardProcessor implements PaymentProcessor { // implements the contract
private String cardNumber; // specific to credit cards
public CreditCardProcessor(String cardNumber) {
this.cardNumber = cardNumber; // set card details
}
@Override
public boolean processPayment(double amount) { // implement required method
System.out.println("Processing $" + amount + " via credit card");
System.out.println("Card: " + cardNumber);
// Credit card specific logic here
return true; // simulate successful payment
}
@Override
public String getPaymentType() { // implement required method
return "Credit Card"; // return specific type
}
}
// PayPal implementation
class PayPalProcessor implements PaymentProcessor { // another implementation
private String email; // specific to PayPal
public PayPalProcessor(String email) {
this.email = email; // set PayPal account
}
@Override
public boolean processPayment(double amount) { // implement required method
System.out.println("Processing $" + amount + " via PayPal");
System.out.println("Account: " + email);
// PayPal specific logic here
return true; // simulate successful payment
}
@Override
public String getPaymentType() { // implement required method
return "PayPal"; // return specific type
}
}
// Code that uses the interface
class OnlineStore { // store class
// Method works with any payment processor
public void checkout(PaymentProcessor processor, double amount) {
System.out.println("Processing order...");
System.out.println("Payment method: " + processor.getPaymentType());
if (processor.processPayment(amount)) { // call interface method
System.out.println("Payment successful!");
} else {
System.out.println("Payment failed!");
}
}
public static void main(String[] args) {
OnlineStore store = new OnlineStore();
// Different payment methods, same interface
PaymentProcessor creditCard = new CreditCardProcessor("1234-5678-9012-3456");
PaymentProcessor paypal = new PayPalProcessor("user@email.com");
// Same method works with different implementations
store.checkout(creditCard, 99.99); // uses credit card processing
store.checkout(paypal, 149.99); // uses PayPal processing
}
}
Summary
These four OOP principles work together to create better programs. Encapsulation protects your data and keeps objects in valid states. Inheritance lets you build specialized classes without starting from scratch. Polymorphism allows your code to work with different types through a common interface. Interfaces define contracts that make your code flexible and testable. Together, these concepts help you write programs that are easier to understand, maintain, and extend. In the next lesson, we'll explore collections and generics, which will help you work with groups of objects efficiently.