Complete Java Notes
Complete Java Notes
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.
Java was officially launched as Java 1.0 in 1995, marketed as a tool for crafting interactive
web content, with applets being particularly popular.
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.
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.
Execution Flow:
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:
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.
Each Object:
1. Occupies memory.
2. Has its own identity and state.
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:
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:
Syntax
class Book {
String title;
int price;
void show() {
System.out.println("Book: " + title + ", Price: " + price);
}
}
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);
}
}
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
Example:
class MathUtil {
static int square(int x) {
return x * x;
}
}
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:
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);
}
}
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.
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);
}
}
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;
}
}
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
}
}
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.
Example:
2. Inheritance:
Note:
This mechanism promotes code reuse, hierarchical classification, and extensibility.
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.
NOTE:
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.");
}
}
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.");
}
}
// 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.
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.");
}
}
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:
Abstract classes
Interfaces
Abstraction Using Abstract Classes
An abstract class is a class that cannot be instantiated and is meant to be extended by other
classes. It can contain:
Syntax:
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:
Syntax:
Note:
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.”
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...");
}
}
// 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.
interface Vehicle {
void start();
}
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.
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.”
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;
}
}
Note: The compiler determines which method to call based on the method signature at
compile time.
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.");
}
}
Note: The method call is determined during execution, not at compile time.
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:
Declaring a Package
package com.knmiet.utils;
Directory Structure:
src/com/knmiet/utils/MathUtils.java
1. Regular Import:
import com.knmiet.utils.MathUtils;
2. Static Import:
Used to access static members directly without needing the class name.
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.
try {
int a = 5 / 0; // ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero!");
An Error is a serious issue that occurs outside the control of the program
and usually cannot be handled by the application.
Examples:
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.
recursive();
}
Differences Between Exception and Error
Recoverable? : Yes No
Types of 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.
1. ArithmeticException
try {
int a = 10;
int b = 0;
int result = a / b;
} catch (ArithmeticException e) {
}
2. NullPointerException
try {
} catch (NullPointerException e) {
3. ArrayIndexOutOfBoundsException
try {
} catch (ArrayIndexOutOfBoundsException e) {
4. StringIndexOutOfBoundsException
String s = "Java";
try {
} catch (StringIndexOutOfBoundsException e) {
5. NumberFormatException
String s = "abc";
try {
} catch (NumberFormatException e) {
6. ClassCastException
7. IllegalArgumentException
Analogy: Asking someone to run 200 km in 5 minutes—it’s not valid.
8. IllegalStateException
import java.util.Scanner;
9. NegativeArraySizeException
10. UnsupportedOperationException
import java.util.*;
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.
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.
Here are some prevalent checked exceptions you might encounter in Java:
Real-Life Analogy
import java.io.*;
import java.io.*;
import java.io.*;
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:
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
}
Examples
Example 1: Divide by Zero
Output:
catch Block
Mistake Fix
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
Explanation:
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:
Key Rule:
Summary Table
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");
Output:
Exception in thread "main" java.lang.ArithmeticException: You are not
eligible to vote
if (name == null) {
throw new NullPointerException("Name cannot be null");
}
System.out.println(name.length());
}
}
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
}
import java.io.*;
System.out.println(fileInput.readLine());
fileInput.close();
}
🔚 Summary
Topic Explanation Example Key Notes
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.
Methods:
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;
B. Using BufferedReader
BufferedReader is used for faster reading, especially useful for large files.
Code Example
import java.io.*;
import java.io.FileWriter;
import java.io.IOException;
B. Using BufferedWriter
import java.io.*;
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.*;
B. FileOutputStream – Writing
import java.io.*;
Best Practices
Summary Table
Task Class Used
File handling operations in Java can fail for several reasons. Here are some
common scenarios where exceptions might occur:
import java.io.*;
Explanation
Multithreading in Java
What is a Thread?
What is 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.
Single Thread:
----------------
| Task A | Task B | Task C |
----------------
Multithreaded:
-----------------
| A1 | B1 | A2 | B2 | A3 | C1 |
-----------------
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:
What is a Thread?
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
Code Example
3. Running State
Explanation
Analogy
You were in the queue, and now it's your turn at the counter—you're
actively being served.
Code Example
Explanation
These are non-runnable states where a thread waits for some event (like
I/O, sleep, or lock).
Analogy
Being in a waiting room—you’re not done, just on hold for some condition.
Explanation
Analogy
Like a completed online exam—you submitted it, and it’s over. You can’t go
back.
Code Example
1. start()
Purpose:
Analogy:
Think of pressing the "start" button on a treadmill—it begins moving on its
own, independently.
Example:
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:
3. sleep(milliseconds)
Purpose:
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:
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:
Analogy:
Waiting for your friend to finish talking before you start.
Example:
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:
Purpose:
Sets or retrieves the name of the thread.
Explanation:
Helpful for debugging and logging.
Example:
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:
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:
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:
t.start();
t.interrupt(); // Interrupt the thread
}
}
Summary Table
Method Description Use Case
Thread Priorities
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;
Inter-Thread Communication in
}
Java
What is Inter-Thread Communication?
🤔 Problem
Consider a scenario with two threads:
Solution
Analogy
🔒 Important:
These methods must be called within a synchronized block, or Java will
throw an IllegalMonitorStateException.
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();
}
}
}
📊 Summary Table
Keyword Purpose Must Be Used In
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);
}
}
Daemon Threads
Summary
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
Analogy
Imagine labeling a folder as "Only One Document Allowed." If someone attempts to add more
than one document, they receive a warning.
@FunctionalInterface
interface Greeting {
void sayHello();
}
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);
}
Explanation
Syntax
@FunctionalInterface
interface Hello {
void say();
}
@FunctionalInterface
interface Operation {
void perform(int a, int b);
}
@FunctionalInterface
interface Square {
int findSquare(int n);
}
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;
3 Predicate
import java.util.function.Predicate;
4 Function<T, R>
import java.util.function.Function;
Explanation
When none of the built-in interfaces meet your needs, you can create your own.
Example
@FunctionalInterface
interface Converter {
int convert(String s);
}
Summary Table
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.
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
@FunctionalInterface
interface MyInterface {
void display();
}
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();
}
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
Example
@FunctionalInterface
interface MyInterface {
void display();
}
Comparison Table
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:
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");
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.
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:
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.
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;
Explanation:
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;
Explanation:
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:
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;
}
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;
In this program, the diamond operator is used with an anonymous class implementing the
Consumer interface, demonstrating how type inference results in cleaner code.
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:
This program demonstrates the creation and use of an anonymous class that implements the
Runnable interface to run a simple task.
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;
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:
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:
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:
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) {}
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:
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.
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.
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.
Note: Map is not a part of the Collection interface but is part of the framework.
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.
Collection
Examples
ArrayList Example
LinkedList Example
Stack Example
Examples
HashSet Example
LinkedHashSet Example
TreeSet Example
Set<Integer> ts = new TreeSet<>();
ts.add(30);
ts.add(10);
ts.add(20);
System.out.println(ts); // Outputs: [10, 20, 30]
Example
PriorityQueue Example
Examples
HashMap Example
LinkedHashMap Example
Summary Table
Analogy
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.*;
LinkedList
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.
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.
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.
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.
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.
TreeMap
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).
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.*;
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.
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.
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.
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.
import java.util.Queue;
import java.util.LinkedList;
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.");
}
}
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.
import java.util.*;
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.
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.
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.
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.
The poll() method retrieves and removes the head of the queue, which is the first element in
the sequence added.
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.
The peek() method retrieves, but does not remove, the head of the queue.
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.
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:
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:
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:
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:
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:
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
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.
// 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;
// Display the state of the deque after removing the last element
System.out.println(arrayDeque); // Outputs: [10]
}
}
Explanation of Operations:
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
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
// 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:
Types of Queues
Type Description
Example:
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).
LinkedList as Queue
LinkedList implements both Queue and Deque, allowing it to be used flexibly.
Example:
Basic Operations:
ArrayDeque as Stack:
ArrayBlockingQueue
LinkedBlockingQueue
Methods like put() and take() block if the queue is full or empty.
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.
import java.util.*;
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.
HashMap ❌ No ✅ Yes ❌ No
LinkedHashMa ✅ Insertion ✅ Yes ❌ No
p
Hashtable ❌ No ❌ No ✅ Yes
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()
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
});
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
LinkedHashMap
Code Example
TreeMap
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
Code Example
Map.Entry Interface
The Map.Entry interface allows retrieval of both key and value from a map,
particularly useful with entrySet().
Example
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.
import java.util.Arrays;
Sorting Strings
import java.util.Arrays;
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.
Returns:
Positive value → this > o
Zero → this == o
Negative value → this < o
Program Example
import java.util.*;
Comparator Interface
What is Comparator?
The Comparator interface is used when you can't modify the class or need multiple sort
criteria.
import java.util.*;
class Student {
int roll;
String name;
Properties Class
What is it?
Common Methods
getProperty(String key)
setProperty(String key, String value)
store(OutputStream, comment)
load(InputStream)
import java.util.*;
import java.io.*;
import java.util.*;
import java.io.*;
Summary Table
Definition:
Spring is a lightweight, open-source Java framework that helps in developing loose-coupled,
scalable, and testable enterprise applications.
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.
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>
Example: The Spring container is responsible for creating the Address object and injecting it
into the Student class, as shown in the previous example.
Key Concepts:
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
@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:
@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:
7. Lifecycle Callbacks
Lifecycle callbacks allow you to perform custom actions on bean initialization and
destruction.
@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.
@Configuration
public class AppConfig {
@Bean
public Student student() {
return new Student();
}
}
Spring Boot
1. Spring Boot Build Systems
Spring Boot supports popular build systems like Maven and Gradle, simplifying project setup
and dependency management.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
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.
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!";
}
}
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;
}
@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.