Exception Handling in Java
Definition
An exception is an unwanted or unexpected event that occurs during the execution of a program and disrupts the normal flow of instructions. Exception handling is a mechanism to handle runtime errors gracefully without terminating the program abruptly.
Key Points
- Exceptions are objects that describe an error condition
- All exceptions are subclasses of
java.lang.Throwable - Two main subclasses:
ExceptionandError - Errors represent serious problems that applications should not try to catch
- Exceptions can be caught and handled by the program
Exception Hierarchy
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
│
└── java.lang.Exception
├── IOException (Checked)
├── SQLException (Checked)
├── ClassNotFoundException (Checked)
│
└── RuntimeException (Unchecked)
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ArithmeticException
└── IllegalArgumentException
Note
Errors are typically caused by problems outside the application's control (e.g., out of memory). Exceptions are conditions that can be anticipated and recovered from within the application.
try, catch, and finally Blocks
Syntax
- try: Contains code that might throw an exception
- catch: Handles the exception if it occurs
- finally: Always executes, used for cleanup code
public class TryCatchDemo {
public static void main(String[] args) {
try {
// Code that may throw an exception
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
// Handle the exception
System.out.println("Error: Division by zero!");
System.out.println("Message: " + e.getMessage());
} finally {
// Always executed
System.out.println("Finally block executed");
}
System.out.println("Program continues...");
}
}
Multiple catch Blocks
public class MultipleCatch {
public static void main(String[] args) {
try {
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // ArrayIndexOutOfBoundsException
int result = 10 / 0; // ArithmeticException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index error: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Arithmetic error: " + e.getMessage());
} catch (Exception e) {
// Catches any other exception
System.out.println("General error: " + e.getMessage());
}
}
}
Multi-catch (Java 7+)
try {
// Code that may throw multiple exceptions
riskyOperation();
} catch (IOException | SQLException e) {
// Handle multiple exception types with single catch block
System.out.println("IO or SQL error: " + e.getMessage());
}
Nested try Blocks
try {
// Outer try block
System.out.println("Outer try");
try {
// Inner try block
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Inner catch: " + e.getMessage());
}
} catch (Exception e) {
System.out.println("Outer catch: " + e.getMessage());
} finally {
System.out.println("Outer finally");
}
Exam Tip
The finally block always executes except when System.exit() is called or the JVM crashes. Even if there is a return statement in try or catch, finally still executes before the method returns.
Try-with-Resources (Java 7+)
import java.io.*;
public class TryWithResources {
public static void main(String[] args) {
// Resources automatically closed after try block
try (FileReader fr = new FileReader("file.txt");
BufferedReader br = new BufferedReader(fr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
// No need for finally to close resources!
}
}
Real-World Example: File Processing with Exception Handling
import java.io.*;
import java.util.*;
public class FileProcessor {
public static void processFile(String filename) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(filename));
String line;
int lineCount = 0;
while ((line = reader.readLine()) != null) {
lineCount++;
try {
// Process each line
int number = Integer.parseInt(line.trim());
System.out.println("Line " + lineCount + ": " + number);
} catch (NumberFormatException e) {
System.out.println("Line " + lineCount + ": Invalid number format");
}
}
System.out.println("Total lines processed: " + lineCount);
} catch (FileNotFoundException e) {
System.out.println("File not found: " + filename);
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
} finally {
// Always close the reader
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Error closing file: " + e.getMessage());
}
}
}
}
public static void main(String[] args) {
processFile("data.txt");
}
}
Practice Questions
Q1: What is the difference between try-catch-finally and try-with-resources?
Q2: Can we have multiple catch blocks for a single try? In what order should they be arranged?
Q3: What happens if an exception occurs in the finally block?
Q4: Write a program that demonstrates nested try-catch blocks.
Q5: Can we have a try block without a catch block? Explain.
throw and throws Keywords
throw Keyword
Definition: The throw keyword is used to explicitly throw an exception from a method or any block of code.
public class ThrowDemo {
public static void validateAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
if (age < 18) {
throw new ArithmeticException("Not eligible to vote");
}
System.out.println("Valid age: " + age);
}
public static void main(String[] args) {
try {
validateAge(15);
} catch (ArithmeticException e) {
System.out.println("Exception: " + e.getMessage());
}
}
}
throws Keyword
Definition: The throws keyword is used in method declaration to specify that the method might throw certain exceptions. It delegates the responsibility of exception handling to the caller.
import java.io.*;
public class ThrowsDemo {
// Method declares that it may throw IOException
public static void readFile(String filename) throws IOException {
FileReader file = new FileReader(filename);
BufferedReader reader = new BufferedReader(file);
String line = reader.readLine();
System.out.println(line);
reader.close();
}
// Method can throw multiple exceptions
public static void process() throws IOException, SQLException {
// Method implementation
}
public static void main(String[] args) {
try {
readFile("test.txt");
} catch (IOException e) {
System.out.println("File error: " + e.getMessage());
}
}
}
Custom Exceptions
// Custom checked exception
class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(double amount) {
super("Insufficient funds. Required: " + amount);
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
// Custom unchecked exception
class InvalidUserException extends RuntimeException {
public InvalidUserException(String message) {
super(message);
}
}
class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}
}
| throw | throws |
|---|---|
| Used to throw an exception explicitly | Used to declare an exception |
| Used inside a method | Used in method signature |
| Followed by an exception object | Followed by exception class names |
| Can throw only one exception at a time | Can declare multiple exceptions |
Checked vs Unchecked Exceptions
Checked Exceptions
Definition: Checked exceptions are exceptions that are checked at compile-time. The compiler verifies that these exceptions are either caught or declared in the method signature.
- Subclasses of
Exception(except RuntimeException) - Must be declared using
throwsor handled usingtry-catch - Examples: IOException, SQLException, ClassNotFoundException
Unchecked Exceptions
Definition: Unchecked exceptions are exceptions that are not checked at compile-time. They occur at runtime and are also called runtime exceptions.
- Subclasses of
RuntimeException - Not required to be declared or caught
- Examples: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException
| Checked Exceptions | Unchecked Exceptions |
|---|---|
| Checked at compile-time | Checked at runtime |
| Must be declared or handled | Not required to declare or handle |
| Extends Exception class | Extends RuntimeException class |
| Recoverable errors | Programming errors |
| IOException, SQLException | NullPointerException, ArithmeticException |
Comprehensive Example: Exception Handling in Banking System
import java.io.*;
// Custom Checked Exception
class InsufficientBalanceException extends Exception {
private double required;
private double available;
public InsufficientBalanceException(double required, double available) {
super("Insufficient balance. Required: " + required +
", Available: " + available);
this.required = required;
this.available = available;
}
public double getShortfall() {
return required - available;
}
}
// Custom Unchecked Exception
class InvalidAccountException extends RuntimeException {
public InvalidAccountException(String message) {
super(message);
}
}
class Account {
private String accountNumber;
private double balance;
private boolean active;
public Account(String accountNumber, double initialBalance) {
if (accountNumber == null || accountNumber.trim().isEmpty()) {
throw new InvalidAccountException("Account number cannot be empty");
}
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.active = true;
}
public void withdraw(double amount) throws InsufficientBalanceException {
if (!active) {
throw new InvalidAccountException("Account is inactive");
}
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (amount > balance) {
throw new InsufficientBalanceException(amount, balance);
}
balance -= amount;
System.out.println("Withdrawn: $" + amount);
}
public void deposit(double amount) {
if (!active) {
throw new InvalidAccountException("Account is inactive");
}
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
System.out.println("Deposited: $" + amount);
}
public void saveToFile(String filename) throws IOException {
try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
writer.println("Account: " + accountNumber);
writer.println("Balance: " + balance);
writer.println("Active: " + active);
}
}
public double getBalance() {
return balance;
}
}
public class BankingSystem {
public static void main(String[] args) {
try {
// Create account
Account acc = new Account("ACC001", 1000.0);
// Deposit money
acc.deposit(500);
// Withdraw money
acc.withdraw(300);
// Try to withdraw more than balance (checked exception)
acc.withdraw(2000);
} catch (InsufficientBalanceException e) {
System.out.println("Transaction failed: " + e.getMessage());
System.out.println("Shortfall: $" + e.getShortfall());
} catch (InvalidAccountException e) {
System.out.println("Account error: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Invalid input: " + e.getMessage());
}
// Demonstrating unchecked exception
try {
Account invalidAcc = new Account("", 100); // Throws InvalidAccountException
} catch (InvalidAccountException e) {
System.out.println("Error creating account: " + e.getMessage());
}
}
}
Common Exception Types
public class CommonExceptions {
public static void main(String[] args) {
// 1. NullPointerException (Unchecked)
String str = null;
try {
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("NPE: Trying to access null object");
}
// 2. ArrayIndexOutOfBoundsException (Unchecked)
int[] arr = {1, 2, 3};
try {
System.out.println(arr[5]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index out of bounds");
}
// 3. ArithmeticException (Unchecked)
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero");
}
// 4. NumberFormatException (Unchecked)
try {
int num = Integer.parseInt("abc");
} catch (NumberFormatException e) {
System.out.println("Invalid number format");
}
// 5. ClassCastException (Unchecked)
try {
Object obj = "Hello";
Integer num = (Integer) obj;
} catch (ClassCastException e) {
System.out.println("Invalid type cast");
}
}
}
Best Practices
Exception Handling Best Practices
- Catch specific exceptions: Always catch the most specific exception type first
- Don't swallow exceptions: Always log or handle exceptions appropriately
- Use try-with-resources: For automatic resource management
- Document exceptions: Use @throws in JavaDoc
- Don't catch Throwable or Error: These represent serious problems
- Clean up resources: Use finally or try-with-resources
- Custom exceptions: Create meaningful custom exceptions for business logic
Practice Questions
Q1: What is the difference between throw and throws? Provide examples.
Q2: Create a custom checked exception for InvalidEmailException and demonstrate its usage.
Q3: Why are unchecked exceptions called runtime exceptions?
Q4: Can a method throw both checked and unchecked exceptions? Demonstrate with code.
Q5: What is exception propagation? How does it work in Java?
import java.io.*;
public class CheckedVsUnchecked {
// Checked exception - must declare with throws
public void readFile() throws IOException {
FileReader fr = new FileReader("file.txt");
}
// Unchecked exception - no declaration needed
public void divide(int a, int b) {
int result = a / b; // May throw ArithmeticException
}
public static void main(String[] args) {
CheckedVsUnchecked demo = new CheckedVsUnchecked();
// Must handle checked exception
try {
demo.readFile();
} catch (IOException e) {
System.out.println("File error");
}
// Unchecked - can handle but not required
demo.divide(10, 2);
}
}
Exam Tip
If a method throws a checked exception, the calling code must either handle it with try-catch or declare it with throws. Unchecked exceptions do not have this requirement.
Java I/O Basics
Definition
Java I/O (Input/Output) is used to process the input and produce the output. Java uses the concept of streams to make I/O operations fast and efficient.
Types of Streams
- Byte Streams: Handle I/O of raw binary data (8-bit bytes)
- Character Streams: Handle I/O of character data (16-bit Unicode)
Stream Hierarchy
Byte Streams:
InputStream (abstract)
├── FileInputStream
├── BufferedInputStream
├── DataInputStream
└── ObjectInputStream
OutputStream (abstract)
├── FileOutputStream
├── BufferedOutputStream
├── DataOutputStream
└── ObjectOutputStream
Character Streams:
Reader (abstract)
├── FileReader
├── BufferedReader
├── InputStreamReader
└── StringReader
Writer (abstract)
├── FileWriter
├── BufferedWriter
├── OutputStreamWriter
└── PrintWriter
Note
Use byte streams for binary data (images, audio, video) and character streams for text data. Character streams automatically handle character encoding.
Byte Streams and Character Streams
Byte Streams
import java.io.*;
public class ByteStreamDemo {
public static void main(String[] args) {
// Writing bytes to file
try (FileOutputStream fos = new FileOutputStream("output.dat")) {
byte[] data = {65, 66, 67, 68, 69}; // A, B, C, D, E
fos.write(data);
System.out.println("Data written successfully");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
// Reading bytes from file
try (FileInputStream fis = new FileInputStream("output.dat")) {
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data); // Prints: ABCDE
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Character Streams
import java.io.*;
public class CharStreamDemo {
public static void main(String[] args) {
// Writing characters to file
try (FileWriter fw = new FileWriter("output.txt")) {
fw.write("Hello, Java I/O!\n");
fw.write("Learning character streams.");
System.out.println("Written to file");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
// Reading characters from file
try (FileReader fr = new FileReader("output.txt")) {
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Buffered Streams (Efficient I/O)
import java.io.*;
public class BufferedStreamDemo {
public static void main(String[] args) {
// BufferedWriter - efficient writing
try (BufferedWriter bw = new BufferedWriter(
new FileWriter("buffered.txt"))) {
bw.write("Line 1");
bw.newLine(); // Platform-independent newline
bw.write("Line 2");
bw.newLine();
bw.write("Line 3");
} catch (IOException e) {
e.printStackTrace();
}
// BufferedReader - efficient reading
try (BufferedReader br = new BufferedReader(
new FileReader("buffered.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
| Byte Streams | Character Streams |
|---|---|
| Handle raw binary data | Handle character data |
| 8-bit (1 byte) at a time | 16-bit (2 bytes) Unicode at a time |
| InputStream, OutputStream | Reader, Writer |
| Used for images, audio, video | Used for text files |
File Reading and Writing
File Class
import java.io.File;
public class FileDemo {
public static void main(String[] args) {
File file = new File("test.txt");
// File information
System.out.println("Exists: " + file.exists());
System.out.println("Name: " + file.getName());
System.out.println("Path: " + file.getAbsolutePath());
System.out.println("Is File: " + file.isFile());
System.out.println("Is Directory: " + file.isDirectory());
System.out.println("Can Read: " + file.canRead());
System.out.println("Can Write: " + file.canWrite());
System.out.println("Size: " + file.length() + " bytes");
// Create new file
try {
if (file.createNewFile()) {
System.out.println("File created");
}
} catch (Exception e) {
e.printStackTrace();
}
// Create directory
File dir = new File("myFolder");
dir.mkdir(); // Creates single directory
// List files in directory
File folder = new File(".");
String[] files = folder.list();
for (String f : files) {
System.out.println(f);
}
}
}
Complete File Operations Example
import java.io.*;
public class FileOperations {
public static void main(String[] args) {
String filename = "students.txt";
// Write to file
try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
writer.println("John,20,CS");
writer.println("Jane,21,IT");
writer.println("Bob,22,ECE");
System.out.println("Data written successfully");
} catch (IOException e) {
e.printStackTrace();
}
// Read from file
System.out.println("\nReading file:");
try (BufferedReader reader = new BufferedReader(
new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
System.out.println("Name: " + parts[0] +
", Age: " + parts[1] +
", Dept: " + parts[2]);
}
} catch (IOException e) {
e.printStackTrace();
}
// Append to file
try (FileWriter fw = new FileWriter(filename, true)) { // true = append mode
fw.write("Alice,23,ME\n");
System.out.println("\nData appended");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Warning
Always close streams after use to release system resources. Use try-with-resources (Java 7+) to automatically close streams.
Multithreading in Java
Definition
Multithreading is a feature that allows concurrent execution of two or more parts of a program to maximize CPU utilization. Each part of such a program is called a thread.
Thread Lifecycle
- New: Thread is created but not yet started
- Runnable: Thread is ready to run and waiting for CPU
- Running: Thread is executing
- Blocked/Waiting: Thread is waiting for a resource or another thread
- Terminated: Thread has completed execution
Creating Threads
There are two ways to create a thread:
Method 1: Extending Thread Class
class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + ": " + i);
try {
Thread.sleep(500); // Sleep for 500 milliseconds
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ExtendThread {
public static void main(String[] args) {
MyThread t1 = new MyThread("Thread-1");
MyThread t2 = new MyThread("Thread-2");
t1.start(); // Starts the thread
t2.start();
System.out.println("Main thread finished");
}
}
Method 2: Implementing Runnable Interface
class MyRunnable implements Runnable {
private String threadName;
public MyRunnable(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + ": " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ImplementRunnable {
public static void main(String[] args) {
MyRunnable r1 = new MyRunnable("Thread-1");
MyRunnable r2 = new MyRunnable("Thread-2");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
Thread Priority
public class ThreadPriority {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Low priority: " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("High priority: " + i);
}
});
// Priority values: 1 (MIN) to 10 (MAX), 5 is NORMAL
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
System.out.println("t1 priority: " + t1.getPriority());
System.out.println("t2 priority: " + t2.getPriority());
t1.start();
t2.start();
}
}
Thread Methods
| Method | Description |
|---|---|
| start() | Starts the thread execution |
| run() | Contains the code to be executed |
| sleep(ms) | Pauses execution for specified milliseconds |
| join() | Waits for thread to complete |
| isAlive() | Checks if thread is still running |
| yield() | Suggests scheduler to give chance to other threads |
| interrupt() | Interrupts the thread |
Common Mistake
Calling run() directly instead of start() will not create a new thread. The code will execute in the current thread. Always use start() to create a new thread.
Synchronization and Inter-thread Communication
Definition
Synchronization is a mechanism that ensures that only one thread can access a shared resource at a time. It prevents thread interference and memory consistency errors.
Synchronized Method
class Counter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SyncMethodDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
System.out.println("Count: " + counter.getCount()); // Always 2000
}
}
Synchronized Block
class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void withdraw(double amount) {
// Synchronized block - locks on 'this' object
synchronized (this) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() +
" withdrawing: " + amount);
balance -= amount;
System.out.println("Balance: " + balance);
} else {
System.out.println("Insufficient funds");
}
}
}
}
Inter-thread Communication
Java provides methods for threads to communicate with each other:
- wait(): Causes current thread to wait until another thread calls notify()
- notify(): Wakes up a single thread that is waiting
- notifyAll(): Wakes up all waiting threads
class SharedQueue {
private int data;
private boolean hasData = false;
public synchronized void produce(int value) throws InterruptedException {
while (hasData) {
wait(); // Wait until data is consumed
}
data = value;
System.out.println("Produced: " + value);
hasData = true;
notify(); // Notify consumer
}
public synchronized int consume() throws InterruptedException {
while (!hasData) {
wait(); // Wait until data is produced
}
System.out.println("Consumed: " + data);
hasData = false;
notify(); // Notify producer
return data;
}
}
public class ProducerConsumer {
public static void main(String[] args) {
SharedQueue queue = new SharedQueue();
// Producer thread
Thread producer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
queue.produce(i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
queue.consume();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
consumer.start();
}
}
Exam Tip
Remember: wait(), notify(), and notifyAll() must be called from within a synchronized context (synchronized method or block). They are methods of the Object class, not Thread class.