Object-Oriented Programming Fundamentals
A comprehensive guide to OOP concepts essential for software engineering interviews and development.
Table of Contents
- Introduction to OOP
- Classes and Objects
- The Four Pillars of OOP
- Advanced Concepts
- SOLID Principles
- Design Patterns Overview
- Common Interview Questions
Introduction to OOP
Object-Oriented Programming is a programming paradigm based on the concept of “objects” which contain data (attributes) and code (methods).
Why OOP?
âś… Modularity - Code is organized into self-contained objects
âś… Reusability - Inheritance allows code reuse
âś… Flexibility - Polymorphism enables flexible code
âś… Maintainability - Encapsulation makes code easier to maintain
âś… Scalability - Good structure for large applications
OOP vs Procedural Programming
| Aspect | OOP | Procedural |
|---|---|---|
| Focus | Objects and data | Functions and logic |
| Data Security | High (encapsulation) | Low (global data) |
| Reusability | High (inheritance) | Moderate (functions) |
| Complexity | Better for large projects | Better for small projects |
| Examples | Java, C++, Python | C, Pascal |
Classes and Objects
Class
A class is a blueprint or template for creating objects. It defines attributes (data) and methods (behavior).
// Class definition
public class Car {
// Attributes (instance variables)
private String brand;
private String model;
private int year;
// Constructor
public Car(String brand, String model, int year) {
this.brand = brand;
this.model = model;
this.year = year;
}
// Methods (behavior)
public void start() {
System.out.println(brand + " " + model + " is starting...");
}
public String getInfo() {
return year + " " + brand + " " + model;
}
}
Object
An object is an instance of a class. It has actual values for attributes defined in the class.
// Creating objects
Car car1 = new Car("Toyota", "Camry", 2024);
Car car2 = new Car("Honda", "Accord", 2023);
// Using objects
car1.start(); // Output: Toyota Camry is starting...
System.out.println(car2.getInfo()); // Output: 2023 Honda Accord
Key Concepts
Constructor:
- Special method called when object is created
- Same name as class
- No return type
- Can be overloaded
public class Student {
private String name;
private int age;
// Default constructor
public Student() {
this.name = "Unknown";
this.age = 0;
}
// Parameterized constructor
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
this keyword:
- Refers to current object
- Differentiates instance variables from parameters
- Can call other constructors
public class Person {
private String name;
public Person(String name) {
this.name = name; // this.name refers to instance variable
}
public void printName() {
System.out.println(this.name);
}
}
The Four Pillars of OOP
1. Encapsulation
Encapsulation is the bundling of data and methods that operate on that data within a single unit (class), and restricting direct access to some components.
Key Principles:
- Make instance variables private
- Provide public getter and setter methods
- Hide implementation details
public class BankAccount {
// Private data - cannot be accessed directly
private String accountNumber;
private double balance;
// Constructor
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// Public getter
public double getBalance() {
return balance;
}
// Public setter with validation
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public boolean withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
}
Benefits:
âś… Data Protection - Invalid data cannot be set directly
âś… Flexibility - Internal implementation can change without affecting users
âś… Testability - Easier to test and debug
âś… Reusability - Well-encapsulated classes are easier to reuse
Access Modifiers:
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
public |
âś… | âś… | âś… | âś… |
protected |
✅ | ✅ | ✅ | ❌ |
default |
✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
2. Inheritance
Inheritance allows a class to inherit properties and methods from another class, promoting code reuse.
Terminology:
- Parent Class (Base/Super class) - Class being inherited from
- Child Class (Derived/Sub class) - Class that inherits
// Parent class
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
}
// Child class
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // Call parent constructor
this.breed = breed;
}
// Additional method
public void bark() {
System.out.println(name + " is barking!");
}
// Override parent method
@Override
public void eat() {
System.out.println(name + " the dog is eating dog food");
}
}
// Usage
Dog myDog = new Dog("Buddy", "Golden Retriever");
myDog.eat(); // Child's overridden method
myDog.sleep(); // Inherited from parent
myDog.bark(); // Dog's own method
Types of Inheritance:
1. Single Inheritance:
class A { }
class B extends A { }
2. Multilevel Inheritance:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
3. Hierarchical Inheritance:
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
Note: Java doesn’t support multiple inheritance with classes (to avoid diamond problem), but supports it through interfaces.
super keyword:
public class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
public void display() {
System.out.println("Brand: " + brand);
}
}
public class Car extends Vehicle {
private int doors;
public Car(String brand, int doors) {
super(brand); // Call parent constructor
this.doors = doors;
}
@Override
public void display() {
super.display(); // Call parent method
System.out.println("Doors: " + doors);
}
}
3. Polymorphism
Polymorphism means “many forms” - ability of an object to take multiple forms. Same method name behaves differently in different contexts.
Types:
A. Compile-Time Polymorphism (Method Overloading)
Same method name, different parameters in the same class.
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;
}
public String add(String a, String b) {
return a + b;
}
}
// Usage
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3)); // Output: 8
System.out.println(calc.add(5.5, 3.2)); // Output: 8.7
System.out.println(calc.add(1, 2, 3)); // Output: 6
System.out.println(calc.add("Hello", "World")); // Output: HelloWorld
Rules for Method Overloading:
- Must have different parameter lists
- Can have different return types
- Can have different access modifiers
- Can throw different exceptions
B. Runtime Polymorphism (Method Overriding)
Child class provides specific implementation of parent class method.
public class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
public double area() {
return 0.0;
}
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing a circle");
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
@Override
public double area() {
return width * height;
}
}
// Runtime polymorphism in action
Shape shape1 = new Circle(5);
Shape shape2 = new Rectangle(4, 6);
shape1.draw(); // Output: Drawing a circle
shape2.draw(); // Output: Drawing a rectangle
System.out.println(shape1.area()); // Output: 78.54
System.out.println(shape2.area()); // Output: 24.0
Rules for Method Overriding:
- Must have same method signature (name + parameters)
- Must have same or covariant return type
- Cannot have more restrictive access modifier
- Cannot throw broader checked exceptions
- Use
@Overrideannotation (recommended)
4. Abstraction
Abstraction means hiding implementation details and showing only essential features.
Abstract Classes
public abstract class Employee {
protected String name;
protected double baseSalary;
public Employee(String name, double baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}
// Abstract method (no implementation)
public abstract double calculateSalary();
// Concrete method
public void displayInfo() {
System.out.println("Employee: " + name);
System.out.println("Salary: $" + calculateSalary());
}
}
public class FullTimeEmployee extends Employee {
private double bonus;
public FullTimeEmployee(String name, double baseSalary, double bonus) {
super(name, baseSalary);
this.bonus = bonus;
}
@Override
public double calculateSalary() {
return baseSalary + bonus;
}
}
public class ContractEmployee extends Employee {
private int hoursWorked;
private double hourlyRate;
public ContractEmployee(String name, int hours, double rate) {
super(name, 0);
this.hoursWorked = hours;
this.hourlyRate = rate;
}
@Override
public double calculateSalary() {
return hoursWorked * hourlyRate;
}
}
Interfaces
An interface is a contract that defines what a class must do, but not how.
public interface Drawable {
// All methods are public and abstract by default
void draw();
void resize(double factor);
}
public interface Colorable {
void setColor(String color);
String getColor();
}
// A class can implement multiple interfaces
public class ColoredCircle implements Drawable, Colorable {
private double radius;
private String color;
public ColoredCircle(double radius, String color) {
this.radius = radius;
this.color = color;
}
@Override
public void draw() {
System.out.println("Drawing a " + color + " circle");
}
@Override
public void resize(double factor) {
radius *= factor;
}
@Override
public void setColor(String color) {
this.color = color;
}
@Override
public String getColor() {
return color;
}
}
Abstract Class vs Interface
| Feature | Abstract Class | Interface |
|---|---|---|
| Methods | Can have abstract and concrete methods | Only abstract (Java 8+ allows default) |
| Variables | Can have any type of variables | Only public static final constants |
| Multiple Inheritance | ❌ Single inheritance only | ✅ Can implement multiple interfaces |
| Constructor | ✅ Can have constructors | ❌ Cannot have constructors |
| Access Modifiers | Can have any access modifier | Methods are public by default |
| Usage | When classes share common code | When classes share common behavior contract |
When to use:
- Abstract Class: When subclasses share common code and state (IS-A relationship)
- Interface: When unrelated classes implement same behavior (CAN-DO capability)
Advanced Concepts
Static Members
Static Variable:
- Shared by all instances
- Belongs to class, not objects
- Initialized once
public class Counter {
private static int count = 0; // Shared by all objects
private int id;
public Counter() {
count++;
this.id = count;
}
public static int getCount() {
return count;
}
public int getId() {
return id;
}
}
// Usage
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
System.out.println(Counter.getCount()); // Output: 3
Static Method:
- Can be called without creating object
- Can only access static members
- Cannot use
thisorsuper
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
public static double calculateCircleArea(double radius) {
return Math.PI * radius * radius;
}
}
// Usage - no object needed
int sum = MathUtils.add(5, 3);
double area = MathUtils.calculateCircleArea(5.0);
Final Keyword
Final Variable (Constant):
public class Constants {
public static final double PI = 3.14159;
public static final int MAX_USERS = 100;
}
Final Method (Cannot be overridden):
public class Parent {
public final void importantMethod() {
// This method cannot be overridden
}
}
Final Class (Cannot be inherited):
public final class ImmutableClass {
// This class cannot be extended
}
Method Overriding Rules
class Parent {
protected Object getData() {
return new Object();
}
}
class Child extends Parent {
// Covariant return type (more specific)
@Override
public String getData() {
return "Data";
}
}
Constructor Chaining
public class Student {
private String name;
private int age;
private String course;
// Constructor 1
public Student() {
this("Unknown", 0, "Not enrolled");
}
// Constructor 2
public Student(String name) {
this(name, 0, "Not enrolled");
}
// Constructor 3
public Student(String name, int age) {
this(name, age, "Not enrolled");
}
// Constructor 4 (master constructor)
public Student(String name, int age, String course) {
this.name = name;
this.age = age;
this.course = course;
}
}
SOLID Principles
Design principles for writing maintainable and scalable OOP code.
S - Single Responsibility Principle
A class should have only one reason to change.
// ❌ Bad - Multiple responsibilities
class User {
public void saveToDatabase() { }
public void sendEmail() { }
public void generateReport() { }
}
// âś… Good - Single responsibility
class User {
private String name;
private String email;
}
class UserRepository {
public void save(User user) { }
}
class EmailService {
public void sendEmail(User user) { }
}
class ReportGenerator {
public void generateReport(User user) { }
}
O - Open/Closed Principle
Classes should be open for extension but closed for modification.
// âś… Good - Using abstraction
interface PaymentProcessor {
void processPayment(double amount);
}
class CreditCardProcessor implements PaymentProcessor {
public void processPayment(double amount) {
// Credit card logic
}
}
class PayPalProcessor implements PaymentProcessor {
public void processPayment(double amount) {
// PayPal logic
}
}
// New payment method - just add new class, don't modify existing
class CryptoProcessor implements PaymentProcessor {
public void processPayment(double amount) {
// Crypto logic
}
}
L - Liskov Substitution Principle
Subtypes must be substitutable for their base types.
// âś… Good
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// Square properly extends Rectangle
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
I - Interface Segregation Principle
Clients should not be forced to depend on interfaces they don’t use.
// ❌ Bad - Fat interface
interface Worker {
void work();
void eat();
void sleep();
}
// âś… Good - Segregated interfaces
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class HumanWorker implements Workable, Eatable, Sleepable {
public void work() { }
public void eat() { }
public void sleep() { }
}
class RobotWorker implements Workable {
public void work() { }
// No need to implement eat() and sleep()
}
D - Dependency Inversion Principle
Depend on abstractions, not concretions.
// âś… Good - Depend on abstraction
interface MessageService {
void sendMessage(String message);
}
class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Email: " + message);
}
}
class SMSService implements MessageService {
public void sendMessage(String message) {
System.out.println("SMS: " + message);
}
}
class NotificationManager {
private MessageService messageService;
// Depends on abstraction, not concrete class
public NotificationManager(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
// Usage
NotificationManager emailNotifier = new NotificationManager(new EmailService());
NotificationManager smsNotifier = new NotificationManager(new SMSService());
🎯 Design Patterns: The Complete Guide
“Design patterns are solutions to recurring problems in software design.” — Gang of Four
1. Creational Patterns (Object Creation)
Singleton
Ensures a class has only one instance and provides a global point of access. Analogy: A country can have only one President. When to use:
- Logging (One log file).
- Driver Objects (One printer spooler).
- Caching (One cache instance).
public class Database {
private static Database instance;
private Database() { /* private constructor */ }
public static synchronized Database getInstance() {
if (instance == null) {
instance = new Database();
}
return instance;
}
public void query(String sql) {
System.out.println("Executing: " + sql);
}
}
Factory Method
Creates objects without specifying the exact class to create. Analogy: Hiring a logistics company. You say “Deliver this”, you don’t care if they use a Truck, Ship, or Plane. When to use:
- When you don’t know ahead of time what class of objects you need.
- When you want to decouple object creation from usage.
interface Notification {
void notifyUser();
}
class SMSNotification implements Notification {
public void notifyUser() { System.out.println("Sending SMS..."); }
}
class EmailNotification implements Notification {
public void notifyUser() { System.out.println("Sending Email..."); }
}
class NotificationFactory {
public Notification createNotification(String channel) {
if (channel == null || channel.isEmpty()) return null;
switch (channel) {
case "SMS": return new SMSNotification();
case "EMAIL": return new EmailNotification();
default: throw new IllegalArgumentException("Unknown channel " + channel);
}
}
}
Builder
Separates the construction of a complex object from its representation. Analogy: Sub-way sandwich. You choose Bread -> Cheese -> Veggies -> Sauce stepwise. When to use:
- When an object has too many parameters (Constructor Explosion).
- When some parameters are optional.
class User {
private final String firstName; // required
private final String lastName; // required
private final int age; // optional
private final String phone; // optional
private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
}
public static class UserBuilder {
private final String firstName;
private final String lastName;
private int age;
private String phone;
public UserBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public UserBuilder age(int age) { this.age = age; return this; }
public UserBuilder phone(String phone) { this.phone = phone; return this; }
public User build() { return new User(this); }
}
}
// Usage:
// User user = new User.UserBuilder("John", "Doe").age(30).build();
2. Structural Patterns (Class Composition)
Adapter
Allows incompatible interfaces to work together. Analogy: A universal travel power plug adapter. When to use:
- Integrating a legacy component with a new system.
- Using a 3rd party library that doesn’t match your interface.
// Target interface
interface Bird {
void makeSound();
}
class Sparrow implements Bird {
public void makeSound() { System.out.println("Chirp Chirp"); }
}
// Adaptee interface
interface ToyDuck {
void squeak();
}
class PlasticToyDuck implements ToyDuck {
public void squeak() { System.out.println("Squeak"); }
}
// Adapter
class BirdAdapter implements ToyDuck {
Bird bird;
public BirdAdapter(Bird bird) { this.bird = bird; }
public void squeak() {
bird.makeSound(); // Translates squeak() to makeSound()
}
}
Decorator
Adds behavior to an object dynamically without affecting other objects. Analogy: Wearing layers of clothes. You are still YOU, but now you have a jacket (Warmth) and a raincoat (Dry). When to use:
- Adding responsibilities to objects dynamically (e.g., UI Borders, Scrollbars).
- Java I/O Streams (
new BufferedReader(new FileReader(...))).
interface Coffee {
String getDescription();
double getCost();
}
class SimpleCoffee implements Coffee {
public String getDescription() { return "Simple Coffee"; }
public double getCost() { return 1.0; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee c) { this.decoratedCoffee = c; }
public String getDescription() { return decoratedCoffee.getDescription(); }
public double getCost() { return decoratedCoffee.getCost(); }
}
class Milk extends CoffeeDecorator {
public Milk(Coffee c) { super(c); }
public String getDescription() { return decoratedCoffee.getDescription() + ", Milk"; }
public double getCost() { return decoratedCoffee.getCost() + 0.5; }
}
// Usage: Coffee c = new Milk(new SimpleCoffee());
Facade
Provides a simplified interface to a library, a framework, or any other complex set of classes. Analogy: Starting a car with a key (or button) without knowing about fuel injection, pistons, or battery. When to use:
- To provide a simple interface to a complex subsystem.
- To decouple clients from implementation details.
class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void start() {
cpu.freeze();
memory.load(BOOT_ADDRESS, hardDrive.read(BOOT_SECTOR, SECTOR_SIZE));
cpu.jump(BOOT_ADDRESS);
cpu.execute();
}
}
3. Behavioral Patterns (Object Communication)
Observer
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified. Analogy: YouTube Subscription. When a creator uploads, all subscribers get a notification. You don’t have to refresh checking for new videos. When to use:
- Event handling systems (DOM Events).
- Pub-Sub systems (Messaging).
interface Observer {
void update(String message);
}
class Subscriber implements Observer {
private String name;
public Subscriber(String name) { this.name = name; }
public void update(String message) {
System.out.println(name + " received: " + message);
}
}
class Channel {
private List<Observer> observers = new ArrayList<>();
public void subscribe(Observer o) { observers.add(o); }
public void unsubscribe(Observer o) { observers.remove(o); }
public void notifyOrbs(String message) {
for (Observer o : observers) o.update(message);
}
}
Strategy
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Use Case: Payment processing (CreditCard, PayPal, Bitcoin), Sorting algorithms.
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardStrategy implements PaymentStrategy {
public void pay(int amount) { System.out.println("Paid " + amount + " with Card"); }
}
class PayPalStrategy implements PaymentStrategy {
public void pay(int amount) { System.out.println("Paid " + amount + " with PayPal"); }
}
class ShoppingCart {
public void checkout(int amount, PaymentStrategy strategy) {
strategy.pay(amount);
}
}
// Usage: cart.checkout(100, new PayPalStrategy());
🚀 Modern OOP & Concurrency
1. Lambda Expressions (Functional OOP)
Introduced in Java 8 / C++11, Lambdas allow you to treat functionality as a method argument, or code as data. They simplify the Strategy Pattern and Event Handling.
Syntax: (parameters) -> { body }
Why use them?
- Conciseness: Reduces boilerplate code (no need for anonymous inner classes).
- Readability: Focuses on what to do, not how to construct the object.
- Parallelism: Enables easy parallel processing with the Stream API.
Example: Sorting without Lambdas vs With Lambdas
// Old Way (Anonymous Class)
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
// New Way (Lambda)
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
Stream API (The Power of Lambdas) Process collections declaratively (Functional Style).
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// Filter names starting with 'A', convert to UpperCase, and print
names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
2. Multithreading Basics (The Foundation)
Thread vs Process
- Process: An executing program (e.g., Discord.exe). Has its own memory space. Heavyweight.
- Thread: A unit of execution within a process. Shares memory (Heap) with other threads. Lightweight.
Thread Lifecycle
- New: Created but not started.
- Runnable: Ready to run, waiting for CPU time.
- Running: Currently executing.
- Blocked/Waiting: Waiting for a resource (I/O, Lock) or another thread.
- Terminated: Finished execution.
Creating a Thread
// Method 1: Implement Runnable (Preferred)
Runnable task = () -> {
System.out.println("Running in: " + Thread.currentThread().getName());
};
Thread t1 = new Thread(task);
t1.start();
3. Concurrency Deep Dive (The Hard Stuff)
Concurrent programming is about dealing with multiple things happening at once. It’s notorious for bugs that are hard to reproduce.
1. The Core Problem: Shared Mutable State
When two threads read/write the same variable at the same time, Race Conditions occur.
Example:
class Counter {
int count = 0;
void increment() { count++; } // Not Atomic! (Read -> Modify -> Write)
}
// If T1 and T2 run increment() same time, count might be 1 instead of 2.
2. Synchronization (The Lock)
The synchronized keyword ensures only one thread can execute a block of code at a time. It uses an Intrinsic Lock (Monitor).
Method Level:
public synchronized void increment() {
count++;
}
Block Level (Better Performance):
public void increment() {
// Only lock this specific object, not the whole method
synchronized(this) {
count++;
}
}
3. Volatile Keyword (Visibility)
Threads often cache variables in CPU registers. If T1 changes a flag, T2 might not see it immediately.
volatile guarantees that the variable is read from Main Memory, not CPU cache.
private volatile boolean running = true;
public void stop() { running = false; } // Immediate visibility to other threads
Note: Volatile does NOT guarantee atomicity (don’t use it for counters).
4. Inter-Thread Communication (Wait/Notify)
Classic Producer-Consumer pattern. How do threads talk?
wait(): “I’ll go to sleep and release the lock until someone wakes me.”notify(): “Wake up one sleeping thread.”notifyAll(): “Wake up everyone.”
class SharedBuffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity = 5;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // Wait if full
}
queue.add(item);
System.out.println("Produced: " + item);
notifyAll(); // Wake up consumer
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // Wait if empty
}
int item = queue.remove();
System.out.println("Consumed: " + item);
notifyAll(); // Wake up producer
return item;
}
}
5. Advanced Locks (ReentrantLock)
More flexible than synchronized.
- TryLock: Attempt to get lock, but give up if busy (avoids waiting forever).
- Fairness: Can ensure longest-waiting thread gets lock first.
Lock lock = new ReentrantLock();
void accessResource() {
lock.lock();
try {
// Critical Section
} finally {
lock.unlock(); // Always unlock in finally!
}
}
6. Atomic Variables
Lock-free thread-safe variables. Faster than synchronization for simple counters.
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // Atomic increment
7. Deadlock
Situation where T1 holds Key A and waits for Key B, while T2 holds Key B and waits for Key A. They wait forever. Prevention:
- Acquire locks in a consistent order.
- Use
tryLock()with timeout. - Minimize lock scope.
4. Exception Handling Mastery
Robust software must handle errors gracefully.
1. The Hierarchy
Everything inherits from Throwable.
Error: Serious system problems (e.g.,StackOverflowError,OutOfMemoryError). Application cannot catch or recover from these.Exception: Problems the application can recover from.- Checked Exceptions: Compile-time check. Must be handled (
try-catch) or declared (throws). Example:IOException. - Unchecked Exceptions (
RuntimeException): Runtime check. Usually programming errors. Example:NullPointerException,IndexOutOfBoundsException.
- Checked Exceptions: Compile-time check. Must be handled (
2. Keywords Deep Dive
try: Block of code that might throw an exception.catch: Block that handles the exception.finally: Block that always executes (cleanup code like closing files).throw: Used to explicitly throw an exception instance.throws: Used in method signature to declare that method might throw an exception.
// throw vs throws
public void readFile(String path) throws IOException { // DECLARE
if (path == null) {
throw new IllegalArgumentException("Path cannot be null"); // THROW
}
// ... read file logic
}
3. Custom Exceptions
Create domain-specific exceptions to make error handling meaningful.
Unchecked (extends RuntimeException) - Preferred for logic errors:
class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message);
}
}
void registerUser(int age) {
if (age < 18) {
throw new InvalidAgeException("User must be 18+");
}
}
Checked (extends Exception) - Preferred for recoverable system errors:
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Balance too low");
}
}
4. Try-With-Resources (Java 7+)
Automatically closes resources that implement AutoCloseable. No need for finally block!
// Old Way
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("test.txt"));
// use br
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) try { br.close(); } catch (IOException e) { }
}
// New Way (Best Practice)
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
// use br
} catch (IOException e) {
e.printStackTrace();
} // br is automatically closed here
5. Best Practices
- Fail Fast: Validate inputs early (start of method).
- Catch Specific: Catch
FileNotFoundExceptionbeforeIOException. - Don’t Swallow: Never leave a
catchblock empty! At least log it. - Don’t Catch
Throwable: It catchesErrortoo (bad idea).
âť“ Common Interview Questions
Theory Questions
1. What is OOP?
- Programming paradigm based on objects containing data and methods
- Key principles: Encapsulation, Inheritance, Polymorphism, Abstraction
2. Difference between class and object?
- Class: Blueprint/template
- Object: Instance of class with actual values
3. What is encapsulation?
- Bundling data and methods in a class
- Hiding internal details using access modifiers
- Providing public interface through getters/setters
4. What is inheritance?
- Mechanism where one class acquires properties of another
- Promotes code reuse
- Forms IS-A relationship
5. What is polymorphism?
- Ability to take multiple forms
- Types: Compile-time (overloading) and Runtime (overriding)
6. Abstract class vs Interface?
- Abstract class: Can have both abstract and concrete methods, single inheritance
- Interface: Only abstract methods (default in Java 8+), multiple inheritance
7. What is method overloading vs overriding?
- Overloading: Same name, different parameters, same class, compile-time
- Overriding: Same signature, different classes (parent-child), runtime
8. Explain SOLID principles
- S: Single Responsibility
- O: Open/Closed
- L: Liskov Substitution
- I: Interface Segregation
- D: Dependency Inversion
Coding Questions
1. Design a class hierarchy for a library system
abstract class LibraryItem {
protected String title;
protected String id;
protected boolean isAvailable;
public abstract double calculateLateFee(int daysLate);
public abstract Display();
}
class Book extends LibraryItem {
private String author;
private String ISBN;
@Override
public double calculateLateFee(int daysLate) {
return daysLate * 0.50;
}
}
class Magazine extends LibraryItem {
private String publisher;
private int issueNumber;
@Override
public double calculateLateFee(int daysLate) {
return daysLate * 0.25;
}
}
2. Implement a Shape calculator with polymorphism
interface Shape {
double calculateArea();
double calculatePerimeter();
}
class Circle implements Shape {
private double radius;
public double calculateArea() {
return Math.PI * radius * radius;
}
public double calculatePerimeter() {
return 2 * Math.PI * radius;
}
}
class Triangle implements Shape {
private double a, b, c;
public double calculateArea() {
double s = (a + b + c) / 2;
return Math.sqrt(s * (s-a) * (s-b) * (s-c));
}
public double calculatePerimeter() {
return a + b + c;
}
}
class ShapeCalculator {
public void printShapeInfo(Shape shape) {
System.out.println("Area: " + shape.calculateArea());
System.out.println("Perimeter: " + shape.calculatePerimeter());
}
}
🎓 Key Takeaways
âś… Master the 4 pillars - Encapsulation, Inheritance, Polymorphism, Abstraction
âś… Understand access modifiers - When to use public, private, protected
âś… Know when to use abstract classes vs interfaces
âś… Practice SOLID principles in your code
✅ Understand polymorphism deeply - It’s the most asked concept
âś… Write clean, maintainable code following OOP best practices
📚 Study Tips
- Code along - Don’t just read, implement examples
- Draw diagrams - UML class diagrams help visualize relationships
- Explain concepts - Teach someone else to solidify understanding
- Practice design questions - Draw class hierarchies for real-world systems
- Review design patterns - Understand common solutions to common problems
Keep practicing! OOP mastery comes with hands-on experience. 🚀