Oopj Unit-4
Oopj Unit-4
Neelima
Exception Handling
In java, the exception handling mechanism uses five keywords namely try, catch, finally,
throw, and throws.
In Java, exceptions are organized into a hierarchical structure, rooted in the Throwable
class. Understanding this hierarchy helps developers grasp the different types of errors,
their origins, and how to handle them effectively.
1. Throwable Class
• The Throwable class is the root of the hierarchy. It provides methods like
printStackTrace(), getMessage(), and getCause() to retrieve details about the error.
All exceptions and errors are subclasses of Throwable.
2. Error Class
• Error represents simple problems that an application typically cannot recover from.
Errors are generated by the Java runtime environment and are not intended to be
caught or handled by the application. Some common subclasses of Error include:
o OutOfMemoryError: Thrown when the JVM runs out of memory.
o StackOverflowError: Thrown when there is a deep or infinite recursion
causing the stack to overflow.
o VirtualMachineError: Indicates a serious problem in the Java Virtual
Machine, such as InternalError.
Examples of Errors
• StackOverflowError
• OutOfMemoryError
import java.util.ArrayList;
Explanation: This code continuously allocates large memory chunks until it runs out of
memory, triggering an OutOfMemoryError.
3. Exception Class
The Exception class represents conditions that an application might want to catch.
Exceptions can be divided into two main categories: checked and unchecked exceptions.
Java exceptions divided into two primary categories: checked exceptions and unchecked
exceptions.
1. Checked Exceptions
the throws keyword. The compiler checks for these exceptions, ensuring they are
handled.
• Use Case: These exceptions are typically caused by external conditions (e.g., file I/O,
database access) and are often outside the programmer’s control.
Occurs when there’s a failure in input or output operations, such as reading from a file
that doesn’t exist.
Example:
import java.io.FileReader;
import java.io.IOException;
public class IOExceptionExample
{
public static void main(String[] args)
{
try
{
FileReader file = new FileReader("nonexistentfile.txt");
}
catch (IOException e)
{
System.out.println("File not found or cannot be opened: " + e.getMessage());
}
}
}
Example:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
Connection conn =
DriverManager.getConnection("jdbc:mysql://localhost:3306/nonexistentdb",
"user", "password");
}
catch (SQLException e)
{
System.out.println("Database access error: " + e.getMessage());
}
}
}
3. ClassNotFoundException
This exception occurs when the application tries to load a class that is not found in the
classpath.
Example:
2. Unchecked Exceptions
1. ArithmeticException
Example:
2. NullPointerException
Thrown when there’s an attempt to use an object reference that has not been initialized.
Example:
3. ArrayIndexOutOfBoundsException
Example:
try
{
System.out.println(numbers[3]);
}
catch (ArrayIndexOutOfBoundsException e)
{
System.out.println("Array index out of bounds: " + e.getMessage());
}
}
}
4. NumberFormatException
Thrown when an attempt is made to convert a string into a numeric type but the string
doesn’t contain a valid format.
Example:
In Java, the exception-handling mechanism revolves around five key components: try,
catch, throw, throws, and finally. Each of these plays a unique role in managing and
recovering from exceptions in a structured way.
1. try Block
The try block is used to wrap a segment of code that might throw an exception. When an
exception occurs within a try block, Java jumps out of the try block and looks for a
corresponding catch block to handle the exception.
Example:
Explanation: Here, an ArithmeticException occurs in the try block (division by zero). The
program control transfers to the catch block, and the message "Cannot divide by zero." is
displayed. The System.out.println line within the try block is skipped.
2. catch Block
The catch block is used to handle exceptions that are thrown in the associated try block.
Each catch block specifies the type of exception it can handle, and multiple catch blocks
can be used to handle different exception types.
Example:
try
{
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // this throw ArrayIndexOutOfBoundsException
}
catch (ArrayIndexOutOfBoundsException e)
{
System.out.println("Index is out of bounds!");
}
}
}
3. throw Statement
The throw keyword is used to explicitly throw an exception from within a method or block
of code. This is useful when you want to create custom error conditions or trigger
exceptions manually.
Syntax:
Example:
4. throws Clause
The throws clause is used in a method’s signature to declare the types of exceptions that a
method might throw. This informs any calling method that it should handle or further
declare the exception.
Syntax:
Example:
import java.io.FileReader;
import java.io.IOException;
public class ThrowsExample
{
public static void main(String[] args)
{
try
{
readFile();
}
catch (IOException e)
{
System.out.println("File not found or cannot be opened.");
}
}
Explanation: The readFile method declares throws IOException, meaning that it might
throw an IOException. Since this exception is declared but not handled in readFile, it is
caught in the main method where readFile is called.
5. finally Block
The finally block is always executed after the try and catch blocks, regardless of whether
an exception was thrown or caught. This block is used for cleanup activities, like closing a
file or releasing resources.
Example:
import java.io.FileInputStream;
import java.io.IOException;
Explanation: Here, the finally block is used to ensure the file is closed regardless of
whether an exception occurs in the try block. This is essential for resource management,
ensuring that resources are released even if an error occurs.
In Java, multiple catch statements allow a program to handle different types of exceptions
separately, based on the specific exception that occurs. Each catch block handles a specific
exception type, providing a more controlled and detailed response to different errors. This
is useful in cases where multiple exceptions might be thrown by the same try block, and
each needs a unique handling approach.
In this structure:
• The try block wraps the code that may throw one or more exceptions.
• Each catch block follows the try block and catches a specific exception type.
• If an exception occurs, the control is transferred to the corresponding catch block
that matches the exception type.
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Error: Array index is out of bounds.");
} catch (ArithmeticException e) {
System.out.println("Error: Cannot divide by zero.");
} catch (Exception e) {
Explanation:
In Java 7 and later, multiple exceptions can also be caught in a single catch block by
separating each exception type with a pipe | symbol. This approach is useful for handling
different exceptions with the same code block, reducing code redundancy.
Custom Exceptions
Custom exceptions in Java are user-defined exceptions created to represent specific error
conditions in an application. When the standard Java exception classes (such as IOException
or NullPointerException) don’t adequately capture the nature of a problem, custom
exceptions provide a way to create a specialized exception that can make the code easier to
understand, debug, and maintain.
1. Define a class that extends the Exception class (for checked exceptions) or the
RuntimeException class (for unchecked exceptions).
2. Implement a constructor that takes an error message or any other parameters as
needed.
3. Optionally, override methods like toString() to provide a custom string
representation of the exception.
Let’s create a custom exception named InvalidAgeException that will be thrown if a user’s
age is below a required threshold.
}
}
Explanation:
Explanation:
• Naming Conventions: Use names that end with "Exception" to make it clear they
are exception classes.
• Use Meaningful Messages: Include informative error messages to clarify what
went wrong.
• Minimize Unchecked Custom Exceptions: Only use unchecked custom exceptions
for runtime conditions that the program generally won’t recover from.
• Document Custom Exceptions: Clearly document custom exceptions for
developers, as they are specific to the application.
Custom exceptions make error-handling code more in-built and tailored to the needs of
specific applications, enhancing maintainability and debugging.
Nested try and catch blocks allow Java developers to handle exceptions that occur within a
specific portion of code, especially when multiple layers of exception handling are needed.
A try block can contain another try-catch block within it, which is useful when an inner code
block may throw an exception that is handled specifically by an inner catch block, while the
outer try-catch structure handles exceptions that arise from a broader scope.
• Specific Exception Handling: They allow different levels of handling for exceptions,
from specific cases in the inner block to more general exceptions in the outer block.
• Preventing Program Termination: They allow the program to handle exceptions
at multiple levels without terminating abruptly.
• Organized Error Management: Useful when some exceptions need to be handled
locally within a specific part of the code, while others need more generalized
handling.
Here’s an example of a nested try-catch block. The inner try block handles specific
exceptions related to array indexing, while the outer try block catches any arithmetic issues.
try {
// Attempt to access an invalid index
System.out.println(numbers[5]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Inner catch: Array index out of bounds.");
}
} catch (ArithmeticException e) {
System.out.println("Outer catch: Cannot divide by zero.");
} finally {
System.out.println("Finally block: Cleanup actions here.");
}
}
}
Explanation:
1. Inner try-catch Block: Attempts to access an invalid index in numbers. When this
causes an ArrayIndexOutOfBoundsException, the inner catch block handles it with
a specific message.
2. Outer try-catch Block: After the inner block executes, an ArithmeticException is
triggered by dividing by zero (10 / 0). The outer catch block catches and handles
this exception.
3. Finally Block: Runs cleanup code regardless of whether an exception occurred.
Output:
In some cases, multiple layers of nesting can be used for very specific error handling.
try {
System.out.println("Trying to divide by zero:");
int result = num / denom;
} catch (ArithmeticException e) {
System.out.println("Innermost catch: Division by zero error.");
}
try {
System.out.println("Accessing an invalid array index:");
System.out.println(arr[10]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Inner catch: Array index out of bounds.");
}
} catch (Exception e) {
System.out.println("Outer catch: General exception caught.");
}
}
}
Explanation:
1. File Processing: Attempting to read/write to files within an inner try block and
catching IO-related exceptions while handling more generic errors (e.g.,
FileNotFoundException) in the outer block.
2. Database Operations: Nesting can handle database connection errors in the outer
try-catch block and specific query-related exceptions in the inner block.
3. Complex Calculations: For applications with multiple potential exception types
(e.g., array, arithmetic), nested try-catch can allow for handling distinct issues
independently.
Nested try-catch blocks help manage complex error conditions by enabling targeted
exception handling at various levels, leading to more organized and maintainable code.
************
What is a Thread?
What is Multithreading?
1. Multithreading Example:
o Gaming Application: Different threads can handle rendering graphics,
processing user input, and managing background sound.
o Web Server: A server might use a new thread for each client request,
enabling multiple requests to be handled concurrently within the same
server application.
2. Multitasking Example:
o Operating System: Running a browser, a text editor, and a music player
simultaneously.
o Background Services: A system may have antivirus software, file syncing
applications, and other services running in the background as separate
processes.
Each approach has its own benefits and is chosen based on specific use cases. Let’s go
through a deep explanation of each method, how they differ, and when to use them.
When a class extends the Thread class, it inherits all its methods and properties. In this
approach, we create a new class that directly extends Thread, and then override its run()
method to define the code that will be executed when the thread starts.
Key Points
• When extending Thread, each instance of the class represents a new thread.
• The start() method (inherited from the Thread class) is called to begin execution of
the new thread. Internally, start() invokes the run() method, which contains the
thread’s task.
• Because Java doesn’t support multiple inheritance, if a class extends Thread, it
cannot extend any other class. This is a significant limitation and one of the reasons
why implementing Runnable is often preferred.
In this example, MyThread class extends Thread, and the run() method contains the task
to be performed by this thread. When t1.start() is called, it internally calls the run()
method, executing the task in a new thread.
• Inflexibility: Since Java only allows single inheritance, extending Thread restricts
the ability to extend another class.
• Tight coupling: By inheriting from Thread, the task’s code is tightly coupled to
thread management code, which reduces flexibility and reusability.
Key Points
In this example, MyRunnable implements Runnable, and the task is defined in its run()
method. A Thread object is created by passing the Runnable object as an argument, then
start() is called on the Thread instance.
• Flexibility: Allows the class to extend another class, which is more flexible in
complex applications.
• Decoupling: The task code (defined in run()) is separate from thread
management, making the design more modular and promoting reusability.
• Resource Efficiency: Allows sharing a single Runnable instance between multiple
threads if needed.
• Less direct access to Thread methods: Since it doesn’t extend Thread, we have to
use Thread.currentThread() to access thread-specific methods.
}
System.out.println("CharacterThread finished execution.");
}
}
// Thread that prints a custom message five times
class MessageThread extends Thread {
private String message;
1. Newborn (New):
• When a thread is created using the new keyword but has not yet started, it is in the
Newborn or New state.
• A thread in this state is not active or running.
• Transition:
o The thread moves to the Runnable state when the start() method is called.
2. Runnable:
• In the Runnable state, the thread is ready to run but may not be actively running.
• It is waiting for CPU time to execute. The Java scheduler determines which thread
runs based on thread priority and other factors.
• Transitions:
o To Running: When the CPU assigns time to the thread, it enters the
Running state.
o To Blocked: If yield() is called, the thread may yield control to another
thread, returning to the Runnable state.
3. Running:
• A thread is in the Running state when it is actively executing its run() method.
• This is where the actual processing takes place.
• Transition:
o To Runnable: The yield() method allows the thread to re-enter the
Runnable state, giving other threads a chance to execute.
o To Blocked: If the thread calls sleep(), wait(), or suspend(), it transitions to
the Blocked state.
4. Blocked:
• In the Blocked state, the thread is temporarily inactive. It is waiting for some
condition to be met before it can continue running.
• This state occurs due to methods like sleep(t), wait(), or suspend().
• Transitions:
o To Runnable: When the thread receives a notify(), notifyAll(), or resume()
signal, or the sleep time ends, it returns to the Runnable state.
5. Dead:
• The thread reaches the Dead state once it has completed execution or if it was
terminated prematurely by the stop() method.
• A thread in this state cannot be restarted or brought back to life.
• Transition:
o A thread moves to the Dead state after the stop() method is called or when
it finishes executing the run() method.
• The stop() method is deprecated due to potential issues with releasing resources
abruptly, so it is generally avoided in modern Java applications.
• The Java scheduler determines which thread will enter the Running state when
multiple threads are in the Runnable state, typically based on priority or other
system-specific factors.
Java threads have priorities that help the scheduler decide the order in which threads are
scheduled for execution. Each thread has a priority between MIN_PRIORITY (1) and
MAX_PRIORITY (10), with NORM_PRIORITY (5) as the default.
To set a thread's priority, use the setPriority(int priority) method, where priority is an
integer between 1 and 10.
t1.start();
t2.start();
t3.start();
}
}
In this example:
• t1 has MIN_PRIORITY.
• t2 has NORM_PRIORITY.
• t3 has MAX_PRIORITY.
Since t3 has the highest priority, it is more likely to execute before t1 and t2. However,
this behavior may vary due to JVM scheduling.
1. Synchronized Method
2. Synchronized Block
3. Static Synchronization
1. Synchronized Method
A synchronized method locks the object for any other synchronized method on the same
object, allowing only one thread to execute at a time.
@Override
public void run() {
for (int i = 0; i < 5; i++) {
deposit(amount); // Calls the synchronized deposit method
}
}
}
Example – 2
// Synchronized method to prevent multiple threads from booking the same ticket
public synchronized void bookTicket() {
if (availableTickets > 0) {
System.out.println(passengerName + " successfully booked a ticket. Tickets left:
" + --availableTickets);
} else {
System.out.println("Sorry, " + passengerName + ". Tickets are sold out.");
}
}
@Override
public void run() {
bookTicket();
}
}
2. Synchronized Block
A synchronized block limits the scope of synchronization, locking only the specified
section of code. This is useful for minimizing the locking overhead.
class BankAccount {
private int balance = 1000;
balance -= amount;
System.out.println(Thread.currentThread().getName() + " withdrew " +
amount + ". Balance: " + balance);
} else {
System.out.println("Insufficient funds for " +
Thread.currentThread().getName());
}
}
}
t1.start();
t2.start();
}
}
In this example, only the withdrawal logic is synchronized, reducing the time that the lock
is held.
@Override
public void run() {
bookTicket();
}
}
3. Static Synchronization
Static synchronization locks the class rather than an instance, meaning only one thread
can execute any synchronized static method in the class at a time.
class Counter {
private static int count = 0;
t1.start();
t2.start();
}
}
Here, increment is a static synchronized method, ensuring that only one thread can access
this method across all instances of Counter.
The ReentrantLock class provides more control over thread synchronization, allowing
features such as fairness policy, re-entrant locking, and timed waits.
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
t1.start();
t2.start();
}
}
In this example:
Thread Methods
Method Description
start() Starts a new thread and calls the run() method of the thread,
Method Description
placing it in the runnable state.
Contains the code that defines the task of the thread. It should be
run() overridden when implementing a new thread, but it does not start
the thread itself.
Pauses the execution of the current thread for the specified
sleep(long millis) number of milliseconds, allowing other threads to execute. Throws
InterruptedException.
Waits for the thread to complete execution before proceeding with
join()
the code following it in the calling thread.
Waits for a thread to finish or for the specified timeout to expire.
join(long millis)
This is useful to ensure threads finish within a certain time limit.
Temporarily pauses the execution of the current thread, giving
yield()
other threads of equal priority a chance to execute.
Interrupts a thread, setting its interrupt status to true. If the
interrupt()
thread is blocked, it will throw an InterruptedException.
Checks if the thread has been interrupted, returning true if the
isInterrupted()
thread is in the interrupted state.
Sets the priority of the thread to the specified integer value (1 to
setPriority(int
10). Higher priorities increase the likelihood of the thread being
priority)
scheduled earlier.
getPriority() Returns the priority of the thread.
setName(String
Changes the name of the thread to the specified string value.
name)
getName() Returns the name of the thread.
Checks if the thread is still running, returning true if the thread is
isAlive()
alive (i.e., started and not terminated).
currentThread() Returns a reference to the currently executing thread.
getId() Returns a unique identifier for the thread.
Marks the thread as a daemon thread (background thread).
setDaemon(boolean
Daemon threads terminate when all non-daemon threads have
on)
finished.
isDaemon() Checks if the thread is a daemon thread, returning true if it is.
Returns the current state of the thread (e.g., NEW, RUNNABLE,
getState()
BLOCKED, WAITING, TIMED_WAITING, TERMINATED).
Causes the current thread to wait until another thread calls
wait() notify() or notifyAll() on the same object. Must be called in a
synchronized context.
Wakes up one waiting thread on the same object. Must be called in
notify()
a synchronized context.
Wakes up all threads waiting on the same object. Must be called in
notifyAll()
a synchronized context.
This table provides a concise overview of thread methods, highlighting their functionality
and typical usage.
Here's a Java program demonstrating several key thread methods, including start(), run(),
sleep(), join(), setPriority(), interrupt(), isInterrupted(), getState(), and isAlive().
This program creates three threads with different priorities, demonstrates sleep and join
methods, and showcases how to handle interruptions.
} catch (InterruptedException e) {
System.out.println("Main thread interrupted!");
}
// Interrupt one of the threads if it is still running
if (t1.isAlive()) {
System.out.println("Interrupting " + t1.getName());
t1.interrupt();
}
}
}
Explanation of Methods Used
This example shows how to control thread execution and manage interruption gracefully,
providing output on each thread's activity and its final state.
***********