Group C — Long / numerical Questions (15 marks)
Q1. Design and implement a Banking System in Java demonstrating Encapsulation, Inheritance, Polymorphism, Exception Handling, and File I/O.
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.
1. Core OOP Concepts Applied
- Encapsulation (Data Hiding): The account balance and account number must be kept secure. We declare them as
privatevariables and provide controlled access viapublicgetter and setter methods. This prevents unauthorized direct modification. - Inheritance (Code Reusability): A generic
Accountclass acts as the superclass. Specialized accounts likeSavingsAccountandCurrentAccountinherit from it, promoting code reuse (e.g., both share the deposit logic). - Polymorphism (Dynamic Dispatch): The
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. - Exception Handling (Robustness): When a user tries to withdraw more than they have, instead of crashing, we throw a custom checked exception
InsufficientFundsException. - File I/O (Persistence): To maintain an audit trail, every transaction is logged to a text file using
FileWriterandBufferedWriter.
2. System Architecture (UML Class Diagram)
3. Complete Java Implementation
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());
}
}
}
Conclusion
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).
Q2. Critically analyze Dynamic Method Dispatch in Java. Show how run-time polymorphism enables runtime pluggability with code examples.
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.
1. The Mechanism of Dynamic Method Dispatch
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.
- Compile Time: The compiler checks if the method exists in the superclass (the reference type). If it does, compilation succeeds.
- Run Time: The JVM invokes the method belonging to the actual object type assigned to the reference.
2. Why is DMD Important? (Runtime Pluggability)
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.
3. Code Example: E-Commerce Payment Gateway
// 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
}
}
Conclusion
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).
Q3. Explain the Java Collections Framework (JCF). Differentiate between ArrayList, LinkedList, HashSet, and TreeSet with internal working details and time complexities.
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.
1. Architecture of JCF
The framework revolves around two main root interfaces:
- Collection Interface: The root of the collection hierarchy. Extended by
List,Set, andQueue. - Map Interface: Represents key-value pairs. Does not extend the Collection interface but is part of the JCF.
2. Detailed Comparison of Key Data Structures
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. |
3. Internal Working Details
- ArrayList: It starts with an initial capacity (default 10). When it gets full, it creates a new array (usually 1.5x the size), copies old elements over, and discards the old array. This makes adding elements at the end O(1) amortized, but inserting in the middle is O(N) due to array shifting.
- LinkedList: Consists of 'Nodes'. Each node holds data, a pointer to the previous node, and a pointer to the next node. There is no contiguous memory allocation. Memory overhead is higher per element compared to ArrayList.
- HashSet: Under the hood, it uses a
HashMapwhere the element you insert is the 'Key' and a dummy object is the 'Value'. It uses thehashCode()andequals()methods to resolve hash collisions (using buckets with linked lists or balanced trees). - TreeSet: Backed by a
TreeMap. It relies on theComparableinterface (or a providedComparator) to organize the nodes in a Red-Black tree, ensuring operations remain O(log N) even in worst-case scenarios.
Conclusion
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.
Q4. Implement a Producer-Consumer problem in Java using Multithreading, Thread Synchronization, and wait()/notify() mechanisms.
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.
1. The Java Implementation
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();
}
}
2. Concept Analysis
- synchronized(this): Ensures that only one thread can execute the block of code inside the shared buffer at any given time, preventing race conditions.
- while(...) { wait(); }: A
whileloop is crucial here, not anifstatement. When a thread wakes up fromwait(), it must re-check the condition (spurious wakeups or another thread grabbing the lock first). - notify(): Wakes up a single thread that is waiting on the object's monitor (lock). In this two-thread scenario,
notify()works perfectly. If there were multiple producers/consumers,notifyAll()would be necessary to prevent deadlocks.
Q5. Analyze Java Exception Handling architecture. Write a robust program handling multiple catch blocks, nested try-catch, and custom exceptions.
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).
1. Key Architectural Components
- Checked Exceptions: Verified at compile-time (e.g.,
IOException). The compiler forces you to handle them (try/catch or throws). - Unchecked Exceptions: Subclasses of
RuntimeException(e.g.,NullPointerException). Not checked at compile-time; usually indicate programming logic errors. - Keywords:
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).
2. Comprehensive Implementation Example
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.");
}
}
3. Best Practices Highlighted
- Custom Exceptions: Provide specific domain meaning (like
InvalidAgeException) rather than generic exceptions. - Catch Ordering:
NumberFormatExceptionandInvalidAgeExceptionare caught before the genericException. IfException ewas first, compilation would fail (unreachable code). - Resource Management: The
finallyblock ensures theScanneris closed regardless of whether the program crashes or succeeds. - Nested Blocks: The division by zero logic is isolated. If an
ArithmeticExceptionoccurs, 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...