0% found this document useful (0 votes)
15 views154 pages

Complete Java Notes

The document provides an overview of Java, highlighting its key features such as platform independence, object-oriented principles, and robust security. It details the history of Java, the Java Virtual Machine (JVM), and fundamental concepts like classes, objects, constructors, and OOP principles including encapsulation and inheritance. Additionally, it explains static variables and methods, the 'this' keyword, and various types of constructors in Java.

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)
15 views154 pages

Complete Java Notes

The document provides an overview of Java, highlighting its key features such as platform independence, object-oriented principles, and robust security. It details the history of Java, the Java Virtual Machine (JVM), and fundamental concepts like classes, objects, constructors, and OOP principles including encapsulation and inheritance. Additionally, it explains static variables and methods, the 'this' keyword, and various types of constructors in Java.

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/ 154

UNIT-I

Introduction: Why Java?


Key Reasons to Choose Java:

Platform Independence
Java code is compiled into bytecode, which operates on the Java Virtual Machine (JVM).

“Write Once, Run Anywhere” (WORA) – Java programs can be executed on any device equipped
with a JVM.

Object-Oriented
Encourages modular, flexible, and reusable code.
Everything in Java (except primitives) is treated as an object.
Robust and Secure
Features strong memory management, exception handling, and garbage collection.
Offers built-in security features such as bytecode verification and runtime security
policies.
Simple and Familiar
The syntax is akin to C/C++, while eliminating complex features like pointers and multiple
inheritance.
Multithreaded
Java natively supports multithreaded programming, which is beneficial for gaming,
animation, and real-time applications.
Rich API and Libraries
Provides a vast standard library that encompasses everything from networking to data
structures.
Dynamic and Distributed
Crafted for use in the distributed environment of the internet, supporting remote
method invocation (RMI) and dynamic class loading.

History of Java
Early 1990s – The Birth of Java

Java was conceived by James Gosling and his team at Sun Microsystems in 1991, originally named the “Green
Project.” The first version was aimed at embedded systems for consumer electronics like set-top boxes and
televisions.

1996 – Java 1.0

Java was officially launched as Java 1.0 in 1995, marketed as a tool for crafting interactive
web content, with applets being particularly popular.

Evolution of Java Versions (Highlights)

Java 1.2 (1998) – Introduced the Swing toolkit and Collections Framework.
Java 5 (2004) – Added generics, enhanced for-loop, enums, and autoboxing.
Java 8 (2014) – Major update featuring Lambda expressions, Streams API, and a new
Date/Time API.
Java 11 (2018) – LTS version; removed Applet, added var, and introduced HTTP Client API.
Java 17 (2021) – LTS version; introduced sealed classes and pattern matching.
Java 21 (2023) – The latest LTS version with performance enhancements and new
features.

Java Virtual Machine (JVM)


What is JVM?

The Java Virtual Machine (JVM) is an abstract computing machine that enables a computer to
execute Java programs. It converts compiled bytecode (.class files) into machine code for
execution, ensuring Java's platform independence.

Role of JVM:

Loads the Class: The Class Loader subsystem imports .class files (bytecode) into the JVM.
Verifies the Bytecode: Ensures that the code is valid, secure, and complies with Java’s
security model.
Executes the Bytecode: Utilizes an interpreter or JIT (Just-In-Time) compiler to translate
bytecode into native machine code.
Memory Management: Allocates memory and manages Garbage Collection automatically.
Security Management: Enforces access restrictions and prevents unauthorized actions
through the Security Manager and Bytecode Verifier.

JVM Architecture (Components):

1. Class Loader: Imports class files into memory.


2. Method Area: Stores class structures such as metadata, method data, and runtime
constants.
3. Heap: The runtime data area for object allocation.
4. Stack: Holds local variables, operand stacks, and partial results.
5. PC Register: Contains the address of the currently executing JVM instruction.
6. Execution Engine: Executes bytecode using either an interpreter or JIT compiler.
7. Native Method Interface: Enables calls to native (non-Java) code like C/C++.
8. Native Method Libraries: Libraries for native functions.

Execution Flow:

1. Java file → .java


2. Compiled using javac → .class file (Bytecode)
3. .class file provided to JVM
4. JVM loads, verifies, and executes using interpreter or JIT

Note: “The JVM is the heart of Java’s cross-platform capabilities, allowing Java programs to
run in a controlled and secure manner.”

Important Points:

JVM is not platform-independent; however, the bytecode is.


Various JVMs are designed for different operating systems.
JVM is part of the Java Runtime Environment (JRE).

What is a Class?
1. A class in Java serves as a user-defined data type, encapsulating data (fields) and
behaviors (methods) into a single unit. Think of it as a template or blueprint for creating
individual objects.
2. A class outlines the structure and behavior of future objects.
3. No memory is allocated upon defining a class; memory is reserved when objects are
instantiated.

class ClassName {
// Fields (Instance Variables)
// Methods
}

class Car {
String model;
int year;
void displayInfo() {
System.out.println("Model: " + model + ", Year: " + year);
}
}

What is an Object?
An object is a runtime instance of a class, representing a specific implementation of the
class's structure. When a class is instantiated, an object is created with its unique copy of
instance variables and access to class methods.

Creating Objects Syntax:

Car myCar = new Car(); // Object creation

Each Object:

1. Occupies memory.
2. Has its own identity and state.

Accessing Fields/Methods Syntax:

myCar.model = "Tesla";
myCar.year = 2023;
myCar.displayInfo();

Instance Variables

Instance variables are non-static fields declared within a class but outside any method,
constructor, or block. Each object (instance) of the class receives its own separate copy
of these variables.

Key Characteristics:

1. Memory is allocated when an object is instantiated.


2. Their values are specific to the object — changes made by one object do not impact
others.
3. Can have access modifiers like private, public, or protected.

Syntax

class Student {
String name; // instance variable
int rollNumber; // instance variable
}

Instance Methods

Instance methods are non-static methods that operate on instance variables of the class.
They require an object for invocation and can directly access all non-static members.

Key Characteristics:

1. Can read/write instance variables.


2. Can call other instance methods directly.
3. Cannot be invoked without an object.

Syntax
class Book {
String title;
int price;
void show() {
System.out.println("Book: " + title + ", Price: " + price);
}
}

Accessing Instance Members Syntax:

Book b1 = new Book();


b1.title = "Java for Beginners";
b1.price = 450;
b1.show(); // Calling instance method

Program: Instance Variables and Methods

class Employee {
String name;
double salary;
void setDetails(String empName, double empSalary) {
name = empName;
salary = empSalary;
}
void showDetails() {
System.out.println("Employee Name: " + name);
System.out.println("Salary: ₹" + salary);
}
}

public class Company {


public static void main(String[] args) {
Employee e1 = new Employee();
e1.setDetails("Amit", 50000);
e1.showDetails();
Employee e2 = new Employee();
e2.setDetails("Riya", 60000);
e2.showDetails();
}
}

Static Variables and Static Methods in Java


1. The static keyword in Java is utilized for memory management. It allows members of a
class (variables or methods) to be shared across all instances of that class without the
need for object instantiation.
2. When something is declared static, it belongs to the class itself rather than to individual
objects.

Static Variables (Class Variables)

1. Declared using the static keyword.


2. Only one copy exists, shared among all instances.
3. Useful for constants or values that should remain consistent across all objects.

Example

class Counter {
static int count = 0; // static variable
Counter() {
count++; // increments the same variable for all objects
System.out.println("Count: " + count);
}
}

Static Methods

1. Can be invoked without creating an object.


2. Can only access static data directly.
3. Cannot utilize this or super in static context because there is no instance.

Example:

class MathUtil {
static int square(int x) {
return x * x;
}
}

public class Test {


public static void main(String[] args) {
System.out.println("Square of 5: " + MathUtil.square(5));
}
}

What is a Constructor?
A constructor in Java is a special method used to initialize objects upon creation. It shares
the same name as the class and lacks a return type — not even void.
Key Characteristics of Constructors:

1. Automatically invoked upon object creation.


2. Shares the same name as the class.
3. Lacks a return type, not even void.
4. Can be overloaded (multiple constructors with different parameter lists).
5. Used to assign initial values to object fields.

Types of Constructors:

1.Default Constructor:
A) Automatically supplied by Java if no constructor is defined.
B) Initializes fields with default values (e.g., 0, null, false).

Example:

class Student {
String name;
int age;
// Default constructor (automatically provided if you don’t define any)
Student() {
name = "Unknown";
age = 0;
}
void show() {
System.out.println("Name: " + name + ", Age: " + age);
}
}

2.Parameterized Constructor:
A) Used to initialize fields with custom values.

Example:

class Student {
String name;
int age;
Student(String n, int a) {
name = n;
age = a;
}
void show() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
3.Constructor Overloading:
A) Java permits multiple constructors in the same class with varied parameter types or
counts.

Example:

class Box {
int length, width;
Box() {
length = width = 0;
}
Box(int l, int w) {
length = l;
width = w;
}
void show() {
System.out.println("Length: " + length + ", Width: " + width);
}
}

4.Using this() for Constructor Chaining:


A) Use this() to call another constructor within the same class.

Example:

class Employee {
String name;
int id;
Employee() {
this("Default", 0); // calls parameterized constructor
}
Employee(String name, int id) {
this.name = name;
this.id = id;
}
void show() {
System.out.println("Name: " + name + ", ID: " + id);
}
}

Note:

1. If you define any constructor, Java will not provide the default one.
2. Constructor overloading offers flexibility during object creation.
3. Using this() helps avoid code duplication and ensures clean initialization.
this Keyword in Java
The this keyword is a special reference variable in Java that refers to the current object — the
object on which a method or constructor is executed. It is useful in situations where the
context may be unclear, such as when instance variable names and parameters overlap. It
also aids in method chaining, constructor chaining, and passing the current object to other
methods or constructors.

Key Uses of this Keyword:

1. Referring to Instance Variables


2. Calling Another Constructor (this())
3. Passing Current Object as a Parameter

1. Referring to Instance Variables:

When method or constructor parameters share names with instance variables, this is used to
differentiate them.

Example:

class Student {
String name;
int age;
Student(String name, int age) {
this.name = name; // 'this.name' refers to the instance variable
this.age = age; // 'age' refers to the parameter
}
void display() {
System.out.println("Name: " + name + ", Age: " + age);
}
}

2. Calling Another Constructor (this()):

Used for constructor chaining, where one constructor invokes another within the same class
to avoid code duplication.

Example:

class Box {
int length, width;
Box() {
this(0, 0); // Must be the first line
}
Box(int l, int w) {
this.length = l;
this.width = w;
}
}

3. Passing Current Object as a Parameter:

The current object can be passed using this to methods that require an object reference as an
argument.

Example:

class A {
void show(A obj) {
System.out.println("Called with object: " + obj);
}
void call() {
show(this); // passing current object
}
}

Object-Oriented Programming (OOP) Principles in Java


Object-Oriented Programming is a programming paradigm focused on objects, which are
instances of classes. Java is a purely object-oriented language (aside from primitives) and
embraces the four core principles of OOP:

1. Encapsulation:

Encapsulation is the practice of binding data (variables) and code (methods) into a single unit
— a class — while restricting direct access to some of the object's components.

How Java Supports It:

By using private access specifiers for instance variables.


Access to variables is facilitated via public methods (getters/setters).

Example:

// Class with encapsulated fields


class Employee {
// Private fields - cannot be accessed directly
private String name;
private double salary;
// Public setter method - to set values
public void setName(String name) {
this.name = name;
}
public void setSalary(double salary) {
if (salary > 0) {
this.salary = salary;
}
}
// Public getter method - to access values
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
}

// Main class to test encapsulation


public class EncapsulationDemo {
public static void main(String[] args) {
Employee emp = new Employee();
// Setting values using setters
emp.setName("Rohit");
emp.setSalary(75000);
// Getting values using getters
System.out.println("Employee Name: " + emp.getName());
System.out.println("Employee Salary: Rs" + emp.getSalary());
}
}

2. Inheritance:

Inheritance is a fundamental principle of Object-Oriented Programming that allows one class


(the subclass) to acquire the properties and behaviors (methods and fields) of another class
(the superclass).

Note:
This mechanism promotes code reuse, hierarchical classification, and extensibility.

Types of Inheritance in Java:

1. Single Inheritance
2. Multilevel Inheritance
3. Hierarchical Inheritance
4. Multiple Inheritance (via interface)
Note: Java does not support Multiple Inheritance (via class) to avoid ambiguity.

Syntax:

class SuperClass {
// fields and methods
}
class SubClass extends SuperClass {
// inherits from SuperClass
}

Key Features:

1. Subclasses inherit all accessible fields and methods from the superclass.
2. A subclass can add new members or override existing ones.
3. Inheritance supports method overriding for dynamic behavior.

Single Inheritance:

Single Inheritance is the simplest and most widely used form of inheritance in Java. In single
inheritance, a single subclass inherits the members (fields and methods) of one superclass.

This relationship models an "is-a" connection — for instance, a Car is a Vehicle.

Why Use Single Inheritance?

1. Promote code reuse.


2. Reduce redundancy.
3. Simplify maintenance and extension of programs.

NOTE:

1. The subclass inherits all non-private fields and methods.


2. The subclass can introduce its own fields and methods.
3. It can override methods for specific behavior.
4. Constructors are not inherited, but the super keyword can be utilized to call them.

Example:

// Superclass (Parent)
class Animal {
String name;
void eat() {
System.out.println(name + " is eating.");
}
void sleep() {
System.out.println(name + " is sleeping.");
}
}

// Subclass (Child)
class Dog extends Animal {
void bark() {
System.out.println(name + " is barking.");
}
}

// Test class with main method


public class SingleInheritanceDemo {
public static void main(String[] args) {
Dog d = new Dog(); // creating subclass object
d.name = "Bruno"; // inherited field
d.eat(); // inherited method
d.sleep(); // inherited method
d.bark(); // subclass-specific method
}
}

Multilevel Inheritance:

Multilevel inheritance refers to a scenario where a class is derived from another class, which
itself is derived from a third class. This creates an inheritance chain of multiple levels.

Example:
Class C inherits from Class B, and Class B inherits from Class A.

// Base class
class Animal {
void eat() {
System.out.println("Animal eats food.");
}
}

// First derived class


class Mammal extends Animal {
void walk() {
System.out.println("Mammal walks on four legs.");
}
}

// Second derived class (inherits from Mammal → Animal)


class Dog extends Mammal {
void bark() {
System.out.println("Dog barks loudly.");
}
}

// Test class
public class MultilevelInheritanceDemo {
public static void main(String[] args) {
Dog dog = new Dog();
// Methods from all levels of inheritance
dog.eat(); // from Animal
dog.walk(); // from Mammal
dog.bark(); // from Dog
}
}

Hierarchical Inheritance:

Hierarchical inheritance occurs when multiple subclasses inherit from the same superclass.
In this structure, one parent class is extended by two or more child classes.

This reflects the real-world relationship: “One parent – multiple children.”

Example:

// Superclass
class Animal {
void eat() {
System.out.println("All animals eat food.");
}
}

// Subclass 1
class Dog extends Animal {
void bark() {
System.out.println("Dog barks.");
}
}

// Subclass 2
class Cat extends Animal {
void meow() {
System.out.println("Cat meows.");
}
}

// Subclass 3
class Cow extends Animal {
void moo() {
System.out.println("Cow moos.");
}
}

// Main class to test


public class HierarchicalInheritanceDemo {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // from Animal
dog.bark(); // specific to Dog

Cat cat = new Cat();


cat.eat(); // from Animal
cat.meow(); // specific to Cat

Cow cow = new Cow();


cow.eat(); // from Animal
cow.moo(); // specific to Cow
}
}

Note:

1. Java does not support multiple inheritance through classes (to eliminate ambiguity), but
hierarchical inheritance is fully supported.
2. Private members of the superclass are not inherited — access is only via methods.

3. Abstraction:

Abstraction is the object-oriented principle of hiding internal implementation details and


exposing only the essential features of an object.

1. In simple terms: “Show what is necessary, hide what is complex.”


2. Abstraction allows focus on what an object does rather than how it does it.
3. Abstraction defines what operations an object can perform without dictating how those
operations are executed.

Java supports abstraction through:

Abstract classes
Interfaces
Abstraction Using Abstract Classes

What is an Abstract Class?

An abstract class is a class that cannot be instantiated and is meant to be extended by other
classes. It can contain:

1. Abstract methods (without a body)


2. Concrete methods (with a body)

Syntax:

abstract class Shape {


abstract void draw(); // abstract method
void info() {
System.out.println("This is a shape.");
}
}

Example:

// Abstract class
abstract class Shape {
abstract void draw(); // abstract method
void info() {
System.out.println("This is a geometric shape.");
}
}

// Subclass 1
class Circle extends Shape {
void draw() {
System.out.println("Drawing a Circle.");
}
}

// Subclass 2
class Rectangle extends Shape {
void draw() {
System.out.println("Drawing a Rectangle.");
}
}

// Test class
public class AbstractionDemo {
public static void main(String[] args) {
Shape s1 = new Circle(); // Abstract class reference to subclass object
Shape s2 = new Rectangle();
s1.draw(); // Circle's implementation
s1.info(); // Concrete method from Shape
s2.draw(); // Rectangle's implementation
s2.info();
}
}

Abstract Method:

An abstract method is a method declared inside an abstract class or interface without a


body. It provides only the method signature, and the actual implementation is provided by a
subclass or implementing class.

Syntax:

abstract void methodName(); // abstract method - no body

Note:

1. Think of it as a placeholder for behavior that must be implemented by the inheriting


class.
2. Abstract methods lack a code block.
3. The class containing them must be declared abstract.
4. Subclasses are obligated to override and provide concrete implementations unless they
are also abstract.

Example:

// Abstract class
abstract class Animal {
// Abstract method
abstract void makeSound();

// Concrete method
void sleep() {
System.out.println("This animal is sleeping...");
}
}

// Subclass 1
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks.");
}
}

// Subclass 2
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows.");
}
}

// Test class
public class AbstractMethodDemo {
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.makeSound(); // Dog’s implementation
a2.makeSound(); // Cat’s implementation
a1.sleep(); // Concrete method from abstract class
}
}

What is an Interface?

An interface in Java is a fully abstract class-like structure that defines a set of methods that
a class must implement. It specifies a contract that implementing classes must fulfill.

“An interface defines a protocol of behavior but doesn’t provide any implementation.”

Why Use Interfaces?

1. To achieve abstraction.
2. To support multiple inheritance of type (since Java doesn’t permit multiple class
inheritance).
3. To enforce structure across unrelated classes.

Syntax:

interface InterfaceName {
returnType methodName(parameterList); // implicitly public and abstract
}

Example:
// Define interface
interface Printable {
void print(); // implicitly public and abstract
}

// Implementing class
class Document implements Printable {
public void print() {
System.out.println("Printing document...");
}
}

// Another implementing class


class Image implements Printable {
public void print() {
System.out.println("Printing image...");
}
}

// Main class
public class InterfaceDemo {
public static void main(String[] args) {
Printable p1 = new Document();
Printable p2 = new Image();
p1.print(); // Output: Printing document...
p2.print(); // Output: Printing image...
}
}

Note:

1. Interfaces from Java 8 Onward: Default and static methods allowed with body.
2. Interfaces from Java 9 Onward: Private methods in interfaces allowed.

Real World Example:

interface Vehicle {
void start();
}

class Bike implements Vehicle {


public void start() {
System.out.println("Bike starts with a kick.");
}
}

class Car implements Vehicle {


public void start() {
System.out.println("Car starts with a key.");
}
}

NOTE:

Use an interface when you aim to define a common set of operations for multiple
unrelated classes.
Use an abstract class when classes share some common behavior and state, but you still
want to enforce certain method definitions.

Difference between Interface and Abstract Class

1. An abstract class can have a constructor, while an interface cannot.


2. An abstract class can contain concrete methods, while an interface can only contain
abstract methods (prior to Java 8).
3. Interfaces support multiple inheritance, while abstract classes do not.

4. Polymorphism

The term polymorphism derives from Greek: “poly” meaning many, and “morph” meaning
forms. In Java, polymorphism allows an object to take on many forms. It enables a common
interface to behave differently based on the actual class of the object.

“Polymorphism is the ability of one interface to be used for a general class of actions. The
specific action is determined by the exact nature of the situation.”

Types of Polymorphism in Java:

1. Compile-Time Polymorphism (Method Overloading)


2. Run-Time Polymorphism (Method Overriding)

1. Compile-Time Polymorphism (Method Overloading)

Method Overloading occurs when multiple methods in the same class share the same name
but have different parameters.

Example:

class MathUtils {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}

public class OverloadDemo {


public static void main(String[] args) {
MathUtils m = new MathUtils();
System.out.println(m.add(10, 20)); // Output: 30
System.out.println(m.add(5.5, 4.5)); // Output: 10.0
}
}

Note: The compiler determines which method to call based on the method signature at
compile time.

2. Run-Time Polymorphism (Method Overriding)

Occurs when a subclass provides its own implementation of a method already defined in the
superclass. The method call is resolved at runtime according to the object type.

Example:

class Animal {
void sound() {
System.out.println("Animal makes a sound.");
}
}

class Dog extends Animal {


@Override
void sound() {
System.out.println("Dog barks.");
}
}

class Cat extends Animal {


@Override
void sound() {
System.out.println("Cat meows.");
}
}
public class OverrideDemo {
public static void main(String[] args) {
Animal a; // Reference of superclass
a = new Dog(); // Object of subclass
a.sound(); // Calls Dog’s version
a = new Cat();
a.sound(); // Calls Cat’s version
}
}

Note: The method call is determined during execution, not at compile time.

Difference Between Method Overloading and Method Overriding

What is a Package in Java?

1. A package is a namespace that organizes a set of related classes and interfaces. Think of
it as a folder on your computer that groups related files together.

Key Features:

1. Packages help avoid name conflicts.


2. They facilitate easier location and use of classes, interfaces, annotations, and sub-
packages.
3. They provide access protection and can control visibility using access modifiers.

How to Create a Package:

package mypackage; // Declares the package name

public class MyClass {


public void show() {
System.out.println("Inside MyClass");
}
}

1. The first statement must be the package declaration.


2. The class now belongs to mypackage.

Declaring a Package

package com.knmiet.utils;

public class MathUtils {


public static int square(int x) {
return x * x;
}
}

Directory Structure:

src/com/knmiet/utils/MathUtils.java

Import and Static Import:

1. Regular Import:

Used to access classes from other packages.

import com.knmiet.utils.MathUtils;

public class Main {


public static void main(String[] args) {
System.out.println(MathUtils.square(5));
}
}

2. Static Import:

Used to access static members directly without needing the class name.

import static com.knmiet.utils.MathUtils.square;

public class Main {


public static void main(String[] args) {
System.out.println(square(5)); // No need for MathUtils.square()
}
}
UNIT-II
EXCEPTION HANDLING
Exception Handling in Java is a mechanism to
handle runtime errors so that the normal flow of
the application is maintained.
Exception
1. An exception is an unusual or error condition that disrupts the normal
flow of a program. Instead of crashing, Java allows us to detect and
handle such conditions.
2. An Exception is a problem that can happen during program execution
but is recoverable. Java allows you to catch and handle exceptions
using try-catch blocks.

Analogy:
Think of an exception like a car breakdown. We can repair it and continue
our journey. The car doesn’t have to be scrapped.

Code Example (Exception):

public class Example {

public static void main(String[] args) {

try {

int a = 5 / 0; // ArithmeticException

} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero!");

Error (Unrecoverable Problems)

An Error is a serious issue that occurs outside the control of the program
and usually cannot be handled by the application.

They are not meant to be caught or handled by your code.

Examples:

StackOverflowError: Infinite recursion


OutOfMemoryError: JVM runs out of memory
InternalError: Something failed deep in JVM

Analogy:

An Error is like the engine exploding in your car. It’s not something you can
fix and continue driving—professional help or replacement is needed.

Code Example (Error):

public class StackOverflowExample {

public static void recursive() {

recursive(); // Infinite call causes StackOverflowError

public static void main(String[] args) {

recursive();

}
Differences Between Exception and Error

Feature Exception Error

Definition : Problems that can be Serious problems not


handled by the code meant to be handled

Type of issue : Logical/runtime System-level or JVM


issues issues

Recoverable? : Yes No

Subclasses: IOException, StackOverflowError,


NullPointerException OutOfMemoryError

Handling mechanism: Use try-catch or Usually not handled


throws in code

Common Cause : Bad code, invalid JVM bugs, hardware


input, etc. failure, memory full

Programmer’s role: Should handle Should avoid causing


properly errors; not catch
them

Occurs during: Program execution Often before or


during runtime

Types of Exceptions :

In Java programming, exceptions are broadly categorized into checked


exceptions and unchecked exceptions.

Unchecked Exceptions
Unchecked exceptions are a type of exception that are not checked by the
compiler at compile time. Instead, they occur during the execution of the
program. These exceptions typically arise from programming errors, such
as logic errors or improper use of an API.

Characteristics of Unchecked Exceptions

Not Checked at Compile Time: Unlike checked exceptions, the Java


compiler does not require programmers to handle or declare
unchecked exceptions.
Occur at Runtime: These exceptions typically manifest when the
program is running.
Result of Programming Errors: Unchecked exceptions often stem from
logic errors, incorrect use of an API, or other programming mistakes.

Common Examples of Unchecked Exceptions

1. ArithmeticException

Analogy: Dividing your total bill by 0 people to split it—nonsensical!

Cause: Division by zero.

public class Arithmetic {

public static void main(String[] args) {

try {

int a = 10;

int b = 0;

int result = a / b;

System.out.println("Result: " + result);

} catch (ArithmeticException e) {

System.out.println("You can't divide by zero!");

}
2. NullPointerException

Analogy: Asking a remote to change channels when it has no batteries—


null control!

Cause: Calling a method or accessing a property on a null object.

public class NULL {

public static void main(String[] args) {

try {

String str = null;

System.out.println(str.length()); // Causes NullPointerException

} catch (NullPointerException e) {

System.out.println("You are trying to access something that doesn't


exist!");

3. ArrayIndexOutOfBoundsException

Analogy: Trying to open the 6th drawer of a 5-drawer cabinet.

Cause: Accessing an array with an illegal index.

public class ArrayIndex {

public static void main(String[] args) {

try {

int[] numbers = {1, 2, 3};


System.out.println(numbers[5]); // Invalid index

} catch (ArrayIndexOutOfBoundsException e) {

System.out.println("Index does not exist in the array!");

4. StringIndexOutOfBoundsException

Analogy: Trying to read the 10th character from a 5-character word.

Cause: Accessing a character of a string with an invalid index.

public class Ex4 {

public static void main(String[] args) {

String s = "Java";

try {

System.out.println(s.charAt(10)); // Risky line

} catch (StringIndexOutOfBoundsException e) {

System.out.println("Error: You're trying to access an index that


doesn't exist in the string.");

System.out.println("Exception Message: " + e.getMessage());

5. NumberFormatException

Analogy: Trying to convert “apple” into a number.

Cause: Using Integer.parseInt() or similar on a non-numeric string.


public class Ex5 {

public static void main(String[] args) {

String s = "abc";

try {

int num = Integer.parseInt(s); // Risky operation

System.out.println("Converted number: " + num);

} catch (NumberFormatException e) {

System.out.println("Error: The string \"" + s + "\" is not a valid


number.");

System.out.println("Exception Message: " + e.getMessage());

6. ClassCastException

Analogy: Treating a car as if it were a bike—can’t happen.

Cause: Improper casting between unrelated types.

public class Ex6 {


public static void main(String[] args) {
Object obj = new Integer(10);
try {
String str = (String) obj; // This will trigger a ClassCastException
} catch (ClassCastException e) {
System.out.println("ClassCastException caught: " + e.getMessage());
}
}
}

7. IllegalArgumentException
Analogy: Asking someone to run 200 km in 5 minutes—it’s not valid.

Cause: Passing an inappropriate argument to a method.

public class Ex7 {


public static void main(String[] args) {
try {
Thread t = new Thread();
t.setPriority(10); // Valid priority ranges from 1 to 10
} catch (IllegalArgumentException e) {
System.out.println("Caught an exception: " + e.getMessage());
}
}
}

8. IllegalStateException

Analogy: Trying to unlock a car that's already unlocked.

Cause: Calling a method at an inappropriate time.

import java.util.Scanner;

public class Ex8 {


public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
sc.close();
sc.nextLine(); // This will throw an IllegalStateException
} catch (IllegalStateException e) {
System.out.println("Caught an IllegalStateException: " +
e.getMessage());
}
}
}

9. NegativeArraySizeException

Analogy: Saying “I need -5 chairs”—makes no sense.


Cause: Trying to create an array with a negative size.

public class Ex9 {


public static void main(String[] args) {
try {
int[] arr = new int[-10]; // This line will throw a
NegativeArraySizeException
} catch (NegativeArraySizeException e) {
System.out.println("Caught NegativeArraySizeException: " +
e.getMessage());
}
}
}

10. UnsupportedOperationException

Analogy: Trying to add toppings to a frozen pizza that can’t be opened.

Cause: When an operation is not supported, like modifying a fixed-size list.

import java.util.*;

public class Ex10 {


public static void main(String[] args) {
List<String> list = Arrays.asList("A", "B", "C");
try {
list.add("D"); // This line will throw an
UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Operation not supported: " + e.getMessage());
}
}
}

Understanding these analogies and causes, we can better anticipate and


handle exceptions in our Java programs, leading to more robust and error-
resistant code.

Checked Exceptions
Checked exceptions are a fundamental concept in Java programming that
ensure robust code by requiring developers to handle potential errors at
compile time.

Why the Name "Checked"?

The term "checked" comes from the fact that the Java compiler verifies
whether these exceptions are addressed during the compilation process. If
a checked exception is not handled, the program will not compile, ensuring
a level of error handling before runtime.

Common Checked Exceptions

Here are some prevalent checked exceptions you might encounter in Java:

Exception Name When it Occurs

IOException During input/output operations


like reading a file

FileNotFoundException When a file is not found

SQLException While working with databases

InterruptedException When thread execution is


interrupted

ParseException While parsing dates or strings

ClassNotFoundException When JVM can't find a class at


runtime

Real-Life Analogy

Think of checked exceptions like pre-flight safety checks on an airplane.


The pilot (developer) must address these issues before takeoff (code
compilation), or the flight (program) will not be permitted to proceed.

How to Handle Checked Exceptions


1. Using a try-catch Block

A try-catch block allows you to execute code that might throw an


exception and provides a way to handle the exception if it occurs. Here's
an example:

import java.io.*;

public class TryCatchExample {


public static void main(String[] args) {
try {
FileReader fr = new FileReader("myfile.txt");
} catch (FileNotFoundException e) {
System.out.println("File not found!");
}
}
}

2. Using the throws Keyword

Another approach is to use the throws keyword in the method signature,


indicating that the method might throw specific exceptions:

import java.io.*;

public class ThrowsExample {


public static void main(String[] args) throws FileNotFoundException {
FileReader fr = new FileReader("myfile.txt");
}
}

Difference: Checked vs. Unchecked Exceptions


Feature Checked Exceptions Unchecked
Exceptions

Compile-time check Yes No

Must handle? Yes No

Belongs to Exception (excluding RuntimeException


RuntimeException) and subclasses

Example IOException, NullPointerException,


SQLException ArrayIndexOutOfBou
ndsException

Checked Exception Examples :

Example 1: Reading a File (IOException)

This example demonstrates handling file reading operations that may


throw exceptions:

import java.io.*;

public class ReadFile {


public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(new
FileReader("notes.txt"));
String line = reader.readLine();
System.out.println(line);
reader.close();
} catch (IOException e) {
System.out.println("An IO error occurred: " + e.getMessage());
}
}
}

Example 2: Thread Sleep (InterruptedException)


Handling interruptions during thread execution can be crucial in multi-
threaded applications:

public class SleepDemo {


public static void main(String[] args) {
try {
System.out.println("Sleeping for 2 seconds...");
Thread.sleep(2000);
System.out.println("Awake now!");
} catch (InterruptedException e) {
System.out.println("Sleep was interrupted.");
}
}
}

By understanding and properly handling checked exceptions, developers


can create more reliable and error-resilient Java applications.

try and catch Block


Exception handling in Java is a powerful mechanism to handle runtime
errors, ensuring normal program flow. One of the most common ways to
handle exceptions in Java is using the try and catch blocks.

What is try-catch?
The try-catch construct in Java is used to handle exceptions that may
occur during the execution of a program. It helps to manage unexpected
errors without crashing the program.

try block: This is where you place the code that might throw an
exception.
catch block: This block contains the code to handle the exception that
the try block throws.

Real-Life Analogy

Imagine you’re cooking and you try to turn on the stove, but the gas is off.
You handle it by lighting a match. In this analogy:
TRY = Attempt to cook.
CATCH = Handle the gas-off situation.

In coding terms:

try = Risk zone (code that may fail).


catch = Rescue zone (what to do when failure happens).

Basic Syntax
Here's the basic syntax of a try-catch block in Java:

try {
// code that may throw an exception
} catch (ExceptionType e) {
// code to handle the exception
}

Why Do We Use try-catch?


Prevent program from crashing: Ensures the program does not
terminate unexpectedly.
Show meaningful error messages: Provides users with understandable
error messages.
Allow program to continue running safely: Helps maintain program flow
even when an error occurs.

Examples
Example 1: Divide by Zero

public class Example1 {


public static void main(String[] args) {
try {
int a = 10;
int b = 0;
int result = a / b;
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("You can't divide by zero!");
}
}
}

Output:

You can't divide by zero!

Example 2: Array Out of Bounds

public class Example2 {


public static void main(String[] args) {
try {
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // Invalid index
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Index does not exist in the array!");
}
}
}

Example 3: Null Pointer Exception

public class Example3 {


public static void main(String[] args) {
try {
String str = null;
System.out.println(str.length()); // Causes NullPointerException
} catch (NullPointerException e) {
System.out.println("You are trying to access something that doesn't
exist!");
}
}
}

Example 4: Multiple catch blocks

public class Example4 {


public static void main(String[] args) {
try {
String str = null;
int[] arr = new int[3];
System.out.println(arr[4]); // ArrayIndexOutOfBounds
System.out.println(str.length()); // This won't execute
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index error.");
} catch (NullPointerException e) {
System.out.println("Null pointer error.");
}
}
}

Detailed Concepts Behind try-catch


try Block

Holds risky code that is likely to throw an exception.


If an exception occurs, the rest of the try block is skipped.

catch Block

Handles specific types of exceptions such as ArithmeticException.


Must immediately follow the try block.
Multiple catch blocks can be used, but only one is executed per
exception.

Best Practices for Beginners


Tip Why It Matters

1.Always catch specific exceptions More accurate error handling


first

2.Don’t catch Exception unless Avoid hiding bugs


needed

3.Use meaningful messages inside Helps with debugging


catch

4.Don’t leave catch blocks empty You'll miss important errors

Common Mistakes and Fixes

Mistake Fix

1.try without catch or finally Must be followed by one

2.Code after exception inside try Control skips to catch


executes

3.Empty catch block Add at least a message or log

By understanding and using the try-catch blocks effectively, we can make


our Java programs more robust and user-friendly.

Multiple Catch Blocks in Java


Definition
In Java, a try block can be followed by multiple catch blocks, each tailored
to handle a particular type of exception. This structure allows us to
manage different error situations individually and effectively.

Why Use Multiple Catch Blocks?


Analogy

Consider a vending machine that might encounter various issues:

No electricity (power issue)


Item jammed (mechanical issue)
No stock (inventory issue)

Each problem requires a distinct solution, just as different exceptions need


specific handling.

Syntax of Multiple Catch Blocks


try {
// Code that might throw multiple exceptions
} catch (ExceptionType1 e1) {
// Handling code for ExceptionType1
} catch (ExceptionType2 e2) {
// Handling code for ExceptionType2
}
// ...more catch blocks

Note: Only the first matching catch block is executed. Subsequent blocks are ignored
once a match is found.

Examples
Example 1: Arithmetic and Array Exception

public class MultiCatchExample {


public static void main(String[] args) {
try {
int a = 10 / 0; // ArithmeticException
int[] arr = new int[5];
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero.");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index is out of bounds.");
}
}
}

Explanation:

The first exception (divide by zero) is caught.


The second line is never executed.
Only ArithmeticException is handled.

Example 2: Number Format and Null Pointer

public class MultiCatchExample2 {


public static void main(String[] args) {
String s = null;

try {
int num = Integer.parseInt("abc"); // NumberFormatException
System.out.println(s.length()); // NullPointerException
} catch (NumberFormatException e) {
System.out.println("Please enter a valid number.");
} catch (NullPointerException e) {
System.out.println("String is null, can't get length.");
}
}
}

Observation:

The first exception (NumberFormatException) is caught.


The second line is skipped.

Example 3: Common Parent Catch Block

public class MultiCatchHierarchy {


public static void main(String[] args) {
try {
String s = null;
System.out.println(s.length());
} catch (NullPointerException e) {
System.out.println("Caught NullPointerException");
} catch (Exception e) {
System.out.println("Caught general exception");
}
}
}

Key Rule:

Catch blocks must progress from specific to general.


Writing catch(Exception e) before a more specific exception causes a
compiler error: Unreachable catch block.

❌ Invalid Example (Incorrect Order)


// Compiler error: Unreachable catch block
try {
int a = 10 / 0;
} catch (Exception e) {
System.out.println("Generic exception");
} catch (ArithmeticException e) {
System.out.println("Arithmetic exception");
}

Summary Table

Keyword Meaning Example

try Code that might int a = 10 / 0;


throw an exception

catch Handle a specific type catch(ArithmeticExce


of exception ption e)

Multiple catch Handle different catch(NullPointerExc


exception types eption e) and
separately catch(NumberFormat
Exception e)
By understanding and effectively utilizing multiple catch blocks, we can
write robust and error-resistant Java applications.

throw and throws Keywords in Java


In Java, exception handling is a crucial part of writing robust and error-free
programs. Two keywords that play a significant role in this process are
throw and throws.

The throw Keyword


Definition:

The throw keyword is used within a method to manually throw an


exception, either predefined or custom. It is a way to signal that an error
has occurred and needs to be handled elsewhere in the program.

Analogy:

Imagine you're in a classroom and you spot a problem. You raise your hand
(i.e., throw the issue) to the teacher, initiating the problem report.

🧾 Syntax:
throw new ExceptionType("message");

Example 1: Throwing ArithmeticException

public class ThrowExample1 {


public static void main(String[] args) {
int age = 15;

if (age < 18) {


throw new ArithmeticException("You are not eligible to vote");
}

System.out.println("Welcome to voting booth");


}
}

Output:
Exception in thread "main" java.lang.ArithmeticException: You are not
eligible to vote

Example 2: Throwing NullPointerException

public class ThrowExample2 {


public static void main(String[] args) {
String name = null;

if (name == null) {
throw new NullPointerException("Name cannot be null");
}

System.out.println(name.length());
}
}

🔐 Key Notes about throw:


Only one exception: We can throw only one exception at a time using
throw.
Must be throwable: The object must be of type Throwable.
Used in method body: It is used inside methods or blocks.

The throws Keyword


Definition:

The throws keyword is used in method declarations to indicate that the


method might throw an exception, and the caller must handle it. It serves
as a warning to the method's users.

Analogy:

You’re a librarian who says, “This book has sharp edges (i.e., dangerous). Be
careful while reading it.” You’re passing the responsibility to the reader.

🧾 Syntax:
returnType methodName() throws ExceptionType {
// method code
}

Example 1: Method that throws IOException

import java.io.*;

public class ThrowsExample1 {


static void readFile() throws IOException {
FileReader file = new FileReader("test.txt");
BufferedReader fileInput = new BufferedReader(file);

System.out.println(fileInput.readLine());
fileInput.close();
}

public static void main(String[] args) {


try {
readFile();
} catch (IOException e) {
System.out.println("File not found or reading error.");
}
}
}

Example 2: Method throwing ArithmeticException

public class ThrowsExample2 {


static void divide(int a, int b) throws ArithmeticException {
System.out.println(a / b);
}

public static void main(String[] args) {


try {
divide(10, 0);
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero.");
}
}
}

🔁 throw vs throws – Difference Table


Feature throw throws

Purpose Used to explicitly Declares that a


throw an exception method may throw an
exception

Location Inside a In method signature


method/block

Number Throws one exception Can declare multiple


at a time exceptions

Object Needs an object of Doesn’t need object


Throwable type

Example throw new void myMethod()


ArithmeticException() throws IOException
;

🔚 Summary
Topic Explanation Example Key Notes

What is Runtime int a = 10/0; Handled using


Exception? problem that try-catch
disrupts normal
program flow

Exception vs Exception: Exception → Exceptions can


Error RecoverableErr NullPointerExc be handled;
or: eptionError → errors usually
Unrecoverable OutOfMemoryE cannot
rror

Checked Checked during FileReader f = Must be


Exception compile-time new handled using
FileReader("abc try-catch or
.txt"); throws

Unchecked Occurs during String s = null; Handling is


Exception runtime s.length(); optional

Common Various runtime See next table ↓ Occur if code


Unchecked exceptions has bugs or bad
Exceptions input

try-catch Block Handles try { int a = Write risky


exceptions 10/0; } code in try,
gracefully catch(Exceptio handling in
n e) {} catch

Multiple catch Handle catch(Arithmeti Order: specific


Blocks multiple types cException → general
of exceptions e)catch(Excepti
separately on e)

throw Keyword Used to throw throw new Inside method


an exception ArithmeticExce body
manually ption("Invalid
age");

throws Declares void read() Used in method


Keyword exceptions a throws signature
method might IOException {}
throw

Common Unchecked Exceptions


Exception When It Occurs Example Code

ArithmeticException Divide by zero int a = 10/0;

NullPointerException Calling method on str.length(); where str


null = null

NumberFormatExcept Invalid string to Integer.parseInt("abc"


ion number conversion );

ArrayIndexOutOfBou Invalid array index arr[10];


ndsException access

StringIndexOutOfBou Invalid index in string "abc".charAt(5);


ndsException

ClassCastException Invalid type casting String s = (String)


(Object)5;

IllegalArgumentExcep Illegal method Thread.sleep(-1000);


tion arguments

IllegalStateException Calling methods in Scanner used after


the wrong state closing

NegativeArraySizeExc Negative size array int[] a = new int[-2];


eption

InputMismatchExcept Invalid input type Entering a string for


ion with Scanner nextInt()

File Handling in Java


1. 📁 Introduction to File Handling
Explanation

File Handling in Java provides the ability to read from and write to files,
allowing data to be stored and retrieved from the disk. This is crucial for
persisting information, reading configurations, and logging application
data.

Analogy

Imagine a file as a notebook. Java provides tools (classes) like pens and
readers to write content into and read content from this notebook.

Why File Handling?

Storing Data Permanently: Data can be stored in files for long-term


access.
Reading Configurations: Configuration settings for applications can be
stored in files.
Logging Application Data: Logs can be maintained in files for debugging
and analysis.

Code Setup (Always Required)

import java.io.*; // For File, FileWriter, etc.

2. 📂 Working with File Class


Explanation

The File class in Java is used to represent file or directory paths. It


provides several methods to handle files and directories.

Methods:

exists(): Checks if the file or directory exists.


createNewFile(): Creates a new file if it doesn't exist.
mkdir() / mkdirs(): Creates a directory or directories.
delete(): Deletes a file or directory.
getName(), getAbsolutePath(), length(): Retrieves file details.

Example 1: Creating a File


import java.io.File;
import java.io.IOException;

public class CreateFile {


public static void main(String[] args) {
try {
File file = new File("demo.txt");
if (file.createNewFile()) {
System.out.println("File created: " + file.getName());
} else {
System.out.println("File already exists.");
}
} catch (IOException e) {
System.out.println("An error occurred.");
}
}
}

Example 2: File Information

File file = new File("demo.txt");


if (file.exists()) {
System.out.println("Name: " + file.getName());
System.out.println("Path: " + file.getAbsolutePath());
System.out.println("Size: " + file.length());
}

3. 📖 Reading Files – Text Input


A. Using Scanner Class

The Scanner class is simple and beginner-friendly for reading text files.

Code Example

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ReadWithScanner {


public static void main(String[] args) {
try {
File file = new File("demo.txt");
Scanner reader = new Scanner(file);
while (reader.hasNextLine()) {
String data = reader.nextLine();
System.out.println(data);
}
reader.close();
} catch (FileNotFoundException e) {
System.out.println("File not found.");
}
}
}

B. Using BufferedReader

BufferedReader is used for faster reading, especially useful for large files.

Code Example

import java.io.*;

public class ReadWithBufferedReader {


public static void main(String[] args) {
try {
BufferedReader br = new BufferedReader(new
FileReader("demo.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
} catch (IOException e) {
System.out.println("Error reading file.");
}
}
}
4. 📝 Writing Files – Text Output
A. Using FileWriter

FileWriter is used for simple writing to files.

Code Example 1: Write new file

import java.io.FileWriter;
import java.io.IOException;

public class WriteFile {


public static void main(String[] args) {
try {
FileWriter writer = new FileWriter("demo.txt");
writer.write("Hello, this is a Java file.\nWelcome to File Handling!");
writer.close();
System.out.println("Successfully written.");
} catch (IOException e) {
System.out.println("An error occurred.");
}
}
}

Code Example 2: Append mode

FileWriter writer = new FileWriter("demo.txt", true);


writer.write("\nThis is an appended line.");
writer.close();

B. Using BufferedWriter

BufferedWriter is efficient for writing multiple lines.

import java.io.*;

public class BufferedWrite {


public static void main(String[] args) {
try {
BufferedWriter bw = new BufferedWriter(new FileWriter("demo.txt",
true));
bw.write("Line 1");
bw.newLine();
bw.write("Line 2");
bw.close();
} catch (IOException e) {
System.out.println("Error writing file.");
}
}
}

5. 📤 FileInputStream & FileOutputStream


Explanation

These classes are used for reading and writing binary data byte-by-byte,
suitable for files like images and PDFs.

A. FileInputStream – Reading

import java.io.*;

public class BinaryRead {


public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("demo.txt");
int i;
while ((i = fis.read()) != -1) {
System.out.print((char) i);
}
fis.close();
} catch (IOException e) {
System.out.println("Error reading.");
}
}
}

B. FileOutputStream – Writing
import java.io.*;

public class BinaryWrite {


public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("output.txt");
String text = "Binary writing in Java.";
byte[] b = text.getBytes();
fos.write(b);
fos.close();
System.out.println("Written successfully.");
} catch (IOException e) {
System.out.println("Error writing.");
}
}
}

6. Best Practices, Error Handling & Recap

Best Practices

Always close file streams to release system resources.


Use try-with-resources for automatic closing of resources.
Handle exceptions using try-catch blocks to prevent application
crashes.
Avoid hardcoded file paths; use relative paths instead.

Summary Table
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