OOP is a paradigm based on 'objects' containing data and methods. Unlike Procedural Programming, which focuses on functions and top-down logic, OOP emphasizes data security (encapsulation) and reusability.
A Class is a blueprint or template that defines variables and methods. An Object is a runtime instance of a class containing actual values.
Encapsulation is wrapping data (variables) and code (methods) into a single unit. Data Hiding restricts direct access to internal data, usually achieved via private access modifiers.
Abstraction hides complex implementation details and shows only the essential features. In Java, it is implemented using abstract classes and interfaces.
Inheritance is a mechanism where a new class inherits properties and behaviors of an existing class. Its primary benefit is code reusability.
Polymorphism allows entities to take multiple forms. Compile-time (method overloading) is resolved during compilation, while Run-time (method overriding) is resolved dynamically at execution.
A Constructor is a special method used to initialize objects. Characteristics: it has the same name as the class, has no return type, and is called automatically during object creation.
Constructor overloading is defining multiple constructors in a class with different parameter lists (different number or types of arguments) to initialize objects in various ways.
The 'this' keyword is a reference variable in Java that refers to the current object. It is used to resolve naming conflicts between instance variables and parameters.
The 'super' keyword refers to the immediate parent class object. It is used to call parent class constructors, methods, and access hidden parent fields.
Overloading occurs in the same class with same method name but different parameters (compile-time). Overriding occurs in a subclass with the exact same signature as the parent method (run-time).
Java doesn't support multiple inheritance with classes to avoid the Diamond Problem (ambiguity when multiple parent classes have a method with the same signature).
A class can implement multiple interfaces simultaneously because interfaces only contain abstract methods (prior to Java 8), so there is no ambiguity in method implementation.
An Abstract Class can have both abstract and concrete methods with state, while an Interface historically has only abstract methods and static final constants, and a class can implement multiple interfaces.
A static variable belongs to the class rather than instances, shared among all objects. A static method can be called without creating an object and can only directly access static data.
Garbage Collection automatically frees memory by destroying unreachable objects. System.gc() requests a garbage collection run, and finalize() is a method called just before an object is destroyed for cleanup.
A Package is a namespace that organizes a set of related classes and interfaces. It is created using the package keyword at the top of the file and imported using the import keyword.
Exception Handling manages runtime errors to maintain normal flow. Checked exceptions are verified at compile-time (e.g., IOException), while Unchecked exceptions occur at runtime (e.g., NullPointerException).
A user-defined exception is a custom exception class created by a developer (usually by extending the Exception or RuntimeException class) to handle application-specific errors.
Multithreading is the concurrent execution of multiple threads to maximize CPU utilization. A Thread is the smallest lightweight unit of processing or execution within a program.
Process-based involves heavyweight independent programs with separate memory spaces. Thread-based involves lightweight concurrent paths of execution that share the same memory space within one process.
A thread can be created either by extending the Thread class and overriding run(), or by implementing the Runnable interface and passing it to a Thread constructor.
Thread priorities dictate the order in which threads are scheduled. In Java, priorities range from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY), with 5 being the default (NORM_PRIORITY).
Thread Synchronization restricts multiple threads from accessing shared resources concurrently to prevent data inconsistency. The synchronized keyword locks the object or class during execution.
Inter-Thread Communication allows synchronized threads to communicate. wait() pauses a thread, while notify() and notifyAll() wake up one or all waiting threads on that monitor.
Deadlock is a situation where two or more threads are blocked forever, each waiting for a lock held by the other, bringing the program to a halt.
The String class represents a sequence of characters. Strings are immutable (unchangeable) for security, synchronization, caching, and to optimize the String Pool memory management.
String is immutable. StringBuilder is mutable and not thread-safe (faster). StringBuffer is mutable and thread-safe (synchronized, slower).
A Wrapper Class encapsulates a primitive type into an object. Autoboxing is automatic conversion of primitive to wrapper object; Unboxing is the reverse conversion.
The JCF is a unified architecture providing ready-to-use classes and interfaces (like List, Set, Map) for storing, manipulating, and searching groups of objects.
ArrayList uses a dynamic array (better for searching/random access). LinkedList uses a doubly linked list (better for frequent insertions/deletions).
HashMap is a Map implementation that stores key-value pairs using a hash table. It uses a hashing algorithm on keys to compute indices for bucketing values.
Generics allow types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. Their main advantage is compile-time type safety and avoiding explicit casting.
File I/O handles reading and writing to files. Byte Streams (e.g., FileInputStream) read/write 8-bit bytes (for binary data). Character Streams (e.g., FileReader) read/write 16-bit Unicode characters.
Serialization converts an object's state into a byte stream for saving or networking. Deserialization reconstructs the object from the byte stream.
The transient keyword marks a variable so that it is ignored during serialization; its value will not be saved or transmitted.
A Lambda Expression provides a concise syntax to write anonymous methods, typically used to implement the single abstract method of a functional interface.
A Functional Interface has exactly one abstract method. Examples include Runnable and Callable (or Predicate and Function from java.util.function).
The Stream API provides a functional and declarative approach to process collections of objects (filtering, mapping, reducing) allowing for easy parallel execution.
Design Patterns are proven solutions to common software design problems. Creational deals with object creation, Structural with object composition, and Behavioral with object communication.
The Singleton pattern ensures that a class has only one single instance throughout the application lifecycle and provides a global point of access to it.
The Factory Method is a creational pattern that defines an interface for creating objects, but lets subclasses decide which class to instantiate.
SOLID stands for Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—five principles for maintainable object-oriented design.
UML is a standard visual modeling language used in software engineering to document, specify, construct, and visualize the artifacts of a software system.
Association is a general relationship. Aggregation is a "has-a" relationship (weak ownership). Composition is a strict "part-of" relationship (strong ownership, part dies with whole).
A Class Diagram is a structural UML diagram that shows the system's static structure by detailing classes, their attributes, methods, and relationships.
A Virtual Function allows derived classes to override a method for runtime polymorphism. A Pure Virtual Function (assigned = 0) forces derived classes to implement it, making the base class abstract.
A Friend Function/Class in C++ is granted access to the private and protected members of the class in which it is declared as a friend.
Object-Oriented Programming is a paradigm based on the concept of 'objects', which can contain data (attributes/fields) and code (methods). The four fundamental pillars that define OOP are:
balance is private and only updated via deposit() or withdraw() methods.Vehicle class might have properties like wheels and methods like startEngine(). A Car class inherits from Vehicle, automatically gaining those features, but can add its own specific features like airConditioning().draw() method will behave differently for a Circle object vs a Square object.Programming paradigms dictate how we approach problem-solving in code. Procedural Oriented Programming (POP) and Object-Oriented Programming (OOP) are the two most prominent paradigms.
| Feature | Procedural Programming (POP) | Object-Oriented Programming (OOP) |
|---|---|---|
| Core Concept | Focuses on functions and procedures. The program is divided into smaller parts called functions. | Focuses on data and objects. The program is divided into objects that interact with each other. |
| Approach | Follows a Top-Down approach in program design. | Follows a Bottom-Up approach in program design. |
| Data Security | Poor security. Data moves freely around the system from function to function. Most data is global. | High security. Data is hidden (encapsulated) and cannot be accessed by external functions without permission. |
| Real-world modeling | Difficult to map to real-world scenarios. | Extremely easy to map real-world entities into software objects. |
| Reusability | No direct mechanism for reusing code, though functions help to some extent. | Inheritance provides a powerful mechanism to reuse existing code. |
| Modifiability | Adding new data or functions is difficult as it might affect the entire program. | Highly extensible. Adding new classes or modifying existing ones is easier without breaking the system. |
| Examples | C, Pascal, FORTRAN, BASIC. | Java, C++, Python, C#. |
Conclusion: While POP is suitable for small, simple scripts where execution speed is critical, OOP is indispensable for building large, complex, maintainable, and scalable enterprise applications.
Constructor chaining is the process of calling one constructor from another constructor within the same class (or from a child class to a parent class) during the object creation process. This technique is primarily used to prevent code duplication and to ensure that multiple constructors share common initialization logic.
this() keyword. It must always be the first statement inside the constructor.super() keyword. It also must be the first statement. If not explicitly written, the Java compiler implicitly inserts a default super() call.class Employee {
String name;
int id;
String department;
// Constructor 1 (Takes only name)
public Employee(String name) {
// Calls Constructor 2
this(name, 0);
System.out.println("Inside Constructor 1");
}
// Constructor 2 (Takes name and id)
public Employee(String name, int id) {
// Calls Constructor 3 (The main initialization constructor)
this(name, id, "Unassigned");
System.out.println("Inside Constructor 2");
}
// Constructor 3 (Takes all parameters)
public Employee(String name, int id, String department) {
// This constructor actually sets the values
this.name = name;
this.id = id;
this.department = department;
System.out.println("Inside Constructor 3");
}
}
public class Main {
public static void main(String[] args) {
// When we call the single-parameter constructor, it triggers the chain.
Employee emp = new Employee("Alice");
}
}
Output of the above code:
Inside Constructor 3
Inside Constructor 2
Inside Constructor 1
As seen in the output, the this() call immediately pauses the execution of the current constructor and jumps to the target constructor, meaning the most parameterized constructor finishes executing first.
Dynamic Method Dispatch is a mechanism by which a call to an overridden method is resolved at runtime, rather than at compile-time. This is how Java implements runtime polymorphism. When an overridden method is called through a superclass reference, Java determines which version of that method to execute based on the actual type of the object being referred to at the time the call occurs, not the type of the reference variable.
class Animal {
public void sound() {
System.out.println("Animal makes a generic sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks: Woof Woof!");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("Cat meows: Meow!");
}
}
public class RuntimePolymorphismDemo {
public static void main(String[] args) {
// Superclass reference points to Animal object
Animal myAnimal = new Animal();
myAnimal.sound(); // Output: Animal makes a generic sound
// Superclass reference points to Dog object (Upcasting)
Animal myDog = new Dog();
// At runtime, JVM checks the actual object type (Dog)
myDog.sound(); // Output: Dog barks: Woof Woof!
// Superclass reference points to Cat object (Upcasting)
Animal myCat = new Cat();
myCat.sound(); // Output: Cat meows: Meow!
}
}
Advantages: It allows Java to support overriding, which is central to OOP. It enables you to write robust, extensible code because you can write methods that accept a superclass parameter and they will automatically behave correctly for any subclass passed to them.
Both abstract classes and interfaces are used to achieve abstraction in Java, but they have distinct differences in their design and usage.
| Feature | Abstract Class | Interface |
|---|---|---|
| Keyword & Definition | Declared using the abstract keyword. Can have both abstract (no body) and concrete (with body) methods. | Declared using the interface keyword. (Before Java 8) Could only have abstract methods. Now can have default and static methods. |
| Variables | Can have final, non-final, static, and non-static variables. | Variables are implicitly public static final (constants). |
| Inheritance | A class can extend only ONE abstract class (Single Inheritance). | A class can implement MULTIPLE interfaces (Multiple Inheritance). |
| Constructors | Can have a constructor (used during subclass object creation). | Cannot have a constructor. |
| Access Modifiers | Methods and variables can have any access modifier (private, protected, etc.). | Methods are implicitly public abstract. |
// Interface
interface Drawable {
void draw(); // implicitly public and abstract
}
// Abstract Class
abstract class Shape {
String color;
// Constructor in abstract class
public Shape(String color) { this.color = color; }
// Concrete method
public void displayColor() { System.out.println("Color: " + color); }
// Abstract method
abstract double calculateArea();
}
// Concrete Class implementing Interface AND extending Abstract Class
class Circle extends Shape implements Drawable {
double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public void draw() { System.out.println("Drawing a Circle."); }
@Override
double calculateArea() { return Math.PI * radius * radius; }
}
Access specifiers determine the scope or visibility of classes, variables, methods, and constructors in Java. They are crucial for implementing Encapsulation (data hiding). Java provides four access specifiers:
package com.example.pack1;
public class Parent {
private int privateVar = 1; // Visible ONLY inside Parent
int defaultVar = 2; // Visible ONLY inside pack1
protected int protectedVar = 3; // Visible inside pack1 AND subclasses everywhere
public int publicVar = 4; // Visible everywhere
public void show() {
System.out.println(privateVar); // OK
}
}
// Different package scenario:
package com.example.pack2;
import com.example.pack1.Parent;
class Child extends Parent {
public void testAccess() {
// System.out.println(privateVar); // Error: private
// System.out.println(defaultVar); // Error: different package
System.out.println(protectedVar); // OK: accessed via inheritance
System.out.println(publicVar); // OK: public
}
}
The static keyword in Java is used primarily for memory management. It indicates that a particular member belongs to the class itself, rather than to instances (objects) of the class. It can be applied to variables, methods, blocks, and nested classes.
ClassName.methodName()). A crucial rule is that static methods can only directly access other static data and static methods; they cannot access non-static (instance) data directly or use this or super keywords.main() method executes.class Student {
int rollNo; // Instance variable
String name; // Instance variable
static String college = "MIT"; // Static variable shared by all
// Static Block
static {
System.out.println("Static Block Executed");
// college = "Stanford"; // Can modify static variables here
}
public Student(int r, String n) {
rollNo = r;
name = n;
}
// Static Method
public static void changeCollege(String newCollege) {
college = newCollege;
// name = "John"; // ERROR: Cannot make a static reference to non-static field
}
public void display() {
System.out.println(rollNo + " " + name + " " + college);
}
}
public class Main {
public static void main(String[] args) {
Student.changeCollege("Harvard"); // Calling static method without object
Student s1 = new Student(101, "Alice");
Student s2 = new Student(102, "Bob");
s1.display(); // 101 Alice Harvard
s2.display(); // 102 Bob Harvard
}
}
The final keyword in Java is a non-access modifier used to restrict the user. It can be applied in three different contexts: variables, methods, and classes. Once applied, it signifies that the entity is complete and cannot be altered.
String, Integer, and Math are declared as final.// 1. Final Class
final class Vehicle {
public void drive() { System.out.println("Driving a vehicle"); }
}
// class Car extends Vehicle { } // ERROR: Cannot inherit from final Vehicle
class Parent {
// 2. Final Method
public final void show() {
System.out.println("This is a final method");
}
}
class Child extends Parent {
// public void show() { } // ERROR: Cannot override final method
public void testVariable() {
// 3. Final Variable
final int MAX_AGE = 100;
// MAX_AGE = 101; // ERROR: Cannot assign a value to final variable
System.out.println("Max age is: " + MAX_AGE);
}
}
An exception is an unwanted or unexpected event occurring during the execution of a program (at runtime) that disrupts the normal flow of instructions. Java provides a robust mechanism to handle these exceptions so that the program can terminate gracefully or recover.
try block encloses the code that might throw an exception. It must be followed by either a catch or a finally block.catch block is used to handle the exception thrown by the preceding try block. You can have multiple catch blocks to handle different types of exceptions specifically.finally block contains crucial code that must execute whether an exception occurs or not, and whether it is caught or not. It is primarily used for cleaning up resources (closing files, database connections, network sockets).import java.util.Scanner;
public class ExceptionDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
System.out.print("Enter a number to divide 100 by: ");
int divisor = scanner.nextInt();
// Risky code that might throw an ArithmeticException
int result = 100 / divisor;
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
// Handled if divisor is 0
System.out.println("Error: Cannot divide by zero!");
} catch (Exception e) {
// Generic catch-all for any other exception (e.g., InputMismatchException)
System.out.println("An unexpected error occurred: " + e.getMessage());
} finally {
// This block ALWAYS executes
System.out.println("Executing finally block: Closing resources.");
scanner.close();
}
System.out.println("Program continues normally after try-catch-finally.");
}
}
In Java, the java.lang.Throwable class is the root class of all exceptions and errors. The exception hierarchy is divided into two primary categories: Checked and Unchecked exceptions.
| Feature | Checked Exceptions | Unchecked Exceptions |
|---|---|---|
| Verification Time | Checked at Compile-time by the compiler. | Occur at Runtime. The compiler does not check them. |
| Handling Requirement | Must be explicitly handled using a try-catch block or declared in the method signature using the throws keyword. Otherwise, the code will not compile. | No mandatory requirement to handle or declare them, though it is good programming practice to do so if they are predictable. |
| Hierarchy | Classes that extend Throwable or Exception (except RuntimeException and its subclasses). | Classes that extend RuntimeException (and Error, though errors are typically unrecoverable system failures). |
| Typical Use Case | Represents conditions outside the immediate control of the program (e.g., a missing file, a broken network connection). The programmer is forced to plan for these. | Represents programming logic errors (e.g., dividing by zero, accessing a null reference, going out of array bounds). These should be fixed by writing better code. |
| Examples | IOException, SQLException, ClassNotFoundException, InterruptedException. | NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException, IllegalArgumentException. |
Custom Exception:
Thread Life Cycle: New -> Runnable -> Running -> Blocked/Waiting -> Terminated.
Thread vs Runnable: Implementing Runnable is better as Java doesn't support multiple inheritance, so you can extend another class if needed.
Thread Synchronization: Prevents thread interference using synchronized keyword on methods or blocks.
Inter-Thread Communication: Threads communicate via wait(), notify(), and notifyAll() inside synchronized context.
Deadlock: Two or more threads are blocked forever, waiting for each other. Avoid by acquiring locks in a consistent order.
String Immutability: Strings cannot be changed once created for security, thread-safety, and performance (String Pool).
Autoboxing and Unboxing: Automatic conversion between primitive types and their corresponding wrapper classes.
ArrayList vs Vector: ArrayList is unsynchronized and fast. Vector is synchronized and slow.
HashMap Working: Uses hashing. Stores key-value pairs in buckets. Handles collisions using Linked Lists or Trees.
Java Generics and Wildcards: Generics provide compile-time type safety. Wildcards (?) allow flexibility in generic types.
Byte vs Character Streams: Byte streams (InputStream/OutputStream) read/write 1 byte at a time. Character streams (Reader/Writer) read/write 2 bytes (Unicode).
Serialization: Process of converting an object into a byte stream. Deserialization is the reverse.
Lambda Expressions: Anonymous functions in Java 8 that simplify code by treating functionality as a method argument.
Functional Interfaces: Interfaces with exactly one abstract method. Annotated with @FunctionalInterface.
Stream API:
Singleton Pattern: Ensures only one instance exists. Lazy initializes on first use; Eager initializes at class loading.
Factory Pattern: Creates objects without exposing the instantiation logic to the client.
SOLID Principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
UML Relationships: Association (uses-a), Aggregation (has-a, weak), Composition (part-of, strong).
Use Case Diagram: Visual representation of system interactions by actors and use cases.
Friend Functions: Functions in C++ that can access private/protected members of a class.
Operator Overloading: Giving special meaning to an existing operator in C++ for user-defined types.
Deep vs Shallow Copy: Shallow copy copies references. Deep copy creates new copies of the referenced objects.
Diamond Problem: Ambiguity in multiple inheritance. Solved in C++ using virtual inheritance.
Generic Stack:
Reflection API: Allows inspection and modification of classes, interfaces, fields, and methods at runtime.
Try-with-Resources: Automatically closes resources like files or sockets at the end of the statement.
Word Frequency:
MVC Architecture: Separates application into Model (data), View (UI), and Controller (logic).
Introduction
Object-Oriented Programming (OOP) provides a robust framework for building complex, real-world applications like a Banking System. By modeling the system using OOP principles, we ensure code reusability, security, and maintainability.
private variables and provide controlled access via public getter and setter methods. This prevents unauthorized direct modification.Account class acts as the superclass. Specialized accounts like SavingsAccount and CurrentAccount inherit from it, promoting code reuse (e.g., both share the deposit logic).withdraw() method behaves differently for a Savings Account (cannot withdraw below minimum balance) vs a Current Account (allows overdraft). We achieve this via Method Overriding.InsufficientFundsException.FileWriter and BufferedWriter.import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
// 1. Custom Exception (Exception Handling)
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
// 2. Abstract Base Class (Abstraction & Encapsulation)
abstract class Account {
private String accountNumber; // Encapsulated
protected double balance; // Accessible to child classes
public Account(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
public String getAccountNumber() { return accountNumber; }
public double getBalance() { return balance; }
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
logTransaction("Deposited: $" + amount + " | New Balance: $" + balance);
}
}
// Polymorphic method to be overridden
public abstract void withdraw(double amount) throws InsufficientFundsException;
// 3. File I/O for persistent logging
protected void logTransaction(String message) {
try (FileWriter fw = new FileWriter(accountNumber + "_log.txt", true);
PrintWriter pw = new PrintWriter(fw)) {
pw.println(message);
} catch (IOException e) {
System.err.println("Failed to log transaction for " + accountNumber);
}
}
}
// 4. Inheritance & Polymorphism (Savings Account)
class SavingsAccount extends Account {
private double minimumBalance = 500.0;
public SavingsAccount(String accNo, double initialBalance) {
super(accNo, initialBalance);
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (balance - amount < minimumBalance) {
throw new InsufficientFundsException("Transaction Failed: Minimum balance of $500 must be maintained.");
}
balance -= amount;
logTransaction("Withdrew: $" + amount + " | New Balance: $" + balance);
}
}
// 5. Inheritance & Polymorphism (Current Account)
class CurrentAccount extends Account {
private double overdraftLimit = 1000.0;
public CurrentAccount(String accNo, double initialBalance) {
super(accNo, initialBalance);
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (balance - amount < -overdraftLimit) {
throw new InsufficientFundsException("Transaction Failed: Overdraft limit of $" + overdraftLimit + " exceeded.");
}
balance -= amount;
logTransaction("Withdrew: $" + amount + " | New Balance: $" + balance);
}
}
// Execution Class
public class BankingSystem {
public static void main(String[] args) {
try {
Account mySavings = new SavingsAccount("SAV-101", 2000);
mySavings.deposit(500);
mySavings.withdraw(300); // Success
System.out.println("Savings Balance: $" + mySavings.getBalance());
mySavings.withdraw(2000); // Throws Exception
} catch (InsufficientFundsException e) {
System.err.println(e.getMessage());
}
}
}
This implementation proves the power of OOP. If the bank decides to add a 'LoanAccount' tomorrow, we simply extend the Account class without modifying the existing, tested code (adhering to the Open/Closed Principle).
Introduction
Polymorphism in Java is divided into Compile-time (Method Overloading) and Run-time (Method Overriding). Dynamic Method Dispatch (DMD) is the core mechanism by which Java implements run-time polymorphism. It allows Java to determine which overridden method to call at runtime rather than at compile time.
In Java, a superclass reference variable can point to a subclass object. When an overridden method is called through this superclass reference, the Java Virtual Machine (JVM) looks at the actual object type created in memory at runtime, NOT the reference type, to decide which method implementation to execute.
DMD is the foundation of Runtime Pluggability (or the Strategy Pattern). It allows a system to swap out behaviors dynamically without recompiling the core logic. A common example is a Payment Gateway in an e-commerce application. The cart doesn't need to know how a payment is processed (Credit Card vs UPI), it just needs to know that the payment strategy can be processed.
// 1. The Strategy Interface (Super type)
interface PaymentStrategy {
void processPayment(double amount);
}
// 2. Concrete Implementation A (Sub type)
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String card) { this.cardNumber = card; }
@Override
public void processPayment(double amount) {
System.out.println("Processing $" + amount + " via Credit Card: " + cardNumber);
// Complex credit card API logic here
}
}
// 3. Concrete Implementation B (Sub type)
class UPIPayment implements PaymentStrategy {
private String upiId;
public UPIPayment(String upi) { this.upiId = upi; }
@Override
public void processPayment(double amount) {
System.out.println("Processing $" + amount + " via UPI: " + upiId);
// Complex UPI API logic here
}
}
// 4. The Core Application
class ShoppingCart {
// Pluggable dependency - Notice we use the interface reference!
private PaymentStrategy paymentMethod;
// Injecting the strategy at runtime
public void setPaymentMethod(PaymentStrategy strategy) {
this.paymentMethod = strategy;
}
public void checkout(double totalAmount) {
if (paymentMethod == null) throw new IllegalStateException("Select a payment method.");
// Dynamic Method Dispatch happens here!
// The compiler only knows paymentMethod is of type PaymentStrategy.
// The JVM dynamically calls the CreditCard or UPI processPayment at runtime.
paymentMethod.processPayment(totalAmount);
}
}
// Execution
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// User selects UPI at runtime
cart.setPaymentMethod(new UPIPayment("user@okhdfcbank"));
cart.checkout(150.50); // Outputs: Processing $150.5 via UPI
// User switches to Credit Card at runtime
cart.setPaymentMethod(new CreditCardPayment("4111-2222-3333-4444"));
cart.checkout(5000.00); // Outputs: Processing $5000.0 via Credit Card
}
}
Without Dynamic Method Dispatch, the ShoppingCart class would require complex, hard-coded if-else or switch statements to handle different payment types. DMD allows the code to be incredibly clean, modular, and adhering strictly to the Open/Closed Principle (open for extension by adding new payment classes, closed for modification of the cart class).
Introduction
The Java Collections Framework (JCF) is a unified architecture representing and manipulating collections of objects (like a dynamic array, a set, or a queue). It provides ready-to-use, highly optimized, and standardized interfaces and classes, drastically reducing programming effort and increasing performance.
The framework revolves around two main root interfaces:
List, Set, and Queue.Understanding the internal working of these classes is crucial for writing performant Java applications.
| Feature | ArrayList (List) | LinkedList (List/Deque) | HashSet (Set) | TreeSet (Set) |
|---|---|---|---|---|
| Internal Data Structure | Dynamic, resizable Array. | Doubly Linked List. | Hash Table (backed by a HashMap). | Self-Balancing Binary Search Tree (Red-Black Tree). |
| Ordering | Maintains Insertion Order. | Maintains Insertion Order. | Unordered (No guarantee of order). | Sorted Order (Natural sorting or via Comparator). |
| Duplicates | Allows duplicates. | Allows duplicates. | No duplicates. | No duplicates. |
| Time Complexity (Search/Get) | O(1) - Instant access via index. | O(N) - Must traverse nodes. | O(1) average case via hashing. | O(log N) via binary search. |
| Time Complexity (Insert/Delete) | O(N) - Requires shifting elements if not at the end. | O(1) - If the node reference is known (just changing pointers). | O(1) average case. | O(log N) to maintain tree balance. |
| Best Use Case | Frequent read operations, random access. | Frequent insertions and deletions in the middle of the list. | Fastest way to ensure uniqueness and fast lookups. | When you need a unique collection that is always sorted. |
HashMap where the element you insert is the 'Key' and a dummy object is the 'Value'. It uses the hashCode() and equals() methods to resolve hash collisions (using buckets with linked lists or balanced trees).TreeMap. It relies on the Comparable interface (or a provided Comparator) to organize the nodes in a Red-Black tree, ensuring operations remain O(log N) even in worst-case scenarios.Choosing the right collection is the hallmark of a good Java developer. If you need fast access, use ArrayList. If you need fast sorting, use TreeSet. If you need lightning-fast unique storage, use HashSet. The JCF provides a perfectly tuned tool for every scenario.
Introduction
The Producer-Consumer problem is a classic synchronization problem in concurrent programming. It involves two threads, a Producer and a Consumer, sharing a common, fixed-size buffer. The Producer's job is to generate data and put it into the buffer. The Consumer's job is to consume the data from the buffer. The challenge is to ensure the Producer doesn't try to add data to a full buffer, and the Consumer doesn't try to remove data from an empty buffer.
We use Java's built-in monitor locks (synchronized blocks) along with the wait() and notify() methods inherited from the Object class to achieve Inter-Thread Communication.
import java.util.LinkedList;
// The Shared Buffer Class
class SharedBuffer {
private LinkedList<Integer> list = new LinkedList<>();
private int capacity = 2; // Bounded buffer size
// Called by Producer thread
public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (this) {
// Wait if buffer is full
while (list.size() == capacity) {
System.out.println("Buffer is full. Producer is waiting...");
wait(); // Releases lock and waits
}
System.out.println("Producer produced: " + value);
list.add(value++); // Add data to buffer
// Notify the consumer that data is available
notify();
// Sleep to simulate time taken to produce
Thread.sleep(1000);
}
}
}
// Called by Consumer thread
public void consume() throws InterruptedException {
while (true) {
synchronized (this) {
// Wait if buffer is empty
while (list.size() == 0) {
System.out.println("Buffer is empty. Consumer is waiting...");
wait(); // Releases lock and waits
}
// Consume data
int val = list.removeFirst();
System.out.println("Consumer consumed: " + val);
// Notify the producer that space is available
notify();
// Sleep to simulate time taken to consume
Thread.sleep(1000);
}
}
}
}
// Main Execution Class
public class ProducerConsumerDemo {
public static void main(String[] args) throws InterruptedException {
final SharedBuffer buffer = new SharedBuffer();
// Create Producer Thread
Thread producerThread = new Thread(new Runnable() {
@Override
public void run() {
try { buffer.produce(); }
catch (InterruptedException e) { e.printStackTrace(); }
}
});
// Create Consumer Thread
Thread consumerThread = new Thread(new Runnable() {
@Override
public void run() {
try { buffer.consume(); }
catch (InterruptedException e) { e.printStackTrace(); }
}
});
// Start both threads
producerThread.start();
consumerThread.start();
}
}
while loop is crucial here, not an if statement. When a thread wakes up from wait(), it must re-check the condition (spurious wakeups or another thread grabbing the lock first).notify() works perfectly. If there were multiple producers/consumers, notifyAll() would be necessary to prevent deadlocks.Introduction
Java's Exception Handling architecture is designed to manage runtime errors gracefully, ensuring the normal flow of the application is maintained. The root of the exception hierarchy is the Throwable class, which splits into Error (serious JVM problems, usually unrecoverable like OutOfMemoryError) and Exception (recoverable conditions).
IOException). The compiler forces you to handle them (try/catch or throws).RuntimeException (e.g., NullPointerException). Not checked at compile-time; usually indicate programming logic errors.try (encloses risky code), catch (handles specific exceptions), finally (guaranteed execution for cleanup), throw (explicitly throws an exception), throws (declares exceptions a method might throw).The following code demonstrates a robust architecture combining Custom Exceptions, Nested Try-Catch, Multiple Catch blocks, and the Finally block.
import java.util.Scanner;
// 1. Creating a Custom Checked Exception
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
public class RobustExceptionHandling {
// Method declaring it might throw a custom checked exception
public static void validateVoterEligibility(int age) throws InvalidAgeException {
if (age < 18) {
throw new InvalidAgeException("Applicant is under 18. Voting strictly prohibited.");
}
System.out.println("Age verified. Applicant is eligible to vote.");
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Outer Try-Catch
try {
System.out.print("Enter total registered voters (for calculation): ");
String inputVoters = scanner.nextLine();
// Multiple Catch scenario - parsing integer
int totalVoters = Integer.parseInt(inputVoters);
System.out.print("Enter polling booths available: ");
int booths = scanner.nextInt();
// Nested Try-Catch for specific logic isolation
try {
int votersPerBooth = totalVoters / booths;
System.out.println("Voters per booth: " + votersPerBooth);
} catch (ArithmeticException ae) {
System.err.println("Inner Catch: Cannot divide by zero booths! " + ae.getMessage());
}
System.out.print("Enter voter age for registration: ");
int age = scanner.nextInt();
// Calling a method that throws a checked exception
validateVoterEligibility(age);
}
// Handling Multiple Exceptions (Ordering matters: Most specific to least specific)
catch (NumberFormatException nfe) {
System.err.println("Outer Catch: Invalid number format. Please enter valid integers.");
}
catch (InvalidAgeException iae) {
System.err.println("Outer Catch: Validation Error -> " + iae.getMessage());
}
catch (Exception e) {
// Generic fallback for any unforeseen exceptions
System.err.println("Outer Catch: An unexpected error occurred -> " + e.toString());
}
finally {
// Cleanup block always executes
System.out.println("Finally Block: Releasing scanner resources and closing connection.");
scanner.close();
}
System.out.println("Program execution completed smoothly.");
}
}
InvalidAgeException) rather than generic exceptions.NumberFormatException and InvalidAgeException are caught before the generic Exception. If Exception e was first, compilation would fail (unreachable code).finally block ensures the Scanner is closed regardless of whether the program crashes or succeeds.ArithmeticException occurs, the inner catch handles it, and the program continues to the next line (asking for voter age), demonstrating true recovery.Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...
Answer to be generated...