Core Java · Lesson 6
Object-Oriented Programming
The Four Pillars of Object-Oriented Programming
Object-Oriented Programming (OOP) is a design philosophy that organizes code around objects and how they interact. The four core principles are Encapsulation (hiding internal details), Inheritance (sharing behavior through class hierarchies), Polymorphism (objects of different types responding to the same request), and Abstraction (showing only essential features). Mastering these principles makes your code reusable, maintainable, and flexible.
Encapsulation: Bundling Data and Methods
Encapsulation hides internal implementation details and exposes only what's necessary. Use private for attributes and internal methods, and public for the interface others interact with. This protects data from invalid states — for example, a BankAccount can prevent negative balances by hiding the balance field and controlling access through validated deposit/withdraw methods.
public class BankAccount {
private String accountNumber;
private double balance; // private: only this class can access
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// public: others can call this
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
// private: only this class can call
private void logTransaction(String type, double amount) {
System.out.println("[LOG] " + type + ": $" + amount);
}
}
Inheritance: Reusing Code Through Class Hierarchies
Inheritance lets one class (subclass) inherit attributes and methods from another (superclass), promoting code reuse. Use the extends keyword to create a subclass. The subclass inherits everything from the superclass and can add its own unique attributes and methods. This prevents duplicating common code across related classes.
// Superclass
public class Animal {
protected String name; // protected: subclasses can access
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println(name + " makes a sound");
}
public void eat() {
System.out.println(name + " is eating");
}
}
// Subclass 1
public class Dog extends Animal {
public Dog(String name) {
super(name); // call parent constructor
}
@Override // override parent method
public void makeSound() {
System.out.println(name + " barks: Woof!");
}
}
// Subclass 2
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " meows: Meow!");
}
}
class InheritanceDemo {
public static void main(String[] args) {
Animal dog = new Dog("Buddy");
Animal cat = new Cat("Whiskers");
dog.makeSound(); // Buddy barks: Woof!
cat.makeSound(); // Whiskers meows: Meow!
dog.eat(); // Buddy is eating (inherited from Animal)
cat.eat(); // Whiskers is eating
}
}
Polymorphism: One Interface, Many Forms
Polymorphism means "many forms" — the same method call behaves differently depending on the object type. The makeSound() method exists in the parent class, but each subclass (Dog, Cat) provides its own implementation. This lets you write flexible code: call makeSound() on any Animal without knowing its specific type.
public class PolymorphismDemo {
public static void main(String[] args) {
// Array of Animal references (can hold any Animal subclass)
Animal[] animals = {
new Dog("Rex"),
new Cat("Mittens"),
new Dog("Poodle"),
new Cat("Fluffy")
};
// Same method call, different behavior for each animal
for (Animal animal : animals) {
animal.makeSound(); // Calls Dog.makeSound() or Cat.makeSound()
}
// Polymorphism in action: one method, multiple outcomes
System.out.println("\nAll animals eating:");
for (Animal animal : animals) {
animal.eat(); // All use inherited eat() method
}
}
}
Abstraction: Focusing on What, Not How
Abstraction simplifies complex systems by hiding unnecessary details and showing only essential features. In Java, you achieve this with abstract classes and interfaces. An abstract class uses the abstract keyword to define methods that subclasses must implement, establishing a contract. You cannot instantiate an abstract class directly; you must create concrete subclasses.
// Abstract class: defines what subclasses must do
public abstract class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
// abstract method: subclasses must provide implementation
public abstract void start();
public abstract void stop();
// concrete method: same for all vehicles
public void honk() {
System.out.println(brand + " vehicle goes: Beep!");
}
}
public class Car extends Vehicle {
public Car(String brand) {
super(brand);
}
@Override
public void start() {
System.out.println(brand + " car engine started");
}
@Override
public void stop() {
System.out.println(brand + " car engine stopped");
}
}
public class Motorcycle extends Vehicle {
public Motorcycle(String brand) {
super(brand);
}
@Override
public void start() {
System.out.println(brand + " motorcycle ignition on");
}
@Override
public void stop() {
System.out.println(brand + " motorcycle ignition off");
}
}
class AbstractionDemo {
public static void main(String[] args) {
// Vehicle vehicle = new Vehicle(); // ERROR: can't instantiate abstract class
Vehicle car = new Car("Toyota");
Vehicle bike = new Motorcycle("Harley");
car.start(); // Toyota car engine started
bike.start(); // Harley motorcycle ignition on
car.honk(); // Toyota vehicle goes: Beep! (inherited)
}
}
| OOP Principle | Purpose | Example |
|---|---|---|
| Encapsulation | Hide internal details, protect data | Private attributes with public getters/setters |
| Inheritance | Share code across related classes | Dog extends Animal |
| Polymorphism | Same method, different behavior | makeSound() in Dog vs Cat |
| Abstraction | Show only essential features, hide complexity | Abstract class Vehicle with required methods |
Common Mistake: Using inheritance when composition would be better. Inheritance should represent an "is-a" relationship (Dog IS-A Animal). Don't inherit just to reuse code; if it's a "has-a" relationship (Car HAS-A Engine), use composition (a field) instead. Over-using inheritance creates rigid, hard-to-maintain hierarchies.
Best Practices for Object-Oriented Design
- Apply SOLID principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
- Prefer composition over inheritance when it makes sense; it's more flexible
- Use abstract classes for shared behavior, interfaces for contracts
- Design for interfaces, not implementations: reduces coupling between classes
- Keep inheritance hierarchies shallow: deep hierarchies are harder to understand
- Override
toString(),equals(), andhashCode()in your classes for better usability - Use
@Overrideannotation to catch mistakes if a parent method signature changes
Real-World Example: E-Commerce Product Catalog
An online store models products with an abstract Product class defining getPrice(), applyDiscount(), and getDescription(). Concrete subclasses like PhysicalProduct, DigitalProduct, and Subscription override these methods differently (physical items have shipping, digital items don't). A shopping cart accepts any Product through polymorphism. When calculating totals, the cart calls getPrice() on each item without caring about its specific type. This design is extensible: adding a new product type is as simple as creating a new subclass, without changing the cart logic. The architecture demonstrates all four OOP principles working together to create flexible, maintainable code.
Quiz Check