0% found this document useful (0 votes)
13 views98 pages

Java Unit 5

The document provides an overview of file handling and multithreading in Java, detailing the classes and methods used for file operations, as well as exception handling best practices. It also explains the concept of threads, the benefits of multithreading, and how to create and manage threads in Java, including their life cycle and key methods. Additionally, it highlights the differences between multitasking and multithreading, and offers practical examples for better understanding.

Uploaded by

mayankit2023
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
13 views98 pages

Java Unit 5

The document provides an overview of file handling and multithreading in Java, detailing the classes and methods used for file operations, as well as exception handling best practices. It also explains the concept of threads, the benefits of multithreading, and how to create and manage threads in Java, including their life cycle and key methods. Additionally, it highlights the differences between multitasking and multithreading, and offers practical examples for better understanding.

Uploaded by

mayankit2023
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 98

Task Class Used

File info File

Read text Scanner, BufferedReader

Write text FileWriter, BufferedWriter

Binary read FileInputStream

Binary write FileOutputStream

Exception Handling in File Handling


When working with file handling in Java, it's crucial to implement proper
exception handling to ensure robust and error-free applications. File
handling operations are prone to various errors, primarily related to file
accessibility and permissions. This section discusses the importance of
handling IOException and other potential issues that may arise during file
operations.

Common File Handling Exceptions

File handling operations in Java can fail for several reasons. Here are some
common scenarios where exceptions might occur:

1. File Not Found:


Cause: Attempting to open or read from a file that does not exist.
Handling: Use FileNotFoundException to catch and handle this
situation.
2. File is Locked or in Use:
Cause: Trying to access a file that is currently being used by another
process or is locked.
Handling: Implement logic to check file availability or handle using
appropriate catch blocks.
3. No Write Permissions:
Cause: Attempting to write to a file or directory without sufficient
permissions.
Handling: Catch IOException and provide feedback to the user about
permission issues.

Best Practices for Handling IOExceptions

To handle exceptions effectively during file operations, follow these best


practices:

Wrap File Operations in try-catch Blocks: Always use try-catch blocks


to capture and handle potential exceptions during file operations. This
approach ensures that your program can gracefully handle errors
without crashing.
Use try-with-resources: This feature automates resource management
and ensures that files are closed properly, even if exceptions occur. It
simplifies code and helps prevent resource leaks.
Provide Meaningful Error Messages: When catching exceptions,
provide clear and informative messages to the user. This feedback can
help diagnose and resolve issues quickly.

Code Example: Handling File Exceptions

Here is an example demonstrating how to handle common file handling


exceptions using try-catch blocks:

import java.io.*;

public class FileHandlingExample {


public static void main(String[] args) {
// Using try-with-resources to ensure the file is closed automatically
try (BufferedReader reader = new BufferedReader(new
FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.out.println("File not found! Please check the file path.");
} catch (IOException e) {
System.out.println("An error occurred while accessing the file: " +
e.getMessage());
}
}
}

Explanation

try-with-resources: Automatically manages resource closure, ensuring


that the BufferedReader is closed after use.
FileNotFoundException Catch Block: Provides a specific message if the
file is not found.
IOException Catch Block: Handles other I/O errors, such as read
permissions or file in use, and outputs a detailed error message.

By following these practices and understanding common file handling


exceptions, you can develop Java applications that are more resilient and
user-friendly.

Multithreading in Java
What is a Thread?

A thread is the smallest unit of a program that can run independently. In


Java, a thread represents an individual path of execution inside a program.

What is Multithreading?

Multithreading is the capability of a CPU or a single process to execute


multiple threads concurrently. Java allows you to write programs that
perform many tasks simultaneously using multithreading.

🧠 Analogy
Imagine you're in a kitchen (your program), and you’re doing multiple tasks
like:

Boiling water
Cutting vegetables
Washing dishes

Each task is like a thread, and doing them at the same time (or switching
between them quickly) is multithreading.

💡 Key Concepts of Multithreading


Concept Description

Thread Smallest unit of processing

Multithreading Running multiple threads


concurrently in a single program

Concurrent Executing more than one thread


seemingly at the same time

Asynchronous Threads may run independently of


each other without blocking

Why Use Multithreading?

Better resource utilization: CPU is not idle.


Improves performance: Increases efficiency of applications.
Responsive GUI: Crucial for games, user interfaces, etc.
Useful in servers: Handles multiple users at once.

Difference Between Multitasking and Multithreading


Feature Multitasking Multithreading

Definition Executing multiple Executing multiple


processes threads

Unit of Execution Process Thread

Memory Usage High (each process Low (threads share


has its own memory) memory)

Context Switching Higher Lower


Cost

Communication Inter-process Threads


communication communicate more
needed easily

Example Running Word, In Zoom: audio, video,


Chrome, and Zoom screen sharing
together

Single Thread vs Multi-thread

Single Thread:

----------------
| Task A | Task B | Task C |
----------------

Multithreaded:

-----------------
| A1 | B1 | A2 | B2 | A3 | C1 |
-----------------

Switches between tasks quickly so it feels parallel.

How Java Supports Multithreading


Java provides two ways to create threads:

1.By extending Thread class

class MyThread extends Thread {


public void run() {
System.out.println("Thread is running...");
}

public static void main(String[] args) {


MyThread t = new MyThread();
t.start(); // Starts the thread
}
}

2.By implementing Runnable interface

class MyRunnable implements Runnable {


public void run() {
System.out.println("Runnable thread running...");
}

public static void main(String[] args) {


Thread t = new Thread(new MyRunnable());
t.start(); // Starts the thread
}
}

Real-world Examples of Multithreading


Application Type Threads Used

Web Browser Rendering page, downloading file,


playing video

MS Word Typing, Auto-saving, Grammar


check

Games Background music, character


movement, scoring

ATM Machine Balance check, printing, updating


database

By leveraging multithreading, Java applications can perform more


efficiently, providing better performance and responsiveness. This is
especially beneficial in environments where tasks can be parallelized, such
as in graphical user interfaces or server-side applications.

Introduction to Multithreading
Understanding Threads

Analogy: Imagine you're cooking and washing clothes at the same time.
Each task represents a thread, and multitasking is akin to multithreading in
programming.

Explanation:

A thread is a lightweight sub-process, the smallest unit of a program


that can be executed independently.
Multithreading refers to the concurrent execution of two or more
threads, enabling multiple tasks to be processed simultaneously.

Java supports multithreading through the Thread class or the Runnable


interface.
public class MyThread extends Thread {
public void run() {
System.out.println("Thread is running...");
}

public static void main(String[] args) {


MyThread t = new MyThread();
t.start(); // Starts the thread
}
}

public class MyRunnable implements Runnable {


public void run() {
System.out.println("Runnable thread running...");
}

public static void main(String[] args) {


MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start(); // Runnable with Thread
}
}

Creating Threads – Two Ways

A. Extending Thread Class

class MyThread extends Thread {


public void run() {
System.out.println("Thread using Thread class.");
}

public static void main(String[] args) {


MyThread t = new MyThread();
t.start();
}
}

B. Implementing Runnable Interface


class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread using Runnable interface.");
}

public static void main(String[] args) {


Thread t = new Thread(new MyRunnable());
t.start();
}
}

When to Use What?

Use Thread if you don’t need to extend any other class.


Use Runnable if your class already extends another class.

Life Cycle of a Thread in Java


Understanding the life cycle of a thread in Java is crucial for developing
robust multithreaded applications. This guide provides an in-depth look at
each state a thread can be in during its execution.

What is a Thread?

A thread is a lightweight subprocess, serving as a path of execution within


a program. In Java, multithreaded programming allows multiple threads to
run concurrently, optimizing CPU usage.

1. New State

Explanation

When you create a thread object, it enters the New state. At this point, the
thread is constructed but not yet started.

Analogy

Think of a new car parked in your garage—built and ready, but not yet
driven.

Code Example
Thread t = new Thread(); // Thread is in NEW state

2. Runnable State

Explanation

Calling the start() method moves the thread to the Runnable state. It is
now eligible to run, though it may not start immediately as it depends on
the thread scheduler.

Analogy

Being in a queue at a bank—ready, but waiting your turn.

Code Example

Thread t = new Thread();


t.start(); // Thread is now in Runnable state

3. Running State

Explanation

A thread transitions from Runnable to Running when the thread scheduler


selects it for execution. The thread's run() method is executed at this stage.

Analogy

You were in the queue, and now it's your turn at the counter—you're
actively being served.

Code Example

class MyThread extends Thread {


public void run() {
System.out.println("Thread is running..."); // Running state
}

public static void main(String[] args) {


MyThread t = new MyThread();
t.start(); // Scheduler moves it to running
}
}
4. Blocked / Waiting / Timed Waiting

Explanation

These are non-runnable states where a thread waits for some event (like
I/O, sleep, or lock).

Blocked: Waiting to enter a synchronized block, but another thread


holds the lock.
Waiting: Indefinitely waiting for another thread to perform a specific
action.
Timed Waiting: Waiting for a specified time, such as with sleep() or
join().

Analogy

Being in a waiting room—you’re not done, just on hold for some condition.

Code Example (Timed Waiting)

public class TestSleep {


public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
Thread.sleep(2000); // Timed Waiting state
System.out.println("Woke up!");
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
});
t.start();
}
}

5. Terminated / Dead State

Explanation

A thread reaches the Terminated state once it completes execution or is


terminated due to an error.

Analogy
Like a completed online exam—you submitted it, and it’s over. You can’t go
back.

Code Example

class MyThread extends Thread {


public void run() {
System.out.println("Running");
}

public static void main(String[] args) {


MyThread t = new MyThread();
t.start(); // Runs and dies after run() completes
System.out.println("Is thread alive? " + t.isAlive()); // false after
completion
}
}

Transitions Summary Table


State How to Reach What Happens Here

New Thread t = new Thread object created


Thread(); but not started

Runnable t.start(); Thread ready to run,


waiting for CPU

Running Picked by scheduler Thread is executing


run()

Blocked Waiting for lock Can't enter


synchronized block

Waiting wait(), join() Waiting for another


thread indefinitely

Timed Waiting sleep(), join(time), Waiting for specified


wait(time) time

Terminated Run completed or Thread is dead, can’t


stop() called restart

Understanding these states and transitions helps in optimizing thread


management and ensuring efficient program execution.

Java Thread Methods


Threads in Java allow concurrent execution of two or more parts of a
program. Java provides several methods to manage and control threads.
Below is an explanation of key methods provided by the Thread class.

1. start()

Purpose:

Begins a new thread by using the run() method in a different series of


commands.
Explanation:
The start() method doesn't call run() itself. Instead, it makes a new thread
and then runs the run() method inside that new thread.

Analogy:
Think of pressing the "start" button on a treadmill—it begins moving on its
own, independently.

Example:

class MyThread extends Thread {


public void run() {
System.out.println("Thread is running...");
}
}

public class Test {


public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // Starts the thread
}
}

2. run()

Purpose:

This part shows the exact code the thread will run. It's important for
setting up what the thread will do while it's active.

Explanation:

When you create a thread, you need to change a specific method to make
the thread do what you want. This helps the thread work correctly in your
application. You can do this by either extending the Thread class or using
the Runnable interface. By extending the Thread class, you override the
run method to specify what the thread should do. Alternatively, with the
Runnable interface, you define the run method in a separate class, which is
useful if you need to extend another class. Both methods let you control
how the thread works to fit your needs..
Note:
Calling run() directly won’t start a new thread; it runs on the current
thread.

Example:

class MyThread extends Thread {


public void run() {
System.out.println("Inside run method");
}
}

public class Test {


public static void main(String[] args) {
MyThread t = new MyThread();
t.run(); // Runs in the main thread, not a new one
}
}

3. sleep(milliseconds)

Purpose:

To stop the thread for a set amount of time.

Explanation:
This is used to hold off execution, making the thread start again after the
chosen sleep time is over.

Analogy:
Like setting a timer for a microwave to pause before continuing.

Example:

public class SleepDemo {


public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
Thread.sleep(1000); // Sleeps for 1 second
}
}
}

4join()

Purpose:

The join() method is used in multithreading to make one thread wait for
another thread to finish its execution. This is crucial when a task needs to
be executed in a specific order and a thread must complete before others
can proceed.

Explanation:

In multithreading, threads often run concurrently and independently,


which means they can execute out of order. However, there are situations
where it's essential for one thread to complete its task before other
threads continue. The join() method ensures this by pausing the execution
of the calling thread until the thread on which join() is invoked has
completed its task. For instance, if you have a main thread that relies on
data processed by a worker thread, you would use join() to make sure the
main thread waits for the worker thread to finish processing before it
proceeds with its dependent operations. This coordination ensures the
program runs smoothly and tasks are completed in the desired sequence.

Analogy:
Waiting for your friend to finish talking before you start.

Example:

class MyThread extends Thread {


public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("Child Thread: " + i);
}
}
}

public class JoinDemo {


public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
t.join(); // Main thread waits until t finishes
System.out.println("Main Thread Finished");
}
}

5. isAlive()

Purpose:
Checks if a thread is still active (running or not finished).

Explanation:
Returns true if the thread has been started and hasn’t finished yet.

Example:

class MyThread extends Thread {


public void run() {
System.out.println("Thread running...");
}
}

public class AliveCheck {


public static void main(String[] args) {
MyThread t = new MyThread();
System.out.println(t.isAlive()); // false
t.start();
System.out.println(t.isAlive()); // true
}
}

6. setName(String name) and getName()

Purpose:
Sets or retrieves the name of the thread.

Explanation:
Helpful for debugging and logging.
Example:

class MyThread extends Thread {


public void run() {
System.out.println("Running: " + Thread.currentThread().getName());
}
}

public class NameDemo {


public static void main(String[] args) {
MyThread t = new MyThread();
t.setName("WorkerThread");
t.start();
System.out.println("Thread name: " + t.getName());
}
}

7. setPriority(int priority) and getPriority()

Purpose:
Sets or retrieves thread priority (1 to 10).

Explanation:
Default is 5. Higher priority doesn't guarantee early execution—it’s just a
suggestion to the JVM.

Example:

public class PriorityDemo {


public static void main(String[] args) {
Thread t1 = new Thread(() -> System.out.println("Thread 1"));
Thread t2 = new Thread(() -> System.out.println("Thread 2"));

t1.setPriority(2); // Minimum
t2.setPriority(8); // Higher

t1.start();
t2.start();
}
}

8. yield()

Purpose:
Temporarily pauses the current thread to give a chance to other threads of
the same priority.

Explanation:
It’s a hint to the scheduler and may or may not be honored.

Example:

public class YieldDemo {


public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 1");
Thread.yield(); // Hint to give up CPU
}
});

Thread t2 = new Thread(() -> {


for (int i = 0; i < 5; i++) {
System.out.println("Thread 2");
}
});

t1.start();
t2.start();
}
}

9. interrupt()

Purpose:
Interrupts a thread that’s in sleep or waiting state.
Explanation:
Used to stop or signal a thread for a task like cancellation.

Example:

public class InterruptDemo {


public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
Thread.sleep(5000);
System.out.println("Woke up!");
} catch (InterruptedException e) {
System.out.println("Thread interrupted!");
}
});

t.start();
t.interrupt(); // Interrupt the thread
}
}

Summary Table
Method Description Use Case

start() Starts a new thread Initiates parallel


execution

run() Code executed by Custom thread logic


thread

sleep(ms) Pauses thread for Delay in task


time

join() Waits for thread to Sequential task


finish execution

isAlive() Checks if thread is Thread monitoring


still running

setName(), getName() Set or retrieve thread Debugging, labeling


name threads

setPriority(), Set or get priority Control execution


getPriority() order (suggestive)

yield() Give up CPU Cooperative


voluntarily multitasking

interrupt() Interrupts a Cancel or pause a


sleeping/waiting thread
thread

Thread Priorities

Analogy: In a queue, VIPs (high priority) are served first.


Range: 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY)
Default is 5 (NORM_PRIORITY)

public class PriorityDemo extends Thread {


public void run() {
System.out.println("Thread: " + Thread.currentThread().getName() +
" Priority: " + Thread.currentThread().getPriority());
}

Thread Synchronization

Problem:

When multiple threads attempt to access and modify shared data at the
same time, it can lead to errors and unpredictable behavior. This is
because each thread may interfere with the other's operations, leading to
conflicting changes and corrupted data.

Analogy:

Imagine two people trying to write in the same diary at the same time. One
person might write over the other's words, making it impossible to read or
understand what was written. This creates a chaotic and confusing
situation, similar to what happens when threads access shared resources
without coordination.

Solution:

To manage access to shared data and prevent these issues, you can use a
synchronized block or method. This approach ensures that only one
thread can access the critical section of code at a time, effectively locking
the resource until the thread has completed its task. This way, the data
remains consistent and free of conflicts..

Without Synchronization

class Counter {
int count = 0;
void increment() {
count++;
}
}

With Synchronization

class Counter {
int count = 0;

synchronized void increment() {


count++;
}

Inter-Thread Communication in
}

Java
What is Inter-Thread Communication?

Inter-thread communication in Java enables multiple threads to


coordinate and work together efficiently by sharing resources and
notifying each other about their execution states.

Why Do We Need Inter-Thread Communication?

🤔 Problem
Consider a scenario with two threads:

Producer Thread: Generates data.


Consumer Thread: Consumes data.

Without proper communication, the Consumer might attempt to access


data before the Producer has finished generating it, leading to the
consumption of incomplete or invalid data.

Solution

Java provides mechanisms for threads to communicate using wait(),


notify(), and notifyAll() to ensure:

The Producer can pause the Consumer using wait().


The Producer can notify the Consumer when data is ready using
notify().

Analogy

Imagine a Chef and a Waiter:

The Chef (Producer) prepares the food.


The Waiter (Consumer) waits until the food is ready.
Once ready, the Chef notifies the Waiter.

This communication ensures the correct order delivery, akin to threads


using wait() and notify().

⚙️ Methods Used for Inter-thread Communication


Method Description

wait() Causes the current thread to wait


until another thread calls notify()
or notifyAll() on the same object.

notify() Wakes up one waiting thread on


the same object.

notifyAll() Wakes up all waiting threads on


the same object.

🔒 Important:
These methods must be called within a synchronized block, or Java will
throw an IllegalMonitorStateException.

Code Example: Producer-Consumer Problem

class SharedResource {
private int data;
private boolean hasValue = false;

// Producer method
public synchronized void produce(int value) {
try {
while (hasValue) {
wait(); // Wait until the value is consumed
}
this.data = value;
System.out.println("Produced: " + data);
hasValue = true;
notify(); // Notify consumer
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// Consumer method
public synchronized void consume() {
try {
while (!hasValue) {
wait(); // Wait until the value is produced
}
System.out.println("Consumed: " + data);
hasValue = false;
notify(); // Notify producer
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class Producer extends Thread {


SharedResource resource;
Producer(SharedResource r) {
this.resource = r;
}
public void run() {
for (int i = 1; i <= 5; i++) {
resource.produce(i);
}
}
}

class Consumer extends Thread {


SharedResource resource;
Consumer(SharedResource r) {
this.resource = r;
}
public void run() {
for (int i = 1; i <= 5; i++) {
resource.consume();
}
}
}

public class InterThreadDemo {


public static void main(String[] args) {
SharedResource resource = new SharedResource();
Producer p = new Producer(resource);
Consumer c = new Consumer(resource);
p.start();
c.start();
}
}

🔐 Why Synchronization is Important?


wait(), notify(), and notifyAll() must be called from a synchronized context
because:

They work on the monitor lock of the object.


Without holding the lock, Java cannot determine which thread controls
access to the shared resource.

🛑 Common Mistakes and Exceptions


Mistake Exception

Calling wait() outside IllegalMonitorStateException


synchronized block

Forgetting to call notify() Thread waits forever (deadlock)

Using notify() when multiple Only one thread wakes up


threads are waiting

📊 Summary Table
Keyword Purpose Must Be Used In

wait() Pause current thread Synchronized block


and release lock

notify() Wake up one waiting Synchronized block


thread

notifyAll() Wake up all waiting Synchronized block


threads

Real-world Use Cases

Producer-Consumer problems (data queues)


Thread pools and task scheduling
Real-time communication between services

🧪 Advanced Tip (Optional for Beginners)


For advanced inter-thread communication, Java offers high-level APIs like:

BlockingQueue in java.util.concurrent
Semaphore, CountDownLatch, and CyclicBarrier
Inter-thread Communication (wait,
notify, notifyAll)
Analogy:

Imagine a busy restaurant where waiters are eagerly waiting for the chef to
announce that the “food is ready.” In this scenario, the chef's
announcement is equivalent to a notification in a multi-threaded program.

wait()
When a thread calls wait(), it voluntarily enters a waiting state. This is
akin to a waiter patiently standing by until the chef signals that the
food is prepared. The thread releases the lock it holds so that other
threads can proceed with their tasks. It remains inactive until it
receives a notification to resume its operations.
notify()
The notify() method is used to wake up a single thread that is in the
waiting state. In our restaurant analogy, this is like the chef calling
out to one specific waiter, letting them know that their order is ready
to be served. The notified thread will then reacquire the lock and
continue with its execution as soon as it gets the chance.
notifyAll()
Unlike notify(), the notifyAll() method wakes up all the threads that
are currently waiting. Returning to the restaurant example, this
would be similar to the chef announcing loudly to all the waiters that
multiple orders are ready, prompting all of them to spring into
action. Each thread will compete to reacquire the lock and proceed
with its task. This method is useful when multiple threads need to be
informed of a change in state.

class Shared {
synchronized void print() {
try {
wait();
System.out.println("Printing after notify...");
} catch (InterruptedException e) {
System.out.println(e);
}
}

synchronized void trigger() {


notify();
}
}

Daemon Threads

Analogy: Think of daemon threads as background cleaners. Their role is to


assist and support other threads, performing essential tasks quietly in the
background. However, their existence is tied to the main thread. When the
main thread finishes its work and exits, these daemon threads
automatically stop working and terminate, just like diligent cleaners who
finish their job when the main event is over.

public class DaemonDemo extends Thread {


public void run() {
if (Thread.currentThread().isDaemon())
System.out.println("Daemon thread");
else
System.out.println("User thread");
}

public static void main(String[] args) {


DaemonDemo d = new DaemonDemo();
d.setDaemon(true);
d.start();
}
}

Thread Group (Optional)

Organize threads into groups to manage them together.


public class GroupExample {
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("MyGroup");
Thread t1 = new Thread(tg, () -> System.out.println("Thread 1"));
Thread t2 = new Thread(tg, () -> System.out.println("Thread 2"));
t1.start();
t2.start();
System.out.println("Group Name: " + tg.getName());
}

Summary

Topic Key Idea Keyword/Method

Thread Basics Lightweight unit of Thread, Runnable


execution

Life Cycle States from creation getState()


to termination

Creating Threads Two approaches extends, implements

Thread Methods Control thread start(), join(), sleep()


behavior

Priority Influences thread setPriority()


scheduling

Synchronization Avoid data synchronized


inconsistency

Inter-thread Coordinate thread wait(), notify()


Communication activities

Daemon Threads Background service setDaemon(true)


threads
UNIT-III
Functional Interfaces
What is a Functional Interface?
A functional interface in Java is an interface with exactly one abstract method. It may contain
multiple default or static methods, but it can only have one abstract method.

Analogy

Consider a remote control with a single button—it performs just one task. Similarly, a
functional interface is designed to accomplish one primary function.

Syntax Example

@FunctionalInterface
interface MyInterface {
void display();
}

@FunctionalInterface Annotation

Explanation

The @FunctionalInterface annotation is not mandatory, but it is recommended. It serves as a


directive to the compiler, ensuring that the interface contains only one abstract method.

Analogy

Imagine labeling a folder as "Only One Document Allowed." If someone attempts to add more
than one document, they receive a warning.

Code Example 1 – Correct Use

@FunctionalInterface
interface Greeting {
void sayHello();
}

Code Example 2 – Compiler Error


@FunctionalInterface
interface Invalid {
void methodOne();
void methodTwo(); // ❌ Error: More than one abstract method
}

Why Functional Interfaces?

Functional interfaces are vital for using lambda expressions in Java (introduced in Java 8).
They allow you to pass functions as arguments, making your code more concise and
expressive.

Code Example

@FunctionalInterface
interface Calculator {
void calculate(int a, int b);
}

public class Test {


public static void main(String[] args) {
Calculator add = (a, b) -> System.out.println(a + b);
add.calculate(10, 20); // Output: 30
}
}

Lambda Expressions with Functional Interfaces

Explanation

Lambda expressions provide a concise way to write anonymous functions in Java.

Syntax

(parameters) -> expression

Code Example 1 – No Parameters

@FunctionalInterface
interface Hello {
void say();
}

public class Demo {


public static void main(String[] args) {
Hello h = () -> System.out.println("Hello, Java!");
h.say();
}
}

Code Example 2 – With Parameters

@FunctionalInterface
interface Operation {
void perform(int a, int b);
}

Operation multiply = (a, b) -> System.out.println("Product: " + (a * b));


multiply.perform(3, 4); // Output: Product: 12

Code Example 3 – With Return Type

@FunctionalInterface
interface Square {
int findSquare(int n);
}

Square s = (n) -> n * n;


System.out.println("Square: " + s.findSquare(5)); // Output: Square: 25

Built-In Functional Interfaces (java.util.function package)

These are predefined functional interfaces for common use cases:

Interface Abstract Method Purpose

Consumer accept(T t) Takes input, returns


nothing

Supplier get() Takes nothing, returns


result

Predicate test(T t) Returns true/false

Function apply(T t) Takes input, returns result

1 Consumer

import java.util.function.Consumer;
public class Demo {
public static void main(String[] args) {
Consumer<String> printer = (msg) -> System.out.println("Printing: " + msg);
printer.accept("Hello!");
}
}

2 Supplier

import java.util.function.Supplier;

public class Demo {


public static void main(String[] args) {
Supplier<Double> randomValue = () -> Math.random();
System.out.println("Random: " + randomValue.get());
}
}

3 Predicate

import java.util.function.Predicate;

public class Demo {


public static void main(String[] args) {
Predicate<String> isLong = (s) -> s.length() > 5;
System.out.println("Is 'HelloWorld' long? " + isLong.test("HelloWorld"));
}
}

4 Function<T, R>

import java.util.function.Function;

public class Demo {


public static void main(String[] args) {
Function<String, Integer> length = (s) -> s.length();
System.out.println("Length: " + length.apply("Lambda"));
}
}

Custom Functional Interfaces

Explanation

When none of the built-in interfaces meet your needs, you can create your own.
Example

@FunctionalInterface
interface Converter {
int convert(String s);
}

public class Demo {


public static void main(String[] args) {
Converter c = (str) -> Integer.parseInt(str);
System.out.println("Converted: " + c.convert("123"));
}
}

Summary Table

Concept Key Idea Example Interface

Functional Interface 1 abstract method only Custom or built-in

@FunctionalInterface Enforces functional rule See example above

Lambda Expression Short way to define ()->{}


method

Consumer Input only accept(T t)

Supplier Output only get()

Predicate Boolean result test(T t)

Function Input → Output apply(T t)

Note:
A lambda needs a functional interface.
Use @FunctionalInterface for safety.
Practice makes it easy to remember.
Focus on writing short, readable lambda code.
Three Ways to Use Functional
Interfaces in Java
Functional Interfaces in Java can be utilized in three primary ways. These methods allow you
to dynamically assign behavior, thus facilitating flexible and clean coding—especially when
employing lambda expressions and anonymous classes.

1. Using a Concrete Class that Implements the Functional


Interface
Explanation

This approach involves defining a class that implements the functional interface and
overrides its single abstract method. You then create an instance of this class to invoke the
method.

Analogy

Think of this like hiring a full-time employee to perform one specific task. You define their job,
hire them (create a class), and then ask them to work (call the method).

Example

Step 1: Define Functional Interface

@FunctionalInterface
interface MyInterface {
void display();
}

Step 2: Create Concrete Class

class MyClass implements MyInterface {


public void display() {
System.out.println("Display method implemented using concrete class.");
}
}

Step 3: Use in Main Method

public class Test {


public static void main(String[] args) {
MyInterface obj = new MyClass();
obj.display();
}
}

2. Using an Anonymous Inner Class


Explanation

Instead of creating a separate class file, you can define the implementation of the interface
inline using an anonymous inner class.

Analogy

This is akin to hiring a freelancer for a single task—no need to create a permanent role (class).
You define the job and assign it on the spot.

Example

@FunctionalInterface
interface MyInterface {
void display();
}

public class Test {


public static void main(String[] args) {
MyInterface obj = new MyInterface() {
public void display() {
System.out.println("Display method using anonymous class.");
}
};
obj.display();
}
}

3. Using Lambda Expression


Explanation

This is the most concise and modern method (introduced in Java 8) to use functional
interfaces. Lambdas provide a clean syntax to represent an interface’s single abstract
method.

Analogy
It's like sending a text command to get something done. You don't need to introduce yourself
or fill out a form; just send the instruction!

Syntax

(parameters) -> { method body }

Example

@FunctionalInterface
interface MyInterface {
void display();
}

public class Test {


public static void main(String[] args) {
MyInterface obj = () -> System.out.println("Display method using lambda.");
obj.display();
}
}

Comparison Table

Method Code Length Flexibility Use Case

Concrete Class Long Reusable When logic is


reused or code
must be organized

Anonymous Inner Medium Less Reusable One-time use;


Class intermediate
flexibility

Lambda Expression Short Highly Flexible Best for quick and


inline logic

When to Use Which?


Use Case Recommended Approach

You need to reuse logic Concrete Class

You want quick implementation Anonymous Inner Class

You want the cleanest, shortest code Lambda Expression

Java Features Overview


Java has evolved significantly over the years, offering a rich set of features that enhance
coding efficiency, readability, and functionality. Here’s a comprehensive look at some of the
key features introduced in recent versions of Java.

1.Lambda Expression
Concept:
Lambda expressions provide a simple and concise method to represent a single-
method interface through an expression, making it easier to embrace functional
programming principles. They allow you to treat functionality as method arguments
and to interpret code as data. This approach promotes more adaptable and reusable
code structures by encapsulating functionality in compact expressions.
Analogy:

Understanding Lambda
Expressions as Shortcuts
Consider a lambda expression as a quick shortcut. It’s similar to jotting down a quick
note instead of writing a long letter. Rather than developing a detailed method
complete with a name, return type, and modifiers, you can convey the same
functionality in just a single line. This enables you to create an anonymous function
without the intricacies of defining a full method, much like making a brief reminder
instead of crafting a comprehensive document.
Syntax:
parameters -> expression
The syntax parameters -> expression provides a clear definition of the parameters that
the lambda function will accept, as well as the operation it will execute. The parameters
are presented on the left side of the arrow, whereas the corresponding expression or
code block that carries out the functionality is positioned on the right.
Steps to Create a Lambda Expression:
a. Identify the Functional Interface:
Determine which interface you want to implement using a lambda expression. It
should be an interface with a single abstract method, often referred to as a
Functional Interface. Examples include Runnable, Callable, Comparator, etc.
b. Determine the Parameters:
Decide the number and type of parameters the lambda expression will take based
on the abstract method of the functional interface. This step involves
understanding what input the method requires to perform its task.
c. Define the Expression:
Write the expression or block of code that implements the method's functionality.
This is where you specify what the lambda should do with the given parameters.
d. Combine Components:
Use the syntax parameters -> expression to form the complete lambda expression.
This step involves combining the identified parameters and the expression into a
single, cohesive lambda.

Example:

List<String> names = Arrays.asList("John", "Jane", "Jake");


names.forEach(name -> System.out.println(name));
In this instance, the lambda expression name -> System.out.println(name) is utilized to
traverse the list and print each name. This eliminates the need for a conventional loop
and a separate method, simplifying the code by directly tying the action to the list
iteration.
Use Case:
Lambdas are especially beneficial for minimizing boilerplate code associated with
small functional interfaces such as Runnable, Comparator, and event listeners. They
allow for cleaner and more readable code, particularly when you need quick, on-the-
fly implementations of interfaces with a single method.

2. Method References

Concept:
Method references provide a concise way to represent a lambda expression when
calling a method. They allow you to directly refer to methods by their names, which
enhances the readability of your code.
Analogy:
Think of method references as similar to a telephone speed dial. Instead of entering
the entire number each time, you simply press a button to connect instantly.
Example:
Let's consider a basic program example to illustrate method references:
import java.util.Arrays;
import java.util.List;
public class MethodReferenceExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// Using a method reference to print each name


names.forEach(System.out::println);
}
}
In this program, the method reference System.out::println is used to print each name
in the list, achieving the same outcome as a lambda expression but with a more
streamlined approach.
Types:
Method references can take various forms, including static, instance, and constructor
references.

3.Stream API
Concept:
The Stream API is a robust tool designed for processing sequences of elements. It
facilitates declarative data processing through functional programming-style
operations, enabling efficient management of collections. This allows developers to
write clean and efficient code by abstracting the iteration over data. Streams provide
a level of abstraction that allows developers to focus on the logic of data processing
rather than on the details of iteration.
Analogy:
Picture a stream of water flowing through various filters and pipes. Each filter
symbolizes an operation that modifies the water in some way, much like how stream
operations transform data. Just as all water passes through the filters in a sequence,
data flows through a series of operations in a Stream. This analogy highlights the
continuous and sequential nature of stream processing, where each operation
depends on the output of the previous one to refine or change the data further.
Example:
List<String> namesWithJ = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
In this example, the filter operation generates a new list that includes only the names
beginning with "J". This demonstrates how streams can simplify operations on
collections. The power of streams lies in their ability to chain multiple operations,
such as filtering, mapping, and sorting, thus providing a fluent and readable coding
style.
Use Case:
Streams are particularly well-suited for tasks that require filtering, mapping, and
reducing collections of data. They promote more expressive and concise code, making
it easier to perform complex transformations and aggregations without extensive
boilerplate. By using streams, developers can efficiently handle large datasets with
minimal overhead. Streams are also beneficial in parallel processing, where operations
can be easily parallelized to leverage multi-core processors, thereby enhancing
performance and reducing processing time.

4. Default Methods (Interfaces)


Concept: Default methods allow interfaces to have method implementations. This means
you can add new methods to interfaces without breaking existing implementations of
those interfaces.
Analogy: Think of a default method in an interface like a recipe book that comes with
default instructions. If you don't have your own recipe, you can fall back on the default.
Example:
interface MyInterface {
default void show() {
System.out.println("Default Implementation");
}
}
This interface provides a default implementation for the show method, which can be
used by any class that implements MyInterface.

5. Static Methods (Interfaces)


Concept: Interfaces in Java can now include static utility methods. This allows interfaces
to serve as a container for related methods, similar to how utility classes work.
Example:
interface Utility {
static int add(int a, int b) {
return a + b;
}
}
Here, add is a static method in the Utility interface that can be called without an instance
of the interface.

6. Base64 Encode and Decode

Concept:
Base64 encoding and decoding are methods used to convert binary data into a text
format. This transformation is crucial because many systems and protocols, such as
email or web APIs, are designed to handle text rather than binary data. By encoding
binary data into a text representation, it becomes possible to transmit data over these
text-based channels without loss or corruption.
The process of Base64 encoding involves dividing the input data into groups of three
bytes, which are then split into four 6-bit groups. Each of these 6-bit groups is mapped to
a character in the Base64 alphabet. This alphabet consists of 64 characters, including
uppercase and lowercase letters, digits, and symbols like + and /. The resulting encoded
string is generally longer than the original binary data, typically by about 33%.
When decoding, the Base64 algorithm reverses this process. It takes the encoded string,
maps each character back to its corresponding 6-bit value, and then combines these 6-
bit groups to recreate the original binary data. Padding characters, usually =, are used to
ensure the encoded string's length is a multiple of four.

Example:

String encoded = Base64.getEncoder().encodeToString("hello".getBytes());


String decoded = new String(Base64.getDecoder().decode(encoded));

In this Java example, the string "hello" is first converted into a byte array using the
getBytes() method. The Base64.getEncoder().encodeToString() method then encodes this
byte array into a Base64 string. This encoded string is safe for transmission over text-
based mediums. To retrieve the original string, the Base64.getDecoder().decode() method
is used, which decodes the Base64 string back into the original byte array. Finally, this
byte array is converted back into a string with the new String() constructor, resulting in
the original "hello" string. This demonstrates how Base64 encoding and decoding work
seamlessly to preserve data integrity across text-based systems.

7.ForEach Method
Concept:

The forEach method is a feature in Java that allows you to iterate over each element of a
collection, such as a list or set, using a lambda expression. This method simplifies the process
of executing a specific operation on each element within the collection. By using forEach, you
can avoid writing traditional for loops, making your code more concise and readable.

Basic Program 1: Print Each Item in a List

Here's a simple program demonstrating how to use the forEach method to print each item in
a list:

import java.util.Arrays;
import java.util.List;

public class ForEachExample {


public static void main(String[] args) {
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
list.forEach(item -> System.out.println(item));
}
}

Explanation:

We create a list of strings containing "Apple", "Banana", and "Cherry".


The forEach method is called on the list, with a lambda expression that prints each item.
The System.out.println(item) operation is applied to each element of the list, resulting in
each fruit name being printed on a new line.

Basic Program 2: Square Each Number in a List

This example demonstrates using the forEach method to perform a calculation on each
element in a list of numbers:

import java.util.Arrays;
import java.util.List;

public class SquareNumbers {


public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(number -> {
int squared = number * number;
System.out.println(squared);
});
}
}

Explanation:

We create a list of integers from 1 to 5.


The forEach method is used to iterate over each number in the list.
For each number, we calculate its square by multiplying it by itself.
The squared result is then printed out, showing the squares of the numbers 1 through 5.

8.Try-with-Resources
Concept:
The try-with-resources statement streamlines resource management by ensuring that every
resource is closed at the end of the statement. This feature removes the necessity for explicit
finally blocks dedicated to closing resources.

Analogy:
Think of try-with-resources like a smart fridge that automatically shuts its door. You simply
take what you need, and the fridge takes care of the rest.

Example:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
System.out.println(br.readLine());
}

In this instance, the BufferedReader is automatically closed once the try block is exited.

9. Type Annotations

Concept:
Type annotations are a feature in programming languages that allow annotations to be
applied directly to type uses in code. They extend the range of scenarios in which
annotations can be utilized, beyond just declarations. By using type annotations,
developers can enhance the precision and capabilities of type-checking mechanisms.
This, in turn, leads to the creation of more robust and error-resistant code. Type
annotations enable developers to specify constraints or conditions on variables,
parameters, and return types, making it easier to identify and prevent potential issues at
compile time.

Example:

@NonNull String name;


In this example, the @NonNull annotation is applied to the String type of the variable
name. This annotation explicitly indicates that name must never be assigned a null value.
By enforcing this constraint, the compiler can automatically detect any violations of this
rule during compile time, thereby helping developers catch potential null-pointer
exceptions before the code is executed. This proactive error-checking mechanism
contributes to writing safer and more reliable code by preventing common bugs
associated with null values.

10. Repeating Annotations


Concept: Repeating annotations allow the same annotation to be applied multiple times
to a single element, providing greater flexibility in code annotation.
Example:
@Hint("hint1")
@Hint("hint2")
class Demo {}
Here, the Demo class is annotated with two Hint annotations, each providing different
information.
11. Java Module System (JPMS -
Java 9)
Concept:
The Java Module System (JPMS), introduced in Java 9, is a powerful framework that organizes
code into distinct modules. Each module encapsulates its code and data, specifying its
requirements and what it offers to other modules. This modular approach enhances
maintainability by making the application easier to manage and understand. It also bolsters
security by controlling access to internal APIs and reducing unintended interactions.
Additionally, JPMS improves performance through more efficient memory usage and faster
application startup times, as only necessary modules are loaded. It supports scalable
application development by allowing teams to work independently on different modules.

Analogy:
Think of the module system as a sophisticated set of Lego pieces, where each piece has
precise connection points. This design ensures that the pieces fit together in a predetermined
manner, resulting in a well-structured and cohesive model. Just as Lego pieces can be
combined to create complex structures, modules in JPMS can be assembled into larger
applications. Each module clearly defines its dependencies and interfaces, ensuring modular
integrity and clarity. This approach is like constructing a detailed architectural model, where
each piece plays a specific role in forming the final structure, ensuring everything fits
perfectly without gaps or overlaps.

Example:

module com.example.mymodule {
requires java.sql;
exports com.example.service;
}

In this example, the module com.example.mymodule specifies a dependency on the java.sql


module, indicating that it uses functionalities provided by the SQL module. It also exports the
com.example.service package, making it accessible to other modules. This clear delineation of
dependencies and exposed packages ensures controlled and predictable module
interactions. By specifying these requirements and exports, developers can prevent
accidental usage of internal packages, promoting cleaner code and reducing runtime errors.
This methodical organization also simplifies testing and debugging since each module can be
verified independently.

Basic Program Example:


// Module declaration in module-info.java
module com.example.app {
requires java.base;
exports com.example.app.main;
}

// Main class in the module


package com.example.app.main;

public class Main {


public static void main(String[] args) {
System.out.println("Welcome to the modular world of Java!");
}
}

In this basic program, a module named com.example.app is declared, requiring the


java.base module, which is implicitly required by all modules. It exports the
com.example.app.main package, allowing other modules to access it. The Main class
contains a simple main method that prints a welcome message, demonstrating how a
modular Java application can be structured.

12.Diamond Syntax with Anonymous Classes

Explanation:
The diamond syntax (<>) is a feature in Java that simplifies the instantiation of
parameterized types by allowing the compiler to automatically infer the type parameters.
Initially introduced with Java 7 for generic classes, it was later extended in Java 9 to
support anonymous inner classes, making the code more concise and readable. When
using diamond syntax with anonymous classes, the compiler deduces the type
parameters based on the context in which the class is used, eliminating the need to
explicitly specify them.

Program Example:

import java.util.function.Consumer;

public class DiamondSyntaxExample {


public static void main(String[] args) {
Consumer<String> consumer = new Consumer<>() {
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("Hello, World!");
}
}

In this program, the diamond operator is used with an anonymous class implementing the
Consumer interface, demonstrating how type inference results in cleaner code.

13.Inner Anonymous Class

Explanation:
Inner anonymous classes are a common feature in Java where a class is defined without a
name and is typically used for ad-hoc, one-time use cases like event handling. These classes
are declared and instantiated in a single expression and can extend a class or implement
interfaces. They are especially useful when a single-use subclass or implementation is
required, avoiding the overhead of creating a new named class.

Program Example:

public class AnonymousClassExample {


public static void main(String[] args) {
Runnable r = new Runnable() {
public void run() {
System.out.println("Running...");
}
};
r.run();
}
}

This program demonstrates the creation and use of an anonymous class that implements the
Runnable interface to run a simple task.

14.Local Variable Type Inference (var)

Explanation:
Introduced in Java 10, local variable type inference allows developers to declare local
variables without specifying their exact type by using the var keyword. The compiler infers
the type from the initializer, leading to clearer and more maintainable code. It’s particularly
useful in scenarios where the type is either obvious or verbose.

Program Example:
import java.util.ArrayList;

public class VarExample {


public static void main(String[] args) {
var list = new ArrayList<String>();
list.add("Hello");
list.add("World");
for (var item : list) {
System.out.println(item);
}
}
}

This program uses var to declare a list, demonstrating how the type ArrayList<String> is
inferred by the compiler.

15 Switch Expressions

In-Depth Explanation:
Switch expressions, introduced in Java 12 as a preview feature and standardized in Java 14,
enhance the traditional switch statement, allowing it to return a value. Using the -> syntax
and yield keyword, switch expressions make code more concise and expressive. They reduce
boilerplate and improve readability by enabling a more functional style of programming.

Program Example:

public class SwitchExpressionExample {


public static void main(String[] args) {
String day = "MONDAY";
String result = switch (day) {
case "MONDAY" -> "Start of week";
case "FRIDAY" -> "End of week";
default -> "Midweek";
};
System.out.println(result);
}
}

This program uses a switch expression to assign a string based on the value of the day
variable.

16.Yield Keyword
Explanation:
The yield keyword is used within switch expressions to return a value from a switch block.
It clarifies the intention of returning a value, which is particularly useful in more complex
switch expressions where multiple statements are involved. This addition enhances code
readability and maintainability by clearly indicating the value being returned from a case
block.

Program Example:

public class YieldExample {


public static void main(String[] args) {
int hour = 9;
String mood = switch (hour) {
case 9 -> {
yield "Morning";
}
default -> "Later";
};
System.out.println(mood);
}
}

In this program, yield is used to return a string based on the value of hour, showcasing its use
within a switch expression.

17.Text Blocks

Explanation:
Text blocks, introduced in Java 13, provide a way to define multiline strings using triple
quotes ("""). This feature simplifies writing strings that span multiple lines, as it removes the
need for escape sequences and concatenation. Text blocks improve code readability and
maintainability, especially for JSON, XML, and HTML content.

Program Example:

public class TextBlockExample {


public static void main(String[] args) {
String json = """
{
"name": "John",
"age": 30
}
""";
System.out.println(json);
}
}
This example demonstrates how a JSON string can be formatted using a text block for
cleaner and more readable code.

18.Record

Explanation:
Records, introduced in Java 14, offer a concise way to declare data-carrying classes.
By declaring a class as a record, the compiler automatically generates methods like
toString, equals, and hashCode, reducing boilerplate code. Records are immutable by
default and are ideal for modeling simple data aggregates.
Program Example:
public class RecordExample {
record Person(String name, int age) {}

public static void main(String[] args) {


Person person = new Person("John", 30);
System.out.println(person);
}
}
This program defines a Person record, automatically providing a constructor, getters,
and common method overrides.

19.Sealed Classes

Explanation:
Sealed classes, introduced in Java 15, restrict which classes can extend them, providing a
controlled and secure class hierarchy. They enhance maintainability and security by
allowing the developer to specify a finite set of subclasses, which can be useful in domain
modeling and API design.

Program Example:

public class SealedClassesExample {


sealed class Shape permits Circle, Square {}

final class Circle extends Shape {}


final class Square extends Shape {}

public static void main(String[] args) {


Shape shape1 = new Circle();
Shape shape2 = new Square();
System.out.println("Shapes created: " + shape1.getClass().getSimpleName() + ", " +
shape2.getClass().getSimpleName());
}
}
This example demonstrates a sealed class Shape that only allows Circle and Square to
extend it, ensuring a controlled inheritance structure.
UNIT-IV
Introduction to Collections
What are Collections?
A Collection is a group of individual objects represented as a single unit. Imagine it as a
basket containing various fruits like apples, bananas, and oranges. Instead of managing each
fruit separately, you manage the entire basket.

In programming, collections are a crucial concept. They provide a way to group multiple items
together, making it easier to manage and manipulate data. Collections can be homogeneous
(all elements of the same type) or heterogeneous (elements of different types). They allow for
efficient data storage and retrieval, providing a way to handle dynamic data structures that
can grow or shrink as needed. Collections also offer a range of utility methods for operations
like searching, sorting, and filtering.

Why Collections?
Arrays in Java are fixed in size and do not support many utility methods. Collections, on the
other hand, are resizable, type-safe, and versatile.

Collections overcome the limitations of arrays by providing dynamic data structures that can
adjust their size automatically. They offer a more flexible and powerful way to handle data,
with built-in methods for common operations like adding, removing, and searching for
elements. Collections also offer type safety, ensuring that only elements of a specified type
can be added, reducing runtime errors and improving code reliability.

int[] arr = new int[5]; // fixed size


ArrayList<Integer> list = new ArrayList<>(); // dynamic size

Collection Framework
Architecture
Key Interfaces
Collection: The root interface from which other interfaces like List, Set, and Queue
extend.
List, Set, Queue: These extend the Collection interface and provide specific
functionalities.
Map: A separate root interface that deals with key-value pairs.

Collection Framework Overview


The Collection Framework in Java provides a unified architecture for handling collections. It
includes a set of interfaces, implementations (classes), and algorithms, which offer powerful
capabilities for storing, retrieving, and manipulating data.

Components
1. Interfaces: Define the abstract data types that are implemented by various collection
classes.
Interfaces establish the foundation and contract that implementations must follow,
ensuring consistency and interoperability across different collection types.
2. Implementations (Classes): Concrete classes that implement the interfaces.
These classes provide the actual data structures and algorithms for storing and
managing data. They vary in performance characteristics and capabilities, such as
dynamic array handling and linked list operations.
3. Algorithms: Methods that perform operations like sorting and searching on collections.
Algorithms are static methods that operate on collections, providing utilities for data
manipulation without altering the underlying data structures.

Collection Hierarchy Diagram


Iterable (interface)
|
----------------
| |
Collection Map (not under Collection)
|
-------------------
| | |
List Set Queue

Note: Map is not a part of the Collection interface but is part of the framework.

The Root Interface: Iterable and Collection


Iterable

The root interface for all collections, providing a way to traverse through elements.
Enables the use of the enhanced for-loop, simplifying iteration over collections by
abstracting the complexity of iterator management.

List<String> items = List.of("A", "B", "C");


for (String item : items) {
System.out.println(item);
}

Collection

Extended by List, Set, and Queue interfaces.


Provides basic methods like add(), remove(), and size(), forming the foundation for more
specific data structures.

Interface: List (Ordered + Duplicates Allowed)


Implementations: ArrayList, LinkedList, Vector, Stack

ArrayList: Ideal for dynamic arrays with fast random access.


Best suited for scenarios where frequent read operations are necessary due to its
efficient indexing.
LinkedList: Efficient for frequent insertions/deletions.
Preferred in situations where data is frequently added or removed from the list, as it
doesn't require resizing or shifting elements.
Vector: Synchronized and thread-safe.
Suitable for applications where thread safety is a concern, though its performance
may be slower due to synchronization overhead.
Stack: LIFO (Last In, First Out) behavior.
Utilized for managing data with a stack discipline, such as parsing expressions or
backtracking algorithms.

Examples

ArrayList Example

List<String> fruits = new ArrayList<>();


fruits.add("Apple");
fruits.add("Banana");
System.out.println(fruits.get(1)); // Outputs: Banana

LinkedList Example

LinkedList<String> ll = new LinkedList<>();


ll.add("Monday");
ll.add("Tuesday");
System.out.println(ll);
Vector Example

Vector<Integer> v = new Vector<>();


v.add(10);
v.add(20);
System.out.println(v);

Stack Example

Stack<Integer> s = new Stack<>();


s.push(1);
s.push(2);
System.out.println(s.pop()); // Outputs: 2

Interface: Set (No Duplicates, Unordered)


Implementations: HashSet, LinkedHashSet, TreeSet

HashSet: No duplicates, no guaranteed order.


Best used for fast access and retrieval of unique items without concern for order.
LinkedHashSet: Maintains insertion order.
Ideal for cases where the order of elements needs to be preserved alongside
uniqueness.
TreeSet: Sorted order.
Useful when a sorted collection of unique elements is required, supporting efficient
navigation and range operations.

Examples

HashSet Example

Set<String> hs = new HashSet<>();


hs.add("A");
hs.add("B");
hs.add("A");
System.out.println(hs); // Outputs: [A, B] - no duplicate

LinkedHashSet Example

Set<String> lhs = new LinkedHashSet<>();


lhs.add("A");
lhs.add("C");
System.out.println(lhs);

TreeSet Example
Set<Integer> ts = new TreeSet<>();
ts.add(30);
ts.add(10);
ts.add(20);
System.out.println(ts); // Outputs: [10, 20, 30]

Interface: Queue (FIFO - First In First Out)


Implementations: PriorityQueue, LinkedList

PriorityQueue: Elements are ordered based on their natural ordering or by a comparator.


Suitable for scenarios where elements need to be processed based on priority rather
than insertion order.

Example

PriorityQueue Example

Queue<Integer> pq = new PriorityQueue<>();


pq.add(40);
pq.add(10);
pq.add(20);
System.out.println(pq.poll()); // Outputs: 10 (smallest)

Interface: Map (Key-Value Pair, Not under Collection)


Implementations: HashMap, TreeMap, LinkedHashMap

HashMap: Unordered, allows null values and keys.


Offers constant-time performance for basic operations and is ideal for general-
purpose use, where order is not a concern.
TreeMap: Sorted by keys.
Provides a naturally ordered map, useful for applications that require sorted key-value
pairs.
LinkedHashMap: Maintains insertion order.
Combines the benefits of hash map performance with predictable iteration order,
often used in caching applications.

Examples

HashMap Example

Map<Integer, String> map = new HashMap<>();


map.put(1, "A");
map.put(2, "B");
System.out.println(map.get(1)); // Outputs: A
TreeMap Example

Map<Integer, String> tmap = new TreeMap<>();


tmap.put(20, "Z");
tmap.put(10, "Y");
System.out.println(tmap); // Sorted keys

LinkedHashMap Example

Map<Integer, String> lhm = new LinkedHashMap<>();


lhm.put(1, "One");
lhm.put(2, "Two");
System.out.println(lhm);

Utility Methods from Collections Class


Collections.sort(list): Sorts the list in natural order.
Provides a convenient method to sort elements of a list into their natural ordering.
Collections.reverse(list): Reverses the order of the list.
Flips the order of elements, helpful in reversing the iteration order.
Collections.shuffle(list): Randomly permutes the list.
Shuffles the list elements randomly, useful for creating a randomized sequence.

List<Integer> list = Arrays.asList(3, 1, 2);


Collections.sort(list);
System.out.println(list); // Outputs: [1, 2, 3]

Summary Table

Interface Implementation Features

List ArrayList, LinkedList, Ordered, allows duplicates


Vector, Stack

Set HashSet, TreeSet, No duplicates,


LinkedHashSet unordered/sorted

Queue PriorityQueue, LinkedList FIFO

Map HashMap, TreeMap, Key-Value pair, no


LinkedHashMap duplicates in keys
Important Classes
ArrayList, LinkedList: Implementations of the List interface.
HashSet, TreeSet: Implementations of the Set interface.
HashMap, TreeMap: Implementations of the Map interface.

Analogy

Think of Collections like a toolbox:

List: Ordered tools, like a drawer with tools arranged in a specific order.
Set: Unique tools, no duplicates allowed, like a tool rack where each tool is distinct.
Map: Labeled drawers, key-value pairs, where each drawer has a label (key) and contains
a tool (value).

List Interface
Features
Ordered collection
Allows duplicates
Access by index

The List interface represents an ordered collection of elements. It allows duplicate elements
and maintains the order of insertion. Elements in a list can be accessed by their integer index,
which provides fast access to any element in the list. The List interface is implemented by
classes like ArrayList and LinkedList, each offering different performance characteristics.

Implementations
ArrayList

An ArrayList is a resizable array that allows for fast random access due to its underlying array
structure.

ArrayList is implemented using an array that grows dynamically as elements are added. It
provides constant-time access to elements by index, making it ideal for scenarios where
frequent access is required. However, inserting or removing elements from the middle of the
list can be costly because it requires shifting elements.

import java.util.*;

public class ListDemo {


public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
System.out.println(list); // Outputs: [Java, Python, C++]
}
}

LinkedList

A LinkedList is a doubly-linked list implementation that allows for constant-time insertions or


removals using iterators.

LinkedList consists of nodes where each node contains data and references to the previous
and next node. This structure allows for efficient insertions and deletions, especially at the
beginning or end of the list. However, accessing elements by index is slower compared to
ArrayList because it requires traversing the list from the beginning or end.

List<String> ll = new LinkedList<>();


ll.add("One");
ll.add("Two");
ll.addFirst("Zero");
System.out.println(ll); // Outputs: [Zero, One, Two]

Set Interface
Features
No duplicates
Unordered (HashSet), Ordered (LinkedHashSet), Sorted (TreeSet)

The Set interface represents a collection that does not allow duplicate elements. It models
mathematical sets and provides methods to perform set operations like union, intersection,
and difference. Different implementations of Set provide different ordering guarantees, from
no order (HashSet) to insertion order (LinkedHashSet) and sorted order (TreeSet).

Implementations
HashSet

A HashSet is a collection that does not allow duplicate elements and does not guarantee any
specific order of elements.

HashSet is backed by a hash table, which provides constant-time performance for basic
operations like add, remove, and contains. However, it does not maintain any order of
elements, making it unsuitable for scenarios where order is important.

Set<String> set = new HashSet<>();


set.add("Apple");
set.add("Banana");
set.add("Apple"); // Duplicate ignored
System.out.println(set); // No order guaranteed

LinkedHashSet

A LinkedHashSet maintains a linked list of the entries in the set, thereby maintaining the
insertion order.

LinkedHashSet extends HashSet and maintains a doubly-linked list of its elements, which
defines the iteration order. This means that elements are returned in the order they were
inserted. It provides a combination of the features of HashSet and a linked list.

Set<String> set = new LinkedHashSet<>();


set.add("A");
set.add("B");
set.add("C");
System.out.println(set); // Maintains insertion order

TreeSet

A TreeSet is a collection that stores elements in a sorted (natural order), based on their
values.

TreeSet is backed by a TreeMap and stores elements in a sorted order, either natural or
provided by a comparator. It provides guaranteed log(n) time cost for the basic operations
(add, remove, and contains). It is ideal when you need to maintain a sorted order of elements.

Set<Integer> set = new TreeSet<>();


set.add(30);
set.add(10);
set.add(20);
System.out.println(set); // Sorted: [10, 20, 30]

Map Interface
Features
Key-value pairs
Keys are unique; values can be duplicated
The Map interface represents a collection of key-value pairs, where each key is unique. It
provides methods for basic operations, such as adding, removing, and retrieving values based
on keys. Maps are useful for associating unique keys with specific values, similar to a
dictionary.

Implementations
HashMap

A HashMap stores key-value pairs and does not maintain any order of keys.

HashMap is implemented using a hash table and provides constant-time performance for the
basic operations (get and put). It allows null values and the null key. However, it does not
guarantee the order of iteration, which may change over time.

Map<Integer, String> map = new HashMap<>();


map.put(1, "Java");
map.put(2, "Python");
System.out.println(map.get(1)); // Outputs: Java

TreeMap

A TreeMap stores key-value pairs in a sorted order of keys.

TreeMap is a red-black tree-based implementation of the Map interface. It maintains the keys
in a sorted ascending order, according to their natural ordering or by a comparator provided
at map creation time. It provides log(n) time cost for the basic operations (get, put, and
remove).

Map<String, Integer> map = new TreeMap<>();


map.put("C", 30);
map.put("A", 10);
map.put("B", 20);
System.out.println(map); // Sorted by key: {A=10, B=20, C=30}

Vector Class
Features
Synchronized, thread-safe
Can grow as needed

The Vector class is similar to ArrayList, but it is synchronized, making it thread-safe. This
means that it is safe to use in a multi-threaded environment without additional
synchronization. However, this synchronization overhead makes Vector slower than ArrayList
for single-threaded applications.

Vector Example

import java.util.*;

public class VectorDemo {


public static void main(String[] args) {
Vector<String> vector = new Vector<>();
vector.add("Red");
vector.add("Green");
vector.add("Blue");
System.out.println(vector); // Outputs: [Red, Green, Blue]
}
}

Iterating Collections
For-each Loop
Iterating over a collection using a for-each loop is simple and concise.

The for-each loop provides a simple syntax for iterating over collections. It is particularly
useful when you do not need access to the index of the elements. However, it does not allow
you to modify the collection (remove elements) during iteration.

List<String> list = Arrays.asList("Java", "Python", "C++");


for (String lang : list) {
System.out.println(lang);
}

Iterator
Using an Iterator allows safe removal during iteration.

An Iterator provides a way to traverse a collection and optionally remove elements during the
iteration. It offers methods like hasNext(), next(), and remove(), making it flexible and powerful
for modifying collections during traversal.

Iterator<String> itr = list.iterator();


while(itr.hasNext()) {
System.out.println(itr.next());
}
Common Collection Methods
List Methods
get(int index): Retrieves the element at the specified index.
set(int index, E element): Replaces the element at the specified position.
remove(Object o): Removes the first occurrence of the specified element.
size(): Returns the number of elements.

In-Depth Explanation

The List interface provides several methods for manipulating elements. The get and set
methods allow for accessing and modifying elements by their index. The remove method
allows for removing elements, and size returns the number of elements, helping manage list
contents effectively.

list.get(0);
list.set(1, "Ruby");
list.remove("Java");
list.size();

Set Methods
contains(Object o): Returns true if the set contains the specified element.
remove(Object o): Removes the specified element.

In-Depth Explanation

The Set interface provides methods to check for the presence of elements (contains) and to
remove elements (remove). These methods help maintain the uniqueness of elements in a set
and perform set operations efficiently.

set.contains("Apple");
set.remove("Banana");

Map Methods
containsKey(Object key): Checks if the map contains a key.
remove(Object key): Removes the mapping for a key.
keySet(): Returns a set of the keys.

In-Depth Explanation

The Map interface provides methods for interacting with key-value pairs. containsKey checks
for the existence of a key, remove deletes a key-value pair, and keySet returns a set of all keys,
enabling efficient management of mappings.

map.containsKey(1);
map.remove(2);
map.keySet();

Sorting Collections
Collections.sort()
The Collections.sort() method can sort a list of elements that implement the Comparable
interface.

In-Depth Explanation

Collections.sort() is a utility method that sorts a list in natural order. It requires that the
elements of the list implement the Comparable interface. For custom sorting, you can
provide a Comparator to the sort method, allowing flexibility in sorting criteria.

List<Integer> nums = Arrays.asList(5, 1, 3);


Collections.sort(nums);
System.out.println(nums); // Outputs: [1, 3, 5]

Introduction to Queue
What is a Queue?
A Queue is a linear data structure that follows the FIFO (First-In-First-Out) order. This means
the first element added to the queue will be the first to be removed, similar to a line at a
ticket counter where the person who arrives first gets served first.

Code Example: Simple Queue Implementation

import java.util.Queue;
import java.util.LinkedList;

public class SimpleQueueExample {


public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.add("Person1");
queue.add("Person2");
queue.add("Person3");
System.out.println(queue); // Outputs: [Person1, Person2, Person3]
}
}

Why Use Queues?


Queues are useful in various applications, such as CPU scheduling, buffer management, and
printer spooling. They help manage data in a fair and ordered manner, ensuring that each
element is processed in the order it was added.

Code Example: Queue in CPU Scheduling

import java.util.LinkedList;
import java.util.Queue;

class Process {
String name;

Process(String name) {
this.name = name;
}

void execute() {
System.out.println(name + " is executing.");
}
}

public class CPUSchedulingExample {


public static void main(String[] args) {
Queue<Process> processQueue = new LinkedList<>();
processQueue.add(new Process("Process1"));
processQueue.add(new Process("Process2"));
processQueue.add(new Process("Process3"));

while (!processQueue.isEmpty()) {
Process currentProcess = processQueue.poll();
if (currentProcess != null) {
currentProcess.execute();
}
}
}
}
Queue Interface (java.util.Queue)
The Queue is an interface in Java under the java.util package. It provides a blueprint for
classes to implement queue behavior.

Common Classes Implementing Queue

LinkedList: Implements both List and Queue interfaces.


PriorityQueue: Orders elements based on their natural order or a specified comparator.
ArrayDeque: Implements Deque interface and supports insertion and removal from both
ends.

Java Program: Demonstrating


Queue Interface Operations
In this program, we demonstrate various operations available in Java's
Queue interface using a LinkedList implementation. A queue is a linear
data structure that follows the FIFO (First In, First Out) principle, meaning
the first element added will be the first one removed.

import java.util.*;

public class QueueDemo {


public static void main(String[] args) {
// Create a queue using LinkedList
Queue<String> queue = new LinkedList<>();

// 1. Add elements to the queue using add() and offer()


queue.add("A"); // Throws exception if it fails
queue.offer("B"); // Returns false if it fails
queue.add("C");
queue.offer("D");

System.out.println("Initial Queue: " + queue);

// 2. Access head of the queue using element() and peek()


System.out.println("\nHead (element()): " + queue.element()); //
Throws exception if empty
System.out.println("Head (peek()): " + queue.peek()); // Returns
null if empty

// 3. Remove elements from the queue using remove() and poll()


System.out.println("\nRemoved (remove()): " + queue.remove()); //
Throws exception if empty
System.out.println("Queue after remove(): " + queue);

System.out.println("Removed (poll()): " + queue.poll()); // Returns


null if empty
System.out.println("Queue after poll(): " + queue);

// 4. Check size of the queue


System.out.println("\nSize of queue: " + queue.size());

// 5. Check if queue is empty


System.out.println("Is queue empty? " + queue.isEmpty());

// 6. Iterate through queue


System.out.println("\nIterating over queue:");
for (String item : queue) {
System.out.println(item);
}

// 7. Clear the queue


queue.clear();
System.out.println("\nQueue after clear(): " + queue);
System.out.println("Is queue empty now? " + queue.isEmpty());
}
}

Explanation of Key Methods

Here is an explanation of the key methods used in the queue operations:


Method Description

add(E e) Inserts the element and throws an


exception if the queue is full.

offer(E e) Inserts the element and returns


false if the queue is full.

element() Retrieves, but does not remove,


the head of the queue. Throws an
exception if empty.

peek() Retrieves, but does not remove,


the head of the queue. Returns
null if empty.

remove() Retrieves and removes the head


of the queue. Throws an exception
if empty.

poll() Retrieves and removes the head


of the queue. Returns null if
empty.

clear() Removes all elements from the


queue.

This example highlights how to effectively utilize the queue interface and
its methods to manage data in a FIFO manner. Each operation is
demonstrated with clear examples, providing a comprehensive
understanding of queue operations in Java.

Key Queue Operations


Method Description

add() Inserts element, throws exception if full

offer() Inserts element, returns false if full

remove() Removes head, throws exception if empty

poll() Removes head, returns null if empty

element() Returns head, throws exception if empty

peek() Returns head, returns null if empty

LinkedList as a Queue
The LinkedList class in Java is a versatile data structure that implements
both the List and Queue interfaces. This flexibility allows it to act as a
queue where elements are inserted at the end and removed from the
beginning. Additionally, LinkedList can function as a double-ended queue
(Deque), enabling insertion and removal operations at both ends.

In-Depth Explanation of Operations with Code Example


1. Initialization

To use a LinkedList as a queue, we declare and initialize it using the Queue interface. This
ensures that we are using the queue-specific methods.

Queue<Integer> q = new LinkedList<>();

Here, q is a queue that holds integer elements. The LinkedList object is created and assigned
to the Queue reference q.

2. Adding Elements

The add() method is used to insert elements at the end of the queue.

q.add(10);
q.add(20);
q.add(30);

Operation: add(element)
Description: Adds the specified element to the end of the queue.
Code Explanation: In this example, the integers 10, 20, and 30 are added sequentially to
the queue.

3. Removing the Head Element

The poll() method retrieves and removes the head of the queue, which is the first element in
the sequence added.

System.out.println(q.poll()); // Outputs: 10 (removed)

Operation: poll()
Description: Retrieves and removes the head of the queue. Returns null if the queue is
empty.
Code Explanation: In this line, the head of the queue (which is 10) is removed and
printed. After this operation, the queue now starts with 20 as the new head.

4. Peeking at the Head Element

The peek() method retrieves, but does not remove, the head of the queue.

System.out.println(q.peek()); // Outputs: 20 (head)

Operation: peek()
Description: Retrieves, but does not remove, the head of the queue. Returns null if the
queue is empty.
Code Explanation: This line prints the current head of the queue, which is 20. The peek()
operation does not alter the queue's structure.

These operations demonstrate how a LinkedList can effectively function as a queue in Java,
supporting standard queue operations such as insertion, removal, and inspection of the head
element.

PriorityQueue
Understanding PriorityQueue in
Java
A PriorityQueue is a special type of queue in Java that organizes elements based on their
natural ordering or according to a specified comparator. Unlike a standard queue, which
maintains the order of elements based on their insertion, a PriorityQueue ensures that the
element with the highest priority (or lowest value, depending on the comparator) is always at
the front. This makes it particularly useful for scenarios where you need to process elements
in a specific order of importance.

Operations and Code Examples


1. Adding Elements

You can add elements to a PriorityQueue using the add() or offer() methods. Both methods
function similarly, but offer() is preferable in contexts where failure needs to be handled
gracefully, as it returns false if the addition fails, rather than throwing an exception.

Example Code:

PriorityQueue<Integer> pq = new PriorityQueue<>();


pq.add(30);
pq.add(10);
pq.add(20);
System.out.println(pq); // Output might be [10, 30, 20], but the exact order is not guaranteed

2. Polling Elements

The poll() method retrieves and removes the head of the queue, which is the element with the
highest priority. If the queue is empty, it returns null. This method is often used in scenarios
where the smallest or highest-priority element needs to be processed first.

Example Code:

System.out.println(pq.poll()); // Outputs: 10, as it is the smallest element

3. Peeking Elements

The peek() method retrieves, but does not remove, the head of the queue, returning null if the
queue is empty. This is useful for examining the highest-priority element without altering the
queue's state.

Example Code:

PriorityQueue<Integer> pq = new PriorityQueue<>();


pq.add(50);
pq.add(40);
System.out.println(pq.peek()); // Outputs: 40, as it is the smallest element

4. Removing Specific Elements

You can remove a specific element from the queue using the remove() method. If the element
is present, it is removed and the method returns true; otherwise, it returns false.
Example Code:

boolean isRemoved = pq.remove(20);


System.out.println(isRemoved); // Outputs: true if 20 was present and removed

5. Checking Queue Size

The size() method returns the number of elements in the queue. This is useful for determining
how many elements are waiting to be processed.

Example Code:

System.out.println(pq.size()); // Outputs the number of elements in the queue

6. Clearing the Queue

The clear() method removes all elements from the queue, leaving it empty. This operation is
useful when you need to reset the queue for reuse.

Example Code:

pq.clear();
System.out.println(pq.size()); // Outputs: 0, as the queue is now empty

By understanding these operations, you can effectively utilize a PriorityQueue to manage


elements based on priority, ensuring efficient processing in your Java applications.

Deque (Double Ended Queue)


A Deque (pronounced "deck") is a data structure that allows insertion and
removal of elements from both ends. This flexibility is provided through
specific methods such as addFirst(), addLast(), pollFirst(), and pollLast().

Analogy:
Picture a bus where passengers can board or alight from either the front or back. This
illustrates the functionality of a deque, where you can add or remove elements from either
end.

In-Depth Explanation with Code Example: Basic Deque


Operations
import java.util.Deque;
import java.util.LinkedList;
public class DequeExample {
public static void main(String[] args) {
// Create a Deque instance using LinkedList
Deque<String> deque = new LinkedList<>();

// addFirst() inserts an element at the front of the deque


deque.addFirst("Front");

// addLast() appends an element at the end of the deque


deque.addLast("Back");

// Print the current state of the deque


System.out.println(deque); // Outputs: [Front, Back]

// pollFirst() removes and returns the element at the front


deque.pollFirst(); // Removes "Front"

// Print the state of the deque after removing the first element
System.out.println(deque); // Outputs: [Back]
}
}

Explanation of Operations:

1. addFirst("Front"): Adds the string "Front" to the front of the deque. This operation is
useful when you need to prioritize elements by adding them to the beginning.
2. addLast("Back"): Appends the string "Back" to the end of the deque. This is akin to a
typical queue operation where elements are added to the back.
3. pollFirst(): Removes and returns the first element of the deque, which in this case is
"Front". This operation is used when you want to process and remove elements from the
front.

After these operations, the deque initially holds two elements, "Front" at the beginning and
"Back" at the end. After calling pollFirst(), only "Back" remains.

ArrayDeque (Implementation of
Deque)
ArrayDeque is an implementation of the Deque interface that is backed by a resizable array.
Unlike LinkedList, it has no inherent capacity limitations and is often more efficient for queue
operations due to its array-based nature.
In-Depth Explanation with Code Example
import java.util.ArrayDeque;
import java.util.Deque;

public class ArrayDequeExample {


public static void main(String[] args) {
// Create an ArrayDeque instance
Deque<Integer> arrayDeque = new ArrayDeque<>();

// addFirst() to add elements at the front


arrayDeque.addFirst(10);

// addLast() to add elements at the end


arrayDeque.addLast(20);

// Display the current state of the deque


System.out.println(arrayDeque); // Outputs: [10, 20]

// pollLast() removes the last element


arrayDeque.pollLast(); // Removes "20"

// Display the state of the deque after removing the last element
System.out.println(arrayDeque); // Outputs: [10]
}
}

Explanation of Operations:

1. addFirst(10): Inserts the integer 10 at the front of the ArrayDeque.


2. addLast(20): Appends the integer 20 to the end of the deque.
3. pollLast(): Removes and returns the last element, which is 20 in this case. This operation
is useful when you need to process and remove elements from the back.

These examples illustrate how a Deque can be manipulated from both ends, providing a
flexible data structure for various use cases.
Deque<String> dq = new ArrayDeque<>();
dq.addFirst("Front");
dq.addLast("Back");
System.out.println(dq); // Outputs: [Front, Back]
dq.pollFirst(); // Removes Front
System.out.println(dq); // Outputs: [Back]
}
}
Queue vs Stack
Feature Queue Stack

Order FIFO LIFO

Interface Queue Stack/Deque

Use Case Scheduling, Buffers Undo operation, Recursion

Code Example: Queue vs Stack

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

public class QueueStackComparison {


public static void main(String[] args) {
// Queue example
Queue<String> queue = new LinkedList<>();
queue.add("A");
queue.add("B");
queue.add("C");
System.out.println("Queue: " + queue.poll()); // Outputs: A (FIFO)

// Stack example
Stack<String> stack = new Stack<>();
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println("Stack: " + stack.pop()); // Outputs: C (LIFO)
}
}

Summary

A Queue is a linear data structure that follows the FIFO (First In, First Out) principle. This
means that the first element added to the queue will be the first one to be removed.

Analogy: Think of a queue as a line of people waiting for movie tickets — the first person in
line is served first.
Basic Features:

Inserts at rear (tail)


Removes from front (head)

Queue<String> queue = new LinkedList<>();

Types of Queues

Type Description

Simple Queue Follows FIFO strictly

Circular Queue Last node points to the first

Priority Queue Elements processed based on priority

Deque (Double Ended Queue) Insert/delete at both ends

Queue Interface and Hierarchy


Collection
|
Queue (Interface)
|
-----------------------
| |
PriorityQueue Deque (Interface)
|
ArrayDeque, LinkedList

Common Queue Methods


Method Description

add(e) Inserts element. Throws exception if fails

offer(e) Inserts element. Returns false if fails

remove() Removes head. Throws exception if empty

poll() Removes head. Returns null if empty

element() Returns head. Throws exception if empty

peek() Returns head. Returns null if empty

Example:

Queue<String> queue = new LinkedList<>();


queue.add("A");
queue.offer("B");

System.out.println(queue.remove()); // A
System.out.println(queue.poll()); // B
System.out.println(queue.poll()); // null

PriorityQueue
Behavior: Elements are processed according to priority (default: natural order for
numbers/strings).

Example 1: Natural Order

PriorityQueue<Integer> pq = new PriorityQueue<>();


pq.add(30);
pq.add(10);
pq.add(20);
System.out.println(pq); // Internal structure may vary
System.out.println(pq.poll()); // 10

Example 2: Custom Comparator

PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());


pq.add(10);
pq.add(20);
pq.add(5);
System.out.println(pq.poll()); // 20

LinkedList as Queue
LinkedList implements both Queue and Deque, allowing it to be used flexibly.

Example:

Queue<String> queue = new LinkedList<>();


queue.offer("Apple");
queue.offer("Banana");
System.out.println(queue.peek()); // Apple
System.out.println(queue.poll()); // Apple

Deque and ArrayDeque


Deque: Double Ended Queue, supports insertion and deletion from both ends.

ArrayDeque: Resizable-array implementation of Deque.

Basic Operations:

Deque<String> deque = new ArrayDeque<>();


deque.addFirst("Start");
deque.addLast("End");
System.out.println(deque.removeFirst()); // Start
System.out.println(deque.removeLast()); // End

ArrayDeque as Stack:

Deque<String> stack = new ArrayDeque<>();


stack.push("One");
stack.push("Two");
System.out.println(stack.pop()); // Two

BlockingQueue (Conceptual Introduction)


Part of java.util.concurrent, used in multithreaded environments (e.g., producer-consumer
problems). Types include:

ArrayBlockingQueue
LinkedBlockingQueue

Methods like put() and take() block if the queue is full or empty.

Use-Cases and Best Practices


Use-Case Queue Type

Task Scheduling PriorityQueue

Thread Communication BlockingQueue

Stack Replacement ArrayDeque

FIFO Buffer LinkedList as Queue

Recap
Queue follows FIFO
PriorityQueue for prioritized processing
ArrayDeque supports double-end operations
BlockingQueue for threads
LinkedList is flexible, simple, and ideal for beginners

Introduction to Map
Maps are a fundamental part of Java's data structures, designed to
efficiently store data in key-value pairs. This structure allows for quick data
retrieval using unique keys. Unlike lists or sets, Maps aren't part of the
Collection interface due to their distinct structure.

Key Characteristics of Maps

Unique Keys: Each key in a map is unique, ensuring no duplicate keys


exist.
Duplicate Values: Values can be duplicated, allowing multiple keys to
associate with the same value.

Example: Basic Map Usage

Here's a simple Java example demonstrating how to use a Map:

import java.util.*;

public class MapExample {


public static void main(String[] args) {
Map<String, Integer> students = new HashMap<>();
students.put("Aman", 85); // Adds a key-value pair to the map
students.put("Priya", 90); // Adds another key-value pair to the map
System.out.println(students); // Outputs the map's contents
}
}

Map vs Collection
Map: Stores data as key-value pairs.
Collection: Stores individual elements, such as in Lists or Sets.

Maps offer a distinct way to associate values with keys, setting them apart
from the Collection interface, which handles individual values.

Common Map Implementations


Below is a summary of common Map implementations and their
characteristics:

Type Order Allows Null? Thread-safe?


Maintained

HashMap ❌ No ✅ Yes ❌ No
LinkedHashMa ✅ Insertion ✅ Yes ❌ No
p

TreeMap ✅ Sorted (by ❌ Null keys ❌ No


key)

Hashtable ❌ No ❌ No ✅ Yes

Basic Operations on Map


Maps support several basic operations essential for handling key-value
pairs:

Code Example: Basic Map Operations

Here’s how you can perform basic operations on a Map:

Map<String, String> map = new HashMap<>();


map.put("101", "Java"); // Inserts a key-value pair into the map
map.put("102", "Python"); // Inserts another key-value pair
System.out.println(map.get("101")); // Retrieves the value associated with
key "101"
map.remove("101"); // Removes the key-value pair with key "101"
System.out.println(map.size()); // Returns the number of key-value pairs in
the map
System.out.println(map.isEmpty()); // Checks if the map is empty

Iterating a Map
You can iterate over a map in various ways, each serving different
purposes:

Using keySet()

for (String key : map.keySet()) { // Iterates over each key in the map
System.out.println(key + " => " + map.get(key)); // Retrieves and prints the
value associated with each key
}

Using entrySet()

for (Map.Entry<String, String> entry : map.entrySet()) { // Iterates over each


key-value pair in the map
System.out.println(entry.getKey() + " -> " + entry.getValue()); // Prints
each key-value pair
}

Using forEach()
map.forEach((key, value) -> { // Iterates over each key-value pair using a
lambda expression
System.out.println(key + " = " + value); // Prints each key-value pair
});

Detailed Look at Different Map


Types
HashMap

Characteristics: Unordered, fast, allows one null key and multiple null
values.
Use Case: Best when order is not important and quick access is
required.

Code Example

Map<Integer, String> hashMap = new HashMap<>();


hashMap.put(3, "Three"); // Inserts a key-value pair
hashMap.put(null, "Null Key"); // Inserts a null key with a value
System.out.println(hashMap); // Prints the map's contents

LinkedHashMap

Characteristics: Maintains the order of insertion.


Use Case: Ideal when you need to preserve the order of entries.

Code Example

Map<String, Integer> marks = new LinkedHashMap<>();


marks.put("Math", 90); // Inserts a key-value pair
marks.put("Science", 80); // Inserts another key-value pair
System.out.println(marks); // Prints the map's contents in insertion order

TreeMap

Characteristics: Sorted by keys, does not permit null keys.


Use Case: Use when a sorted map by key is required.

Code Example
Map<String, String> treeMap = new TreeMap<>();
treeMap.put("C", "C++"); // Inserts a key-value pair
treeMap.put("A", "Android"); // Inserts another key-value pair
System.out.println(treeMap); // Prints the map's contents sorted by key

Hashtable

Characteristics: Thread-safe, slower, does not allow null keys or values.


Use Case: Suitable when thread safety is necessary.

Code Example

Map<Integer, String> table = new Hashtable<>();


table.put(1, "One"); // Inserts a key-value pair
table.put(2, "Two"); // Inserts another key-value pair
System.out.println(table); // Prints the map's contents

Map.Entry Interface
The Map.Entry interface allows retrieval of both key and value from a map,
particularly useful with entrySet().

Example

for (Map.Entry<String, Integer> entry : students.entrySet()) {


System.out.println("Name: " + entry.getKey() + ", Marks: " +
entry.getValue()); // Prints each key-value pair
}

When to Use Which Map


Scenario Recommended Map

Fast access, no order needed HashMap

Insertion order needed LinkedHashMap

Sorted order needed TreeMap

Thread safety needed Hashtable

Sorting in Java
What is Sorting?
Sorting is the process of arranging elements in a specific order—either ascending or
descending. Java provides mechanisms for sorting both primitive arrays and collections.

Sorting Arrays (Primitive & Object Arrays)

import java.util.Arrays;

public class SortArray {


public static void main(String[] args) {
int[] numbers = {5, 3, 8, 1, 2};
Arrays.sort(numbers); // Ascending order
System.out.println("Sorted Array: " + Arrays.toString(numbers));
}
}

Sorting Strings

import java.util.Arrays;

public class SortStrings {


public static void main(String[] args) {
String[] names = {"Gaurav", "Amit", "Neha", "Zara"};
Arrays.sort(names);
System.out.println("Sorted Names: " + Arrays.toString(names));
}
}

Comparable Interface
What is Comparable?

The Comparable interface is used to define the natural ordering of objects. A class
implements this interface to determine how its instances should be compared.

public int compareTo(T o);

Returns:
Positive value → this > o
Zero → this == o
Negative value → this < o

Program Example

import java.util.*;

class Student implements Comparable<Student> {


int roll;
String name;

Student(int roll, String name) {


this.roll = roll;
this.name = name;
}

public int compareTo(Student s) {


return this.roll - s.roll; // Ascending order by roll number
}

public String toString() {


return roll + " " + name;
}
}

public class ComparableDemo {


public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student(102, "Amit"));
list.add(new Student(101, "Gaurav"));
list.add(new Student(103, "Neha"));
Collections.sort(list); // Uses compareTo
for (Student s : list)
System.out.println(s);
}
}

Comparator Interface
What is Comparator?

The Comparator interface is used when you can't modify the class or need multiple sort
criteria.

public int compare(T o1, T o2);

Program: Sort by Name

import java.util.*;

class Student {
int roll;
String name;

Student(int roll, String name) {


this.roll = roll;
this.name = name;
}

public String toString() {


return roll + " " + name;
}
}

class SortByName implements Comparator<Student> {


public int compare(Student a, Student b) {
return a.name.compareTo(b.name); // Ascending order by name
}
}

public class ComparatorDemo {


public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student(102, "Amit"));
list.add(new Student(101, "Gaurav"));
list.add(new Student(103, "Neha"));

Collections.sort(list, new SortByName());


for (Student s : list)
System.out.println(s);
}
}

Sort in Descending Order

Collections.sort(list, (a, b) -> b.name.compareTo(a.name)); // Lambda

Properties Class
What is it?

java.util.Properties is a subclass of Hashtable used for reading and writing configuration as


key-value pairs, typically used in .properties files.

Common Methods

getProperty(String key)
setProperty(String key, String value)
store(OutputStream, comment)
load(InputStream)

Program: Write Properties to File

import java.util.*;
import java.io.*;

public class WriteProperties {


public static void main(String[] args) throws Exception {
Properties prop = new Properties();
prop.setProperty("username", "admin");
prop.setProperty("password", "12345");

FileOutputStream fos = new FileOutputStream("config.properties");


prop.store(fos, "App Config");
fos.close();
System.out.println("Properties file created.");
}
}
Program: Read from Properties File

import java.util.*;
import java.io.*;

public class ReadProperties {


public static void main(String[] args) throws Exception {
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("config.properties");
prop.load(fis);

System.out.println("Username: " + prop.getProperty("username"));


System.out.println("Password: " + prop.getProperty("password"));
}
}

Summary Table

Concept Interface/Use Key Method Used For

Sorting Arrays.sort() / N/A Sort arrays/lists


Collections.sort()

Comparable java.lang.Comparab compareTo(T o) Natural order (in


le<T> same class)

Comparator java.util.Comparato compare(T o1, T o2) Custom order


r<T> (external
comparator)

Properties java.util.Properties load(), Config files (key-


getProperty() value pairs)
UNIT-V
Spring Framework Core Basics
What is Spring Framework?

Definition:
Spring is a lightweight, open-source Java framework that helps in developing loose-coupled,
scalable, and testable enterprise applications.

1. Dependency Injection (DI)


Definition: Dependency Injection (DI) is a design pattern where dependencies (objects) are
injected into a class rather than the class creating them. This promotes loose coupling and
enhances testability.

Types of Dependency Injection:

Constructor Injection: Dependencies are provided through a class constructor.


Setter Injection: Dependencies are provided through setter methods after object
creation.

In-Depth Example of Setter Injection:

Imagine a Student class that requires an Address object. Instead of the Student class creating
an Address object, it receives one through a setter method.

public class Student {


private Address address;

public void setAddress(Address address) {


this.address = address;
}

public void show() {


System.out.println("Address: " + address);
}
}
public class Address {
public String toString() {
return "Delhi, India";
}
}

Configuration with beans.xml:

Spring uses an XML configuration file to define the beans and their dependencies.

<beans>
<bean id="address" class="Address"/>
<bean id="student" class="Student">
<property name="address" ref="address"/>
</bean>
</beans>

2. Inversion of Control (IoC)


Definition: Inversion of Control (IoC) is a core principle of the Spring Framework where the
control of object creation and dependency management is transferred from the program to
the Spring container. This allows developers to focus on business logic without worrying
about object lifecycle management.

Example: The Spring container is responsible for creating the Address object and injecting it
into the Student class, as shown in the previous example.

3. Aspect-Oriented Programming (AOP)


Definition: Aspect-Oriented Programming (AOP) is a programming paradigm that allows for
the separation of cross-cutting concerns such as logging, security, and transaction
management from the business logic. This helps in keeping the code clean and modular.

Key Concepts:

Aspect: A module that encapsulates cross-cutting concerns.


Advice: Action taken at a particular join point.
Pointcut: A predicate that matches join points.
JoinPoint: A point during the execution of a program.

Example of Logging using AOP:

Using AOP, you can log method execution details without cluttering business logic.

@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Executing: " + joinPoint.getSignature().getName());
}
}

4. Bean Scopes
Bean scopes define the lifecycle and visibility of beans within the Spring container. Each
scope dictates how and when a bean is created and shared.

Scope Description

Singleton One instance per Spring container

Prototype A new instance each time requested

Request One per HTTP request (Web applications


only)

Session One per HTTP session (Web applications


only)

Application One per ServletContext

WebSocket One per WebSocket connection

Example of Prototype Scope:

@Component
@Scope("prototype")
public class User {
public User() {
System.out.println("User object created");
}
}

In this example, a new User object is created each time it is requested from the Spring
container.

5. Autowiring
Spring provides several ways to automatically resolve and inject the correct bean
dependencies into your classes, simplifying configuration and reducing boilerplate code.

Types of Autowiring:

byType: Autowires by matching data type.


byName: Autowires by matching bean names.
constructor: Autowires by matching constructor arguments.
@Autowired: Annotation used to inject dependencies.

Example of Autowiring with @Autowired:

@Component
public class Employee {
@Autowired
private Department department;
}

In this example, Spring automatically injects an instance of Department into the Employee
class.

6. Annotations
Spring provides various annotations to simplify the configuration. Here are some commonly
used annotations:

@Component: Indicates a Spring-managed component.


@Controller, @Service, @Repository: Specialized stereotypes for components.
@Autowired: Marks a dependency to be injected.
@Scope: Defines the scope of a bean.
@PostConstruct, @PreDestroy: Lifecycle callback methods.

7. Lifecycle Callbacks
Lifecycle callbacks allow you to perform custom actions on bean initialization and
destruction.

Example of Lifecycle Callbacks:

@Component
public class HelloBean {
@PostConstruct
public void init() {
System.out.println("Bean is going through init.");
}

@PreDestroy
public void destroy() {
System.out.println("Bean will be destroyed now.");
}
}

The init method is called after the bean is initialized and the destroy method before it is
destroyed.

8. Bean Configuration Styles


Spring supports multiple configuration styles to define beans and their dependencies,
allowing flexibility based on developer preference and application needs.

XML-Based Configuration: Traditional way using XML files.


Annotation-Based Configuration: Leverages annotations for configuration.
Java-Based Configuration: Uses @Configuration classes to define beans.

Example of Java-Based Configuration:

@Configuration
public class AppConfig {
@Bean
public Student student() {
return new Student();
}
}

This Java-based configuration replaces XML configuration, providing a type-safe and


refactoring-friendly way to configure Spring beans.

Spring Boot
1. Spring Boot Build Systems
Spring Boot supports popular build systems like Maven and Gradle, simplifying project setup
and dependency management.

Maven: Commonly used build tool with a large repository of plugins.


Gradle: Offers configuration flexibility and faster builds.

Example Maven Configuration (pom.xml):

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

2. Spring Boot Code Structure


Spring Boot projects have a standard directory structure that promotes organization and
separation of concerns.

src/main/java
├── com.example.demo
│ ├── DemoApplication.java
│ ├── controller
│ └── service
DemoApplication.java: The main entry point for the Spring Boot application.
controller: Contains REST controllers.
service: Contains business logic.

3. Spring Boot Runners


Spring Boot provides interfaces to execute specific code at startup, allowing for initialization
logic outside the standard bean lifecycle.

CommandLineRunner: Executes code after the Spring Boot application is started.


ApplicationRunner: Similar to CommandLineRunner but provides ApplicationArguments.

Example of CommandLineRunner:

@SpringBootApplication
public class MyApp implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}

@Override
public void run(String... args) {
System.out.println("App Started!");
}
}

4. Logger
Logging is an essential part of any application, and Spring Boot makes it easy to integrate
logging using SLF4J and Logback.
Example Logger Usage:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class MyController {
Logger logger = LoggerFactory.getLogger(MyController.class);

@GetMapping("/log")
public String logExample() {
logger.info("This is an info log");
return "Logged successfully!";
}
}

Building RESTful Web Services


1. @RestController
@RestController is a convenience annotation that combines @Controller and
@ResponseBody, simplifying REST API development.

Example:

@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello Spring Boot!";
}
}

2. @RequestMapping
@RequestMapping is used to map web requests to specific handler classes or methods.

Example:

@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/greet")
public String greet() {
return "Greetings!";
}
}

3. @RequestBody
@RequestBody is used to bind the HTTP request body to a domain object, enabling easy
JSON payload handling.

Example:

@PostMapping("/student")
public String addStudent(@RequestBody Student student) {
return "Added student: " + student.getName();
}

4. @PathVariable
@PathVariable is used to extract values from the URI, providing a way to handle dynamic
URLs.

Example:

@GetMapping("/student/{id}")
public String getStudent(@PathVariable int id) {
return "Student ID: " + id;
}

5. @RequestParam
@RequestParam is used to extract query parameters from the URL, making it easy to handle
optional and required parameters.

Example:

@GetMapping("/search")
public String search(@RequestParam String name) {
return "Searched for: " + name;
}

6. GET, POST, PUT, DELETE APIs


Spring Boot simplifies the creation of CRUD operations with easy-to-use annotations for each
HTTP method.
GET Example:

@GetMapping("/students")
public List<Student> getAllStudents() {
return studentService.getAll();
}

POST Example:

@PostMapping("/students")
public Student add(@RequestBody Student student) {
return studentService.save(student);
}

PUT Example:

@PutMapping("/students/{id}")
public Student update(@PathVariable int id, @RequestBody Student student) {
return studentService.update(id, student);
}

DELETE Example:

@DeleteMapping("/students/{id}")
public String delete(@PathVariable int id) {
studentService.delete(id);
return "Deleted Successfully!";
}

These examples demonstrate how Spring Boot's REST capabilities facilitate the creation of
robust and scalable web services.

You might also like