0% found this document useful (0 votes)
3 views79 pages

Java - Advanced

The document provides an overview of exception handling in Java, explaining the types of exceptions (checked, unchecked, and errors) and their hierarchy. It details how to catch exceptions using try-catch blocks, the use of finally blocks, and the try-with-resources statement for resource management. Additionally, it covers throwing exceptions, creating custom exceptions, and exception chaining, along with a brief introduction to generics in Java.

Uploaded by

Ahmed Eldeep
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)
3 views79 pages

Java - Advanced

The document provides an overview of exception handling in Java, explaining the types of exceptions (checked, unchecked, and errors) and their hierarchy. It details how to catch exceptions using try-catch blocks, the use of finally blocks, and the try-with-resources statement for resource management. Additionally, it covers throwing exceptions, creating custom exceptions, and exception chaining, along with a brief introduction to generics in Java.

Uploaded by

Ahmed Eldeep
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

Exceptions

Exceptions

Exceptions are events that occur during the execution of a


program that disrupt the normal flow of instructions. Java
provides a robust and flexible mechanism for handling errors and
other exceptional events, known as exception handling. It’s an
object that contains information about an error.

public class ExceptionsDemo {

public static void show() {


sayHello(null);
}

public static void sayHello(String name) {


[Link]([Link]());
}
}

Here the exception is an object of the NullPointerException class.


The info in this terminal is the stack trace. It shows the
methods that caused the error in the reverse order.
Exception in thread "main" [Link]: Cannot
invoke "[Link]()" because "name" is null​
​ at [Link]([Link])​
​ at [Link]([Link])​
​ at [Link]([Link])

A stack trace in Java is a report that provides a snapshot of the


call stack (a list of methods that were called) at the moment an
exception occurs. It is used to trace the sequence of method
calls that led to an error, helping developers to debug and
understand what went wrong in their code.

When an exception occurs in a method, we say that method throws


an exception and the JRE looks for any exception handling in any
of the called methods in the stack trace.
When JRE doesn’t find any exception handling code, it terminates
the program and displays the exception.
Types of Exceptions

1. Checked Exceptions:

●​ Description: Checked exceptions are exceptions that are


checked at compile time. This means that the compiler
ensures that the exception is either handled using a
try-catch block or declared using the throws keyword in the
method signature.
●​ When They Occur: They usually occur due to external factors
beyond the program's control (e.g., file I/O, network
access, or database operations).
●​ Handling: The programmer must handle these exceptions;
otherwise, the code will not compile.
●​ Examples:
○​ IOException: Thrown when an I/O operation fails or is
interrupted.
○​ SQLException: Thrown when there is a database access
error.
○​ FileNotFoundException: Thrown when an attempt to open a
file that doesn't exist fails.

2. Unchecked Exceptions (Runtime Exceptions):

●​ Description: Unchecked exceptions are not checked at compile


time, but instead occur at runtime. These exceptions usually
indicate programming errors.
●​ When They Occur: Unchecked exceptions often occur due to
invalid operations within the code, such as dividing by zero
or accessing an array out of bounds.
●​ Handling: It is optional for the programmer to handle
unchecked exceptions. The program will still compile even if
they are not caught or declared.
●​ Examples:
○​ NullPointerException: Thrown when attempting to use
null where an object is required.
○​ ArrayIndexOutOfBoundsException: Thrown when trying to
access an index of an array that is out of bounds.
○​ ArithmeticException: Thrown when an illegal arithmetic
operation, like dividing by zero, is performed.

3. Errors:

●​ Description: Errors are severe problems that typically


cannot be recovered from by the application. Errors usually
represent problems that are external to the application and
are not meant to be caught or handled by the program.
●​ When They Occur: Errors usually occur in scenarios where the
JVM (Java Virtual Machine) itself is not able to function
correctly, such as when memory is exhausted or the call
stack overflows.
●​ Handling: Errors are generally not meant to be caught, as
they indicate fundamental issues with the environment or the
JVM itself. They are abnormal conditions that the program
should not try to recover from.
●​ Examples:
○​ OutOfMemoryError: Thrown when the JVM runs out of
memory.
○​ StackOverflowError: Thrown when the call stack
overflows due to deep or infinite recursion.
○​ VirtualMachineError: Thrown when the JVM encounters a
serious problem, such as internal error or resource
limitations.
Exceptions Hierarchy

1. Throwable

●​ Description: The root class of the exception hierarchy. All


exceptions and errors derive from this class. It contains
the common characteristics for all exception classes such as
error messages and stack trace.
●​ Subclasses: Exception and Error.
2. Exception

●​ Description: The base class for all exceptions that a


program might want to catch. It represents the checked and
unchecked exceptions.
●​ Subclasses:RuntimeException and IOException and SQLException

3. Error
●​ Description: Represents serious problems that a reasonable
application should not try to catch. Errors are usually not
recoverable.
●​ Subclasses: OutOfMemoryError and StackOverflowError

4. RuntimeException (Subclass of Exception)

●​ Description: Represents exceptions that occur during the


runtime of the program. These are unchecked exceptions.
●​ Subclasses: ArithmeticException and NullPointerException
Catching Exceptions

Catching exceptions involves using a try-catch block to handle


exceptions that may occur during the execution of a program

●​ Try Block: Contains code that might throw an exception. This


block is where you place the code that you want to monitor
for exceptions.

●​ Catch Block: Catches and handles exceptions that occur in


the try block. You can have multiple catch blocks to handle
different types of exceptions.

public class ExceptionsDemo {


public static void show() {
try {
var reader = new FileReader("[Link]");
} catch (FileNotFoundException e) {
[Link]("File is not found");
[Link]([Link]());
}
}
}
Catching Multiple Types of Exceptions

You can catch multiple types of exceptions. This allows you to


handle various exceptions in a consolidated manner.

public class ExceptionsDemo {


public static void show() {
try {
var reader = new FileReader("[Link]");
var value = [Link]();
} catch (FileNotFoundException e) {
[Link]("Could not open file");
[Link]();
} catch (IOException e) {
[Link]("Could not read data");
}
}
}

Here, each catch block handles a different type of exception.


This approach allows for more specific handling of each exception
type if needed.

The order of the catch clauses matter because there are some
exception classes that inherit from other exception classes.

For example if we switched the catch clauses in the previous


example we would get an error `FileNotFoundException has already
been caught`.
public class ExceptionsDemo {
public static void show() {
try {
var reader = new FileReader("[Link]");
var value = [Link]();
} catch (IOException e) {
[Link]("Could not read data");
[Link]();
} catch (FileNotFoundException e) {
[Link]("Could not open file");
}
}
}

That happened because the `FileNotFoundException` inherits from


the `IOExceptions`, so if the IOException is catched first, it
means that the FileNotFoundException is already caught.
Another way to catch multiple exceptions in a single catch block
using the pipe | operator. This is useful when the exception
handling code is the same for different exceptions.

public class ExceptionsDemo {


public static void show() {
try {
FileReader reader = new FileReader("[Link]");
[Link]();
new SimpleDateFormat().parse("");
} catch (IOException | ParseException e) {
[Link]("An error occurred: " + [Link]());
}
}
}

The Finally Block

The finally block is used in conjunction with try and catch


blocks to ensure that certain code executes regardless of whether
an exception was thrown or not. The finally block is typically
used for cleanup operations such as closing files, releasing
resources, or restoring system states.
public class ExceptionsDemo {
public static void show() {
FileReader reader = null;
try {
reader = new FileReader("[Link]");
int data = [Link]();
} catch (IOException e) {
[Link]("An I/O error occurred: " + [Link]());
} finally {
if (reader != null) {
try {
[Link]();
} catch (IOException e) {
[Link]("Error closing file: " + [Link]());
}
}
}
}
}

The try-with-resources statement


It’s a simplified and more reliable way to handle resources that
need to be closed after use, such as files, sockets, or database
connections. It helps in managing resources more effectively by
automatically closing them, thus reducing the need for explicit
finally blocks to close resources.

●​ Automatic Resource Management: Resources are automatically


closed at the end of the try block, even if an exception
occurs.
●​ Simplified Code: Reduces boilerplate code by removing the
need for explicit finally blocks to close resources.
●​ Ensures Resource Closure: Helps prevent resource leaks by
ensuring that resources are closed properly.
●​ AutoCloseable Interface: The resource used in the
try-with-resources statement must implement the
AutoCloseable interface or its sub-interface
[Link]. This ensures that the resource can be
automatically closed.

public class ExceptionsDemo {


public static void show() {
try (FileInputStream input = new FileInputStream("[Link]");
FileOutputStream output = new FileOutputStream("[Link]")) {
int data;
while ((data = [Link]()) != -1) {
[Link](data);
}
} catch (IOException e) {
[Link]("An I/O error occurred: " + [Link]());
}
}
}
Throwing Exceptions
The throw keyword is used to explicitly throw an exception from a
method or a block of code and the caller of the method or the
block of code should handle that exception by himself.

When you use throw, you signal an exceptional condition and force
the control flow to move to the nearest matching catch block, or,
if none is present, to propagate the exception up the call stack.

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

1.​ Explicitly Throw an Exception: You can use throw to manually


throw an exception at any point in your program.
2.​ Creates Exception Objects: When throwing an exception, you
create an instance of an exception class using new, such as
new NullPointerException().
3.​ Must Be a Throwable Object: The object being thrown must be
an instance of a class that extends Throwable (either
Exception or Error).
public class Main {
public static void main(String[] args) {
try {
validateName(null);
} catch (NullPointerException e) {
[Link]("Caught exception: " + [Link]());
}
}

public static void validateName(String name) {


if (name == null) {
throw new NullPointerException("Name cannot be null");
}
[Link]("Name is valid");
}
}

Throwing Checked Exceptions


The previous example is ok when you throw an unchecked exception.
But when you try to throw a checked exception, the compiler will
complain about an unhandled exception.
Syntax: throw new IOException("");

To solve this problem, Java forces you to declare the exception


in the method signature using the throws keyword. The throws
clause is used to declare exceptions that a method might throw
but doesn't handle.
public class Main {
public static void main(String[] args) {
try {
readFile();
} catch (IOException e) {
[Link]("Caught IOException: " + [Link]());
}
}

public static void readFile() throws IOException {


throw new IOException("File not found");
}
}

Re-throwing Exceptions
After catching an exception, you might want to re-throw it to let
it be handled by another catch block higher up the call stack.
public class Main {
public static void main(String[] args) {
try {
processFile();
} catch (IOException e) {
[Link]("Handled in main: " + [Link]());
}
}
public static void processFile() throws IOException {
try {
throw new IOException("Processing error");
} catch (IOException e) {
[Link]("Logging exception: " + [Link]());
throw e; // Re-throwing the exception
}
}
}
Custom Exceptions
you can create custom exceptions to handle specific error
conditions that are unique to your application. Custom exceptions
allow you to define meaningful exceptions for your application’s
domain, making error handling more intuitive and readable.

Steps to Create a Custom Exception

1.​Create a class that extends the Exception class (for a


checked exception) or RuntimeException class (for an
unchecked exception).
2.​Provide constructors to initialize the exception with a
custom error message or cause.
3.​(Optional) Add custom methods or fields to provide
additional context or functionality to the exception.

Custom Checked Exception


class InvalidAgeException extends Exception {
public InvalidAgeException() {
super("Age is not valid!!!!!");
}

public InvalidAgeException(String message) {


super(message);
}
}

public class Main {


public static void main(String[] args) {
try {
validateAge(15);
} catch (InvalidAgeException e) {
[Link]("Caught exception: " + [Link]());
}
}

public static void validateAge(int age) throws InvalidAgeException {


if (age < 18) {
throw new InvalidAgeException("Age must be 18 or older");
}
[Link]("Age is valid");
}
}

Custom Unchecked Exception (RuntimeException)


class InvalidInputException extends RuntimeException {
public InvalidInputException(String message) {
super(message);
}
}

public class Main {


public static void main(String[] args) {
try {
processInput(-1); // This will throw InvalidInputException
} catch (InvalidInputException e) {
[Link]("Caught exception: " + [Link]());
}
}

public static void processInput(int input) {


if (input < 0) {
throw new InvalidInputException("Input cannot be negative");
}
[Link]("Input is valid");
}
}

Chaining Exceptions

Exception Chaining allows you to relate one exception to another.


This is useful when an exception occurs as a direct result of
another exception. By chaining exceptions, you can keep track of
the root cause of a problem while providing a more specific or
contextual error message for the current layer of code.

class AccountException extends Exception {


public AccountException(Exception cause) {
super(cause);
}
}

class InsufficientFundsException extends Exception {


public InsufficientFundsException() {
super("Insufficient Fund Value!!!!");
}
}

public class Main {

public static void withdraw(float value) throws AccountException {


if (value > 1000)
throw new AccountException(new InsufficientFundsException());
}

public static void main(String[] args) {


try {
withdraw(50000);
} catch (AccountException e) {
[Link]();
}
}
}

Generics
Generics are a powerful feature that allows you to write code
that is flexible, reusable, and type-safe. By using generics, you
can create classes, interfaces, and methods that operate on a
specific data type, without having to define the exact data type
at the time of writing the code.

Why Use Generics?

1.​Type Safety: Generics provide compile-time type checking,


reducing the likelihood of ClassCastException at runtime.
2.​Code Reusability: You can write generic algorithms or data
structures (like collections) that work with any type of
object.
3.​Cleaner Code: By using generics, you eliminate the need for
casting and make the code more readable.

Basic Syntax of Generic Classes

Generics are typically represented by type parameters in angle


brackets (<>), such as <T>, where T is a placeholder for the
actual type that will be specified later.

class Box<T> {
private T item;

public void setItem(T item) {


[Link] = item;
}

public T getItem() {
return item;
}
}

Here, Box is a generic class that works with any type T. The
actual type is specified when an object of Box is instantiated.
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
[Link]("Hello Generics");
[Link]([Link]());

Box<Integer> intBox = new Box<>();


[Link](123);
[Link]([Link]());
}
}

Note: Generic classes only hold reference types such as all


wrapper classes (Integer, String, Float, Boolean), and user
defined classes and interfaces such as (User, Car) and so on.

If you pass a primitive type to a generic class, you get a


compile time error.

Primitive Types: are the most basic data types built into Java.
They hold simple values (e.g., integers, floating-point numbers,
booleans).

Reference Types: are more complex data types in Java. Instead of


holding the actual value, they store a reference (or memory
address) that points to where the object or array is located in
memory. Reference types include classes, interfaces, arrays, and
enum types.

Autoboxing: The automatic conversion of a primitive type to its


corresponding wrapper class.

int x = 10; Integer y = x; // Autoboxing: int to Integer

Unboxing: The automatic conversion of a wrapper class back to its


corresponding primitive type.

Integer a = 10; int b = a; // Unboxing: Integer to int

Generics Constraints
Generic constraints (or bounded type parameters) are used to
restrict the types that can be used as type arguments in generic
classes, interfaces, or methods. This allows you to ensure that
the generic type parameter adheres to specific constraints,
thereby providing more control and safety when working with
generics.

class Box<T extends Number> {


private T item;

public void setItem(T item) {


[Link] = item;
}

public T getItem() {
return item;
}
}

public class Main {


public static void main(String[] args) {
Box<Double> doubleBox = new Box<>();
[Link](10.45);
[Link]([Link]());

Box<Integer> intBox = new Box<>();


[Link](123);
[Link]([Link]());
}
}

Type Erasure is a process in Java generics that occurs during


compilation. It refers to the way Java handles generic types by
removing or "erasing" the type information when the code is
compiled into bytecode. This allows Java to maintain backward
compatibility with older versions that did not support generics
(i.e., versions before Java 5).

How Type Erasure Works

When you write generic code, the type parameters (e.g., <T>, <E>,
<K, V>) are only known at compile-time. Once the code is
compiled, the type information is removed, and the resulting
bytecode operates with raw types (non-generic versions of the
types).

After Compilation (Type Erasure Applied)

During compilation, Java removes the type parameters and replaces


them with their bound (or Object if no specific bound is
provided). Here's what the code looks like internally after type
erasure:

Comparable Interface
The Comparable interface in Java is used to define a natural
ordering for objects of a class. It provides a way for objects to
be compared with each other, which is essential when sorting or
ordering collections of objects.

Key Points

●​ Comparable is found in the [Link] package, so it's


available without needing to import it explicitly.
●​ The interface has only one method: compareTo(T o), which
must be implemented by any class that implements Comparable.

Syntax of the Comparable Interface.

public interface Comparable<T> {


public int compareTo(T o);
}

T is the type of objects that this object may be compared with.

The compareTo method compares the current object with the


specified object o and returns:

●​ (-1) A negative integer if the current object is less than


the specified object.
●​ (0) Zero if the current object is equal to the specified
object.
●​ (1) A positive integer if the current object is greater than
the specified object.

In the following example:

●​ The Student class implements the Comparable<Student>


interface.
●​ The compareTo method compares two Student objects by their
age. It returns a negative value if this student’s age is
less than the other student’s age, zero if they are equal,
and a positive value if it’s greater.

Example of Implementing Comparable


class Student implements Comparable<Student> {​
private String name;​
private int age;​

public Student(String name, int age) {​
[Link] = name;​
[Link] = age;​
}​

public String getName() {​
return name;​
}​

public int getAge() {​
return age;​
}​

// Implementing the compareTo method to compare students by age​
@Override​
public int compareTo(Student other) {​
return [Link] - [Link];​
}​
}

public class Main {​


public static void main(String[] args) {​
Student st1 = new Student("Ali", 25);​
Student st2 = new Student("Amr", 30);​
if ([Link](st2) < 0)​
[Link]("Ali is younger than Amr");
else if ([Link](st2) == 0)​
[Link]("Ali is the same age as Amr");
else​
[Link]("Ali is older Amr");​
}​
}

Generic Methods
Generic methods in Java allow you to write methods that operate
on different types while maintaining type safety. They are
similar to generic classes but apply to individual methods. By
using generics in methods, you can create more flexible and
reusable code.

public class Utils {​


public static <T extends Comparable<T>> T max(T first, T second) {​
return ([Link](second) > 0) ? first : second;​
}​
}

We used `extends Comparable<T>` to be able to compare objects of


type T because the operator >, < won’t work with T because they
don’t know the type of the arguments.

public class Main {​


public static void main(String[] args) {​
[Link]([Link](10, 25));​
[Link]([Link]("Ahmed", "Eldeeep"));​
[Link]([Link](new Student("ahmed", 25), new Student("Amr", 30)));​
}​
}

Multiple Type Parameters

Generic methods and classes can have multiple type parameters.


This allows for more flexibility by enabling you to work with
multiple types while maintaining type safety. Each type parameter
is specified inside angle brackets (<>), and multiple type
parameters are separated by commas.

public class ClassName<T, U> {​


// Class definition using multiple types​
}​
public <T, U> void methodName(T param1, U param2) {​
// Method body​
}
Example on Multiple Type Parameter method
public static <K, V> void print(K key, V value) {​
[Link](key + " = " + value);​
}

public class Main {​


public static void main(String[] args) {​
[Link]("Some Key", 150);​
}​
}

Wild Cards

In Java, wildcards are a feature of generics that allow you to


work with unknown types in a more flexible way. They are
represented by a question mark (?) and are useful when you don’t
know the exact type or want to be more flexible about the types
that your code can accept. Wildcards are particularly helpful
when you want to specify that a generic type can be any type or a
range of types.

Types of Wildcards

Java provides three types of wildcards:

1.​Unbounded Wildcards (?): Represents an unknown type.


2.​Upper Bounded Wildcards (? extends T): Restricts the unknown
type to be a subclass (or the same type) of a specific class
T.
3.​Lower Bounded Wildcards (? super T): Restricts the unknown
type to be a superclass (or the same type) of a specific
class T.

1. Unbounded Wildcards (?)


The unbounded wildcard ? means "any type" and is used when you
don't care about the specific type.

public class Main {​


public static void printList(List<?> list) {​
for (Object element : list) {​
[Link](element);​
}​
}​

public static void main(String[] args) {​
List<Integer> intList = [Link](1, 2, 3);​
List<String> strList = [Link]("A", "B", "C");​

printList(intList); // Works with List<Integer>​
printList(strList); // Works with List<String>​
}​
}

2. Upper Bounded Wildcards (? extends T)

An upper bounded wildcard (? extends T) restricts the unknown


type to be a subtype of T. This is useful when you want to read
from a generic collection but not modify it.

public class UpperBoundedWildcardExample {​


public static double sumOfNumbers(List<? extends Number> list) {​
double sum = 0.0;​
for (Number num : list) {​
sum += [Link]();​
}​
return sum;​
}​

public static void main(String[] args) {​
List<Integer> intList = [Link](1, 2, 3);​
List<Double> doubleList = [Link](1.5, 2.5, 3.5);​

[Link]("Sum of Integers: " +
sumOfNumbers(intList));​
[Link]("Sum of Doubles: " +
sumOfNumbers(doubleList));​
}​
}

3. Lower Bounded Wildcards (? super T)

A lower bounded wildcard (? super T) restricts the unknown type


to be a superclass of T. This is useful when you want to modify a
generic collection by adding elements but don't care about
reading from it.

public class LowerBoundedWildcardExample {​


public static void addNumbers(List<? super Integer> list) {​
[Link](1);​
[Link](2);​
[Link](3);​
}​

public static void main(String[] args) {​
List<Number> numberList = new ArrayList<>();​
addNumbers(numberList);​

[Link](numberList); // Output: [1, 2, 3]​
}​
}

Collections
The Collections Framework in Java is a unified architecture for
representing and manipulating collections of objects. It provides
a set of interfaces, classes, and algorithms to store, retrieve,
manipulate, and communicate aggregate data. The framework is part
of the Java Standard Library and was introduced in Java 2 (JDK
1.2) to simplify the handling of collections such as arrays,
lists, sets, and maps, and to make them more flexible, efficient,
and consistent.

Green are Interfaces. Blue are Classes.

All interfaces in the next pages are implemented by classes which


you can use. So you can make a variable of the interface type but
you have to instantiate the variable with the class type that
implements that interface (Polymorphism).

For example: List<String> list = new ArrayList<>();

Iterable Interface
It represents a collection of objects that can be iterated (i.e.,
traversed) one by one. Any class that implements the Iterable
interface allows its objects to be the target of the enhanced for
loop (for-each loop), making it easier to iterate over elements
without using traditional loops or manually managing an iterator.

It’s implemented by All Collection Classes: All collection


classes in Java (like List, Set, Queue, etc.) implement the
Iterable interface. Therefore, they support the for-each loop and
provide an Iterator for traversing elements.

`ArrayList` is a built-in collection that has already implemented


the `Iterable` interface and implements the iterator method with
an inner `Iterator` class.

public class IterableExample {​


public static void main(String[] args) {​
List<String> fruits = new ArrayList<>();​
[Link]("Apple");​
[Link]("Banana");​
[Link]("Cherry");​

// Using a for-each loop (enhanced for loop)​
for (String fruit : fruits) {​
[Link](fruit);​
}​

// Manual iteration using an Iterator​
Iterator<String> iterator = [Link]();​
while ([Link]()) {​
[Link]([Link]());​
}​
}​
}

Methods of the Iterable Interface:


1.​Iterator<T> iterator():
○​ This is the main method of the interface. It returns an
Iterator that can be used to iterate over the elements
in the collection.
2.​default void forEach(Consumer<? super T> action):
○​ It performs the specified action for each element of
the collection.
3.​default Spliterator<T> spliterator():
○​ It creates a Spliterator over the elements in the
collection, enabling parallel iteration.

Methods of the Iterator Interface:

The Iterator object returned by iterator() provides three main


methods:

1.​boolean hasNext(): Checks if the collection has more


elements to iterate over.
2.​T next(): Returns the next element in the collection.
3.​void remove(): Removes the last element returned by next()
from the collection (optional operation).

To use the Iterable interface in a user-defined class, you need


to implement the interface and override its iterator() method,
which should return an Iterator object. The Iterator is
responsible for providing the mechanism to iterate through the
elements in your class.

Here's a step-by-step guide and an example to demonstrate how to


use the Iterable interface in a user-defined class.

Steps:
1.​Implement the Iterable Interface: Your class should
implement the Iterable interface and provide an
implementation for the iterator() method.
2.​Create an Inner Iterator Class: Define an inner class that
implements the Iterator interface. This class will manage
the traversal of the elements in your class.
3.​Override iterator() Method: The iterator() method should
return an instance of the Iterator class.
4.​Implement the Iterator Methods: In the inner Iterator class,
you must implement the following methods:
○​ boolean hasNext(): Returns true if there are more
elements to iterate over.
○​ T next(): Returns the next element in the collection.

import [Link];​

public class CustomList implements Iterable<Integer> {​
private int[] numbers;​
private int size;​

public CustomList(int size) {​
[Link] = size;​
[Link] = new int[size];​
}​

public void add(int index, int value) {​
if (index >= 0 && index < size) {​
numbers[index] = value;​
}​
}​

@Override​
public Iterator<Integer> iterator() {​
return new CustomIterator();​
}​


private class CustomIterator implements Iterator<Integer> {​
private int currentIndex = 0;​

@Override​
public boolean hasNext() {​
return currentIndex < size;​
}​

@Override​
public Integer next() {​
return numbers[currentIndex++];​
}​
}​

public static void main(String[] args) {​
CustomList customList = new CustomList(5);​

[Link](0, 10);​
[Link](1, 20);​
[Link](2, 30);​
[Link](3, 40);​
[Link](4, 50);​

for (int num : customList) {​
[Link](num);​
}​
}​
}

Another Example `GenericList`

public class GenericList<T> implements Iterable<T> {​


private T[] items = (T[]) new Object[10];​
private int count;​

public void add(T item) {​
[Link][[Link]++] = item;​
}​
public T get(int index) {​
return items[index];​
}​

@Override​
public Iterator<T> iterator() {​
return new ListIterator(this);​
}​

private class ListIterator implements Iterator<T> {​
private GenericList<T> list;​
private int index;​
public ListIterator(GenericList<T> list) {​
[Link] = list;​
}​
@Override​
public boolean hasNext() {​
return ([Link] < [Link]);​
}​
@Override​
public T next() {​
return [Link][index++];​
}​
}​
}

public class Main {​


public static void main(String[] args) {​
GenericList<String> list = new GenericList<>();​
[Link]("A");​
[Link]("B");​
[Link]("C");​
[Link]("D");​
Iterator<String> iterator = [Link]();​
while ([Link]()) {​
[Link]([Link]());​
}​
}​
}
Collection Interface

It provides the standard methods for adding, removing, and


querying objects in a collection, making it the foundation of
other collection types like List, Set, and Queue. All the
collections that implement this interface handle groups of
objects in a structured way. The Collection interface defines
several important methods that all implementing classes must
override.

Note: Search for Collection interface methods on ChatGPT.

import [Link];​
import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
Collection<String> collection = new ArrayList<>();​
[Link]("A");​
[Link]("B");​
[Link](collection, "a", "b", "c");​
[Link](collection);​
[Link]([Link]("b"));​
[Link]([Link]("a"));​
[Link]();​
String[] strArray = [Link](new String[0]);​
[Link]([Link]);​
}​
}

List Interface

The List interface in Java is part of the Java Collections


Framework and represents an ordered collection of elements.
Unlike other collections like Set, which does not allow duplicate
elements, the List interface permits duplicate elements and
maintains the insertion order of elements. It provides various
methods to access, modify, and search for elements in a
collection by their index.
Key Characteristics of the List Interface:

1.​Ordered Collection: Elements are stored in a specific


sequence (insertion order). This allows access to elements
by their position (index) in the list.
2.​Allows Duplicates: A List can contain duplicate elements.
This is a key difference between List and Set.
3.​Indexed Access: You can access, add, or remove elements
based on their position (index) in the list. The first
element has index 0, the second element has index 1, and so
on.
4.​Generic: Like other collection types, List is generic
(List<E>), meaning it can store elements of any type (E is
the type of elements stored in the list).

Note: Search for List interface methods on ChatGPT.

import [Link];​
import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
List<String> list = new ArrayList<>();​
[Link]("A");​
[Link]("B");​
[Link]("C");​
[Link](0, "V");​
[Link](list, "F", "G", "H");​
[Link](0, "A");​
[Link](2);​
[Link]([Link]("F"));​
[Link]([Link]("A"));​
[Link]([Link](0, 3));​
[Link](list);​
}​
}
The comparable Interface

The Comparable interface in Java is used to define a natural


ordering for objects of a class. When a class implements the
Comparable interface, it enables objects of that class to be
compared to each other. This comparison allows objects to be
sorted in a natural order, such as numerically or alphabetically.

Comparable Interface Definition:

The Comparable interface is part of the [Link] package, and


its single method compareTo() is used to compare the current
object with another object of the same type.

public interface Comparable<T> {


int compareTo(T other);
}

The compareTo() method compares the current object with the


specified object (o). It returns:

●​ A negative integer: if the current object is less than the


specified object.
●​ Zero: if the current object is equal to the specified
object.
●​ A positive integer: if the current object is greater than
the specified object.

public class Customer implements Comparable<Customer> {​


private String name;​

public Customer(String name) {​
[Link] = name;​
}​

@Override​
public int compareTo(Customer other) {​
return [Link]([Link]);​
}​

@Override​
public String toString() {​
return [Link];​
}​
}

import [Link];​
import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
List<Customer> customers = new ArrayList<>();​
[Link](new Customer("C"));​
[Link](new Customer("A"));​
[Link](new Customer("B"));​
[Link](customers);​
[Link](customers);​
}​
}

The Comparator Interface

The Comparator interface in Java is used to define custom sorting


logic for objects that do not have a natural ordering or when you
want to sort objects in a way that differs from their natural
ordering. While the Comparable interface defines the natural
order for a class, Comparator allows you to create multiple
sorting sequences or custom comparisons.

Unlike Comparable, which is implemented within the class whose


objects you want to sort, Comparator is implemented as a separate
class or as an anonymous class. This makes Comparator more
flexible because you can define multiple ways to compare objects
without modifying the class itself.
Key Characteristics of the Comparator Interface:

●​ Custom Sorting Logic: You can define multiple different


sorting behaviors for the same type of object.
●​ External to the Class: Comparators are separate from the
class being sorted, allowing you to define custom orderings
without changing the object's class itself.
●​ Multiple Comparators: You can create several Comparator
implementations to sort objects by different criteria (e.g.,
by name, by age, by salary, etc.).

The Comparator interface has two important methods:

1.​compare(T o1, T o2): This method compares two objects (o1


and o2) and returns:
○​ A negative integer: if o1 is less than o2.
○​ Zero: if o1 is equal to o2.
○​ A positive integer: if o1 is greater than o2.

public class Customer implements Comparable<Customer> {​


private String name;​
private String email;​

public String getEmail() {​
return email;​
}​

public Customer(String name, String email) {​
[Link] = name;​
[Link] = email;​
}​

@Override​
public int compareTo(Customer other) {​
return [Link]([Link]);​
}​

@Override​
public String toString() {​
return [Link];​
}​
}

import [Link];​

public class EmailComparator implements Comparator<Customer> {​

@Override​
public int compare(Customer o1, Customer o2) {​
return [Link]().compareTo([Link]());​
}​
}

import [Link];​
import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
List<Customer> customers = new ArrayList<>();​
[Link](new Customer("C", "E1"));​
[Link](new Customer("A", "E3"));​
[Link](new Customer("B", "E2"));​
[Link](customers, new EmailComparator());​
[Link](customers);​
}​
}
The Queue Interface

The Queue interface in Java is a part of the Java Collections


Framework and represents a data structure that follows the
First-In-First-Out (FIFO) principle. A queue is typically used to
hold elements that are waiting to be processed, where the first
element added is the first one to be removed. However, there are
exceptions like PriorityQueue, which may not follow FIFO due to
its nature of prioritizing elements based on their natural
ordering or a specified comparator.

Key Characteristics of the Queue Interface:

●​ FIFO: Elements are processed in the order they are added


(first in, first out).
●​ Addition at the end, Removal from the front: Elements are
added to the tail (end) of the queue and removed from the
head (front).
●​ Multiple Implementations: Different implementations like
LinkedList, PriorityQueue, and ArrayDeque provide various
ways to manage the queue.

public class Main {​


public static void main(String[] args) {​
Queue<String> queue = new ArrayDeque<>();​
[Link]("A");​
[Link]("B");​
[Link]("C");
// If no elements, return null​
[Link]([Link]());
// If no elements, throw Exception ​
[Link]([Link]()); ​
[Link]([Link]());​
[Link]([Link]());​
[Link](queue);​
}​
}

The Set Interface


It represents a collection that does not allow duplicate
elements. It models the mathematical set abstraction, which is an
unordered collection where each element is unique.

Key Characteristics of the Set Interface:

●​ No Duplicates: The primary feature of a Set is that it does


not allow duplicate elements. If you try to add a duplicate,
the existing element remains, and the add operation returns
false.
●​ Unordered Collection: The elements in a Set are not stored
in any particular order. However, some implementations like
LinkedHashSet maintain the insertion order.

import [Link].*;​
public class Main {​
public static void main(String[] args) {​
Set<String> set = new HashSet<>();​
[Link]("Sky");​
[Link]("is");​
[Link]("blue");​
[Link]("blue");​
[Link](set);​

Collection<String> collection = new ArrayList<>();​
[Link](collection, "A", "B", "C", "C");​
[Link](collection);​
Set<String> setColl = new HashSet<>(collection);​
[Link](setColl);​

Set<String> set1 = new HashSet<>([Link]("a", "b", "c"));​
Set<String> set2 = new HashSet<>([Link]("b", "c", "d"));​
[Link](set2); // Union​
[Link](set2); // Intersection​
[Link](set2); // Difference​
}​
}

The Map Interface


It represents a collection of key-value pairs, where each key is
unique and maps to a single value. Unlike Collection interfaces,
which only store values, Map provides a way to associate keys
with values.

Key Characteristics of the Map Interface:

●​ Key-Value Pairs: A Map stores entries where each entry


consists of a key and a corresponding value.
●​ Unique Keys: Each key in a Map must be unique. If you add a
new entry with a key that already exists, the old value
associated with that key is replaced.
●​ No Duplicate Keys: Maps do not allow duplicate keys, but
they can contain duplicate values.
●​ Efficient Lookup: Maps are designed to provide efficient
retrieval, insertion, and deletion operations based on keys.

public class Main {​


public static void main(String[] args) {​
Customer c1 = new Customer("A", "E1");​
Customer c2 = new Customer("B", "E2");​
Map<String, Customer> map = new HashMap<>();​
[Link]([Link](), c1);​
[Link]([Link](), c2);​
[Link](map);​
[Link]([Link]("E1"));​
[Link]([Link]("E45", new Customer("Unknown", "")));​
[Link]([Link]("E1"));​
[Link]([Link](c1));​
[Link]("E1", new Customer("AAA", "E12"));​
[Link](map);​
for (String key: [Link]())​
[Link](key);​
for (Customer value: [Link]())​
[Link](value);​
for (var entry: [Link]())​
[Link](entry);​
}​
}
Lambda Expressions and Functional Interfaces

Functional Interface

It is an interface that has exactly one abstract method. It can


have any number of default or static methods, but only one
abstract method is allowed. Functional interfaces are central to
functional programming in Java, particularly with the
introduction of lambda expressions.

@FunctionalInterface​
public interface Printer {​
void print(String message);​
}

Anonymous Inner Class

It is a type of inner class (a class declared within another


class) that is declared and instantiated in a single expression,
without giving the class a name. It is often used to provide an
implementation of an interface or to extend a class on the fly,
typically for short-lived uses where defining a separate named
class would be overkill.

public class Main {​


public static void main(String[] args) {​
greet(new Printer() {​
@Override​
public void print(String message) {​
[Link](message);​
}​
});​
}​
public static void greet(Printer printer) {​
[Link]("Hello World");​
}​
}
Lambda Expressions

They were introduced in Java 8 as a way to bring functional


programming capabilities to the language. They provide a clean,
concise way to represent a function (or behavior) that can be
passed around and executed, simplifying the syntax for working
with functional interfaces and enabling more readable and
maintainable code.

Lambda expressions allow you to create instances of functional


interfaces (interfaces with a single abstract method) without
having to write an entire class implementation, thus removing the
boilerplate of anonymous inner classes.

public class Main {​


public static void main(String[] args) {​
greet(message -> [Link](message));​
}​

public static void greet(Printer printer) {​
[Link]("Hello World");​
}​
}

public class Main {​


public static void main(String[] args) {​
Runnable runnable = () -> [Link]("Hello From Runnable");​
[Link]();​

Printer printer = message -> [Link](message);​
[Link]("Hello From Printer");​
}​
}
Method References

Method references in Java provide a way to refer to methods


directly by their names, instead of invoking them explicitly
through lambda expressions. They offer a more concise and
readable syntax when using lambda expressions, especially when an
existing method can be used for the functionality of the lambda.

ClassOrObject::methodName

●​ ClassOrObject: This can be a class name or an object


reference.
●​ methodName: The name of the method being referenced (without
parentheses).

Method references can replace lambda expressions when the lambda


is used solely to call a method. They are shorter and can make
the code more readable.

import [Link];​

public class Main {​
public static void main(String[] args) {​
BiFunction<Integer, Integer, Integer> adder = Math::addExact;​
[Link]([Link](10, 20));​
}​
}

●​ Math::addExact is a method reference to the static method


addExact of the Math class.
●​ The method reference replaces a lambda expression like (a,
b) -> [Link](a, b).
public class Main {​
public static void main(String[] args) {​
List<String> names = [Link]("Bob", "Charlie", "Alice");​
[Link](String::compareToIgnoreCase);​
[Link]([Link]::println);​
}​
}

public class Main {​


public static void print(String message) {}​

public static void show() {​
greet(message -> [Link](message));​
greet(Main::print);​
}​

public static void greet(Printer printer) {​
[Link]("Hello World");​
}​
}

public class Main {​


public void print(String message) {}​

public void show() {​
Main main = new Main();​
// The following 3 lines are identical​
greet(message -> [Link](message));​
greet(main::print);​
greet(this::print);​
}​

public static void greet(Printer printer) {​
[Link]("Hello World");​
}​
}
public class Main {​
public Main(String message) {​

}​

public void show() {​
greet(message -> new Main(message));​
greet(Main::new);​
}​

public static void greet(Printer printer) {​
[Link]("Hello World");​
}​
}

Built-in Functional Interfaces

Java 8 introduced a set of built-in functional interfaces in the


[Link] package. These functional interfaces simplify
common programming tasks by allowing developers to pass behaviors
(lambda expressions or method references) around as arguments. A
functional interface is an interface that has exactly one
abstract method, although it can have default and static methods
as well.

These functional interfaces are commonly used in lambda


expressions and the Streams API to perform operations like
filtering, transforming, or combining elements.

There are 4 categories of functional interfaces.


The consumer Interface

Consumer<T>: Represents an operation that accepts a single input


argument and returns no result. It is usually used for performing
operations like printing, modifying, or saving an object.

●​ Abstract Method: void accept(T t)

import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
List<Integer> list = [Link](1, 2, 3, 4);​
// You can create a consumer variable, then pass it​
Consumer<Integer> print0 = item -> [Link](item);​
Consumer<Integer> print1 = [Link]::println;​
[Link](print0);​
[Link](print1);​
// You can pass the consumer directly​
[Link](item -> [Link](item));​
[Link]([Link]::println);​
}​
}

Changing Consumer Using addThen()


It’s going to execute the chained methods on each item.
import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
List<String> letters = [Link]("a", "b", "c");​
Consumer<String> print = item -> [Link](item);​
Consumer<String> printUpper = item ->
[Link]([Link]());​
[Link]([Link](printUpper));​
}​
}
The Supplier Interface

Supplier<T>: Represents a function that provides a result (or


supplies an object). It takes no arguments and returns a value of
type T.

●​ Abstract Method: T get()

import [Link];​
import [Link];​
import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
Supplier<String> msg = () -> "Hello From Supplier";​
[Link]([Link]());​

Supplier<Double> rand0 = () -> [Link]();​
Supplier<Double> rand1 = Math::random;​
[Link]([Link]());​
[Link]([Link]());​

BooleanSupplier booleanSupplier = () -> true;​
[Link]([Link]());​

DoubleSupplier doubleSupplier = Math::random;​
[Link]([Link]());​

IntSupplier intSupplier = () -> 88;​
[Link]([Link]());​
}​
}
The Function Interface

Function<T, R>: Represents a function that accepts one argument


of type T and returns a result of type R. It is commonly used for
transforming or mapping input values to output values.

●​ Abstract Method: R apply(T t)

import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
Function<String, String> map0 = word -> [Link]();​
Function<String, String> map1 = String::toUpperCase;​
[Link]([Link]("ahmed"));​
[Link]([Link]("ahmed"));​

List<String> words = [Link]("hello", "func", "int");​
Function<String, Integer> function0 = item -> [Link]();​
Function<String, Integer> function1 = String::length;​
[Link](word -> [Link]([Link](word)));​
[Link](word -> [Link]([Link](word)));​
// BiFunction​
// IntFunction​
// ToIntFunction​
// IntToDoubleFunction​
}​
}

Composing Functions
import [Link];​

public class Main {​
public static void main(String[] args) {​
// Converting "key:value" to {key=value}​
Function<String, String> replaceColon = str -> [Link](":", "=");​
Function<String, String> addBraces = str -> "{" + str + "}";​
[Link]([Link](addBraces).apply("hello:world"));​
[Link]([Link](replaceColon).apply("hello:world"));​
}​
}
The Predicate Interface

Predicate<T>: Represents a boolean-valued function (a function


that returns a boolean result). It tests a condition on the given
input argument.

●​ Abstract Method: boolean test(T t)

import [Link];​

public class Main {​
public static void main(String[] args) {​
Predicate<Integer> isEven = n -> n % 2 == 0;​
[Link]([Link](4));​
[Link]([Link](3));​
}​
}

Combining Functions
import [Link];​

public class Main {​
public static void main(String[] args) {​
Predicate<String> hasLeftBrace = str -> [Link]("{");​
Predicate<String> hasRightBrace = str -> [Link]("}");​

Predicate<String> hasLeftAndRightBraces = [Link](hasRightBrace);​
Predicate<String> hasLeftOrRightBraces = [Link](hasRightBrace);​

[Link]([Link]("{hello}"));​
[Link]([Link]("hello}"));​
Predicate<String> notHaveLeftBrace = [Link]();​
}​
}
The BinaryOperator Interface

BinaryOperator<T>: A specialization of BiFunction<T, T, T> where


both the input arguments and the result are of the same type. It
represents an operation on two operands of the same type.

●​ Abstract Method: T apply(T t1, T t2)

import [Link];​

public class Main {​
public static void main(String[] args) {​
BinaryOperator<Integer> add0 = (a, b) -> a + b;​
BinaryOperator<Integer> add1 = Integer::sum;​
[Link]([Link](1, 2));​
[Link]([Link](1, 2));​
}​
}

We can’t use the BinaryOperator interface for the square


operation because the BinaryOperator interface takes 2 arguments
and the square operation wants only one argument.
import [Link];​
import [Link];​

public class Main {​
public static void main(String[] args) {​
BinaryOperator<Integer> add = (a, b) -> a + b;​
Function<Integer, Integer> square = a -> a * a;​
[Link]([Link](square).apply(8, 4));​
}​
}
The UnaryOperator Interface

UnaryOperator<T>: A specialization of Function<T, T> where both


the input and output types are the same. It represents an
operation on a single operand that produces a result of the same
type.

●​ Abstract Method: T apply(T t)

import [Link];​

public class Main {​
public static void main(String[] args) {​
UnaryOperator<Integer> square = n -> n * n;​
UnaryOperator<Integer> increment = n -> n + 1;​
[Link]([Link](increment).apply(4));​
}​
}

Streams

1. Imperative Programming

Imperative programming is a paradigm where you explicitly tell


the computer how to perform tasks, focusing on step-by-step
instructions and changes in program state. It is often more
concerned with the how (process) than the what (goal).

public class ImperativeExample {​


public static void main(String[] args) {​
List<Integer> numbers = [Link](1, 2, 3, 4, 5, 6);​
int sum = 0;​
for (int number : numbers) {​
if (number % 2 == 0) {​
sum += number;​
}​
}​
[Link]("Sum of even numbers: " + sum);​
}​
}

2. Declarative Programming

Declarative programming is a paradigm where you describe what you


want to achieve, without explicitly specifying how to do it. You
focus more on the outcome rather than the step-by-step process to
achieve it.

public class DeclarativeExample {​


public static void main(String[] args) {​
List<Integer> numbers = [Link](1, 2, 3, 4, 5, 6);​

// Declarative style: Sum all even numbers using Streams​
int sum = [Link]()​
.filter(number -> number % 2 == 0)​
.mapToInt(Integer::intValue)​
.sum();​

[Link]("Sum of even numbers: " + sum);​
}​
}

3. Functional Programming

Functional programming is a subset of declarative programming. It


focuses on pure functions, immutability, and higher-order
functions. Functional programming aims to treat computation as
the evaluation of mathematical functions, avoiding changing state
and mutable data

public class FunctionalExample {​


public static void main(String[] args) {​
List<Integer> numbers = [Link](1, 2, 3, 4, 5, 6);​
// Functional style: Sum all even numbers using pure functions and lambdas​
int sum = sumOfFilteredNumbers(numbers, n -> n % 2 == 0);
[Link]("Sum of even numbers: " + sum);​
}​

public static int sumOfFilteredNumbers(List<Integer> numbers,
Predicate<Integer> filterCondition) {​
return [Link]()​
.filter(filterCondition)​
.mapToInt(Integer::intValue)​
.sum();​
}​
}

Streams in Java, introduced in Java 8, are a part of the


[Link] package that provides a powerful, flexible, and
declarative way to process collections of data. The Stream API
helps to work with sequences of elements (such as arrays, lists,
or sets) and perform aggregate operations like filtering,
mapping, reducing, and more, in a functional programming style.

Unlike traditional loops, streams allow developers to express


complex data-processing queries in a readable, efficient, and
composable way. Streams can also operate in parallel, which can
lead to better performance on multicore processors.
Key Characteristics of Streams

1.​Declarative: Streams provide a high-level, declarative way


to describe data processing (i.e., "what to do" rather than
"how to do it").
2.​Pipelined: Stream operations are lazy; they are chained and
executed only when a terminal operation is invoked.
3.​Non-modifying: Streams do not modify the original data
source; instead, they produce new results.
4.​Parallelizable: Streams can be processed in parallel to
improve performance on multicore processors using
parallelStream().

Stream Creation

Streams can be created from a variety of sources like


collections, arrays, and I/O channels. Here are some common ways
to create a stream:

1. From a Collection

public class StreamCreationExample {​


public static void main(String[] args) {​
List<String> names = [Link]("Alice", "Bob",
"Charlie");​

// Creating a stream from a list​
Stream<String> stream = [Link]();​

[Link]([Link]::println); // Output: Alice, Bob,
Charlie​
}​
}

2. From an Array

String[] array = {"A", "B", "C"};​


Stream<String> stream = [Link](array);
3. From Static Methods

You can use static methods like [Link](), [Link](),


or [Link]() to create streams:

Stream<Integer> stream = [Link](1, 2, 3, 4, 5);​


[Link]([Link]::println);

4. Infinite Stream

public class Main {​


public static void main(String[] args) {​
Stream<Double> stream = [Link](Math::random);​
[Link]([Link]::println);​
}​
}

4. Using Iterate With Streams


public class Main {​
public static void main(String[] args) {​
[Link](1, n -> n + 1)​
.limit(10)​
.forEach([Link]::println);​
}​
}

Types of Stream Operations

Stream operations are divided into two categories: intermediate


operations and terminal operations.

1. Intermediate Operations

Intermediate operations are lazy, meaning they are not executed


until a terminal operation is invoked. They return a new stream,
which allows chaining multiple operations together.

●​ filter(Predicate): Filters elements based on a condition.


●​ map(Function): Transforms each element in the stream.
●​ flatMap(Function): Flattens a stream of streams into a
single stream.
●​ distinct(): Removes duplicates from the stream.
●​ sorted(): Sorts the stream's elements.
●​ limit(long maxSize): Limits the number of elements in the
stream.
●​ skip(long n): Skips the first n elements of the stream.

2. Terminal Operations

Terminal operations trigger the processing of the stream. Once a


terminal operation is invoked, the stream is considered consumed
and can no longer be used.

●​ forEach(Consumer): Performs an action for each element in


the stream.
●​ collect(Collector): Converts the stream into a collection
(such as a List or Set).
●​ reduce(BinaryOperator): Combines all elements of the stream
into a single result.
●​ count(): Returns the number of elements in the stream.
●​ findFirst() / findAny(): Returns the first or any element
from the stream.
●​ toArray(): Converts the stream into an array.

Mapping Elements
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("C", 20)​
);​

[Link]().map(Movie::getTitle).forEach([Link]::println);​

[Link]().mapToInt(Movie::getLikes).forEach([Link]::println);​

Stream<List<Integer>> stream = [Link]([Link](1, 2, 3),
[Link](4, 5, 6));​
[Link](list -> [Link]()).forEach(num ->
[Link](num));​
}​
}

Filtering Elements
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("C", 20)​
);​

[Link]().filter(movie -> [Link]() >
10).forEach(movie -> [Link]([Link]()));​

Predicate<Movie> isPopular = movie -> [Link]() > 15;​
[Link]().filter(isPopular).forEach(movie ->
[Link]([Link]() + ": " + [Link]()));​
}​
}

Slicing Elements
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("C", 20),​
new Movie("D", 25),​
new Movie("E", 30)​
);​

[Link]().limit(3).forEach(movie -> [Link]([Link]()));​
[Link]().skip(2).forEach(movie -> [Link]([Link]()));​
[Link]().skip(2).limit(1).forEach(movie ->
[Link]([Link]()));​

[Link]().takeWhile(movie -> [Link]() < 25).forEach(movie ->
[Link]([Link]()));​
[Link]().dropWhile(movie -> [Link]() < 20).forEach(movie ->
[Link]([Link]()));​
}​
}

Sorting Streams
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("D", 25),​
new Movie("C", 20),​
new Movie("A", 10),​
new Movie("E", 30),​
new Movie("B", 15)​
);​

// This line will sort movies based on the compareTo method implemented in the Movie
class​
[Link]().sorted().forEach(movie -> [Link]([Link]()));​

// This line will sort movies based on the passed function​
[Link]()​
.sorted((firstMovie, secondMovie)->
[Link]().compareTo([Link]()))​
.forEach(movie -> [Link]([Link]()));​

// The same as previous, only in reverse order​
[Link]()​
.sorted([Link](Movie::getTitle).reversed())​
.forEach(movie -> [Link]([Link]()));​
}​
}

Getting Unique Elements


public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("B", 15),​
new Movie("C", 20),​
new Movie("C", 20)​
);​

[Link]().map(Movie::getLikes).distinct().forEach([Link]::println
);​
}​
}

Peeking Elements
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("C", 20),​
new Movie("D", 25),​
new Movie("E", 30)​
);​

[Link]()​
.filter(movie -> [Link]() > 15)​
.peek(movie -> [Link]("Filtered: " +
[Link]()))​
.map(Movie::getTitle)​
.peek(title -> [Link]("Mapped: " + title))​
.forEach([Link]::println);​
}​
}

Simple Reducers
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("C", 20),​
new Movie("D", 25),​
new Movie("E", 30)​
);​

[Link]([Link]().anyMatch(movie -> [Link]() > 15));​
[Link]([Link]().allMatch(movie -> [Link]() > 15));​
[Link]([Link]().noneMatch(movie -> [Link]() > 100));​

[Link]([Link]().findFirst().get().getTitle());​
[Link]([Link]().findAny().get().getTitle());​

[Link]([Link]().max([Link](Movie::getLikes)).get().getTitle());​
}​
}

Reducing a Stream
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("C", 20),​
new Movie("D", 25),​
new Movie("E", 30)​
);​

[Link]([Link]().map(Movie::getLikes).reduce((a, b) -> a + b).get());​
[Link]([Link]().map(Movie::getLikes).reduce(Integer::sum).get());​
}​
}

Collectors
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10),​
new Movie("B", 15),​
new Movie("C", 20),​
new Movie("D", 25),​
new Movie("E", 30)​
);​

List<Movie> movieList = [Link]().filter(movie -> [Link]() >
10).collect([Link]());​
Set<Movie> movieSet = [Link]().filter(movie -> [Link]() >
10).collect([Link]());​

Map<String, Movie> movieMap = [Link]().filter(movie ->
[Link]() > 10)​
.collect([Link](Movie::getTitle, movie -> movie));​
Map<String, Movie> movieMap1 = [Link]().filter(movie ->
[Link]() > 10)​
.collect([Link](Movie::getTitle, [Link]()));​

String titles = [Link]().filter(movie -> [Link]() >
15).map(Movie::getTitle).collect([Link](", "));​
}​
}

Grouping Elements
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10, [Link]),​
new Movie("B", 15, [Link]),​
new Movie("C", 20, [Link]),​
new Movie("D", 25, [Link]),​
new Movie("E", 30, [Link])​
);​

Map<Genre, Set<Movie>> genreListMap = [Link]().​
collect([Link](Movie::getGenre, [Link]()));​

Map<Genre, Long> genreCountMap = [Link]().​
collect([Link](Movie::getGenre, [Link]()));​

var result = [Link]().​


collect([Link](Movie::getGenre,​
[Link](Movie::getTitle,​
[Link]("-"))));​
}​
}
Partitioning Elements
public class Main {​
public static void main(String[] args) {​
List<Movie> movies = [Link](​
new Movie("A", 10, [Link]),​
new Movie("B", 15, [Link]),​
new Movie("C", 20, [Link]),​
new Movie("D", 25, [Link]),​
new Movie("E", 30, [Link])​
);​

Map<Boolean, List<Movie>> map =
[Link]().collect([Link](movie -> [Link]() > 15));​

Map<Boolean, String> result = [Link]()​
.collect([Link](movie -> [Link]() > 15,​
[Link](Movie::getTitle, [Link]("-"))));​
}​
}

Primitive Types Streams


Concurrency and Multi-threading

Before starting, Ask ChatGPT about this:


●​ Processor VS Core
●​ Definition of Process
●​ Parallelism VS Concurrency
●​ Threading

Process: A process is an instance of a running program. When a


program is executed, the operating system creates a process,
which is an isolated execution environment.

Concurrency at the process level: Operating systems can run


multiple processes at the same time. For example If you open a
text editor, a web browser, and a music player, each of these
will run as separate processes on your system.

Concurrency within the process: Each process has at least one


thread (The Main Thread), but it can have multiple threads that
share the same memory and resources, each of which can execute a
part of the process concurrently.

Thread: is a unit of execution within a process. It’s that thing


that actually runs the program. It represents a sequence of
instructions that can run independently, allowing a program to
perform tasks concurrently or in parallel.

To get the number of active threads in your application.


To get the total number of available threads.
public class Main {​
public static void main(String[] args) {​
[Link]([Link]());​
[Link]([Link]().availableProcessors());​
}​
}

The number of active threads will be 2. One is the main thread


and the other thread is the garbage collector thread which
removes unused objects from memory.
Starting a Thread

The Thread constructor takes a Runnable object which is an object


that implements the Runnable interface. The Runnable interface
represents a task to be run on a thread. It has one method run().

A class represents the task we want to run on a separate thread.


public class DownloadFileTask implements Runnable {​
@Override​
public void run() {​
[Link]("Downloading file task from thread: " +
[Link]().getName());​
}​
}

Then we pass the previous class to a Thread constructor and then


we start the thread.
public class Main {​
public static void main(String[] args) {​
[Link]("Current Thread: " + [Link]().getName());​

Thread thread = new Thread(new DownloadFileTask());​
[Link]();​
}​
}

This is the output we get running 2 threads, the main thread by


default and the thread-0 that we created.
Current Thread: main​
Downloading file task from thread: Thread-0

Starting 10 threads.
public class Main {​
public static void main(String[] args) {​
[Link]("Current Thread: " + [Link]().getName());​

for (int i = 0; i < 10; i++) {​
Thread thread = new Thread(new DownloadFileTask());​
[Link]();​
}​
}​
}
The output
Current Thread: main​
Downloading file task from thread: Thread-0​
Downloading file task from thread: Thread-2​
Downloading file task from thread: Thread-1​
Downloading file task from thread: Thread-3​
Downloading file task from thread: Thread-4​
Downloading file task from thread: Thread-5​
Downloading file task from thread: Thread-6​
Downloading file task from thread: Thread-7​
Downloading file task from thread: Thread-9​
Downloading file task from thread: Thread-8

Pausing a Thread

We use [Link]() to make a delay. If we run this task 10


times in a single thread, it will take 50 seconds because each
one delays for 5 seconds. But if we run each of the 10 tasks in a
separate thread, it will only take 5 seconds (the delay period).
public class DownloadFileTask implements Runnable {​
@Override​
public void run() {​
[Link]("Downloading file task from thread: " +
[Link]().getName());​

try {​
[Link](5000);​
} catch (InterruptedException e) {​
throw new RuntimeException(e);​
}​

[Link]("Downloading Completed: " +
[Link]().getName());​
}​
}
Joining Threads

If we want to download a file in a thread, then scan the file in


another thread. Here we want the scanning thread to wait for the
downloading thread to finish. We can do that using [Link]().
This method will make the current thread that runs the program
(The main thread) wait for the thread that called the join method
which is the object thread which is the downloading thread.

Here the downloading thread is the thread object.


The scanning thread is the main thread.
public class Main {​
public static void main(String[] args) {​
Thread thread = new Thread(new DownloadFileTask());​
[Link]();​

try {​
[Link]();​
} catch (InterruptedException e) {​
throw new RuntimeException(e);​
}​

[Link]("File is ready to be scanned");​
}​
}

In the output the scanning thread (the main thread) waited for
the downloading thread to finish.
Downloading file task from thread: Thread-0​
Downloading Completed: Thread-0​
File is ready to be scanned
Interrupting a Thread

The [Link]() alone doesn’t force the thread to stop


what it is doing, it simply sends an interrupt request.
To support interruption, we should constantly check for the
interruption request signal to interrupt the thread.

Here in the running thread we check for the interruption signal.


public class DownloadFileTask implements Runnable {​
@Override​
public void run() {​
[Link]("Downloading file task from thread: " +
[Link]().getName());​

for (int i = 0; i < Integer.MAX_VALUE; i++) {​
if ([Link]().isInterrupted())​
return;​
[Link]("Downloading Byte: " + i);​
}​

[Link]("Downloading Completed: " +
[Link]().getName());​
}​
}

In the main, we send the interruption request signal.


public class Main {​
public static void main(String[] args) {​
Thread thread = new Thread(new DownloadFileTask());​
[Link]();​

try {​
[Link](1000);​
} catch (InterruptedException e) {​
throw new RuntimeException(e);​
}​

[Link]();​
}​
}
Concurrency Issues
Sometimes our threads may need to access a shared resource. For
example in the DownloadFile task, we might want each thread to
report the total number of bytes of the file it has downloaded to
a shared object (totalBytes). Now if multiple threads access the
same object and one of them changes this object, we’re gonna run
into a couple of issues.

Race Condition
The first issue which happens when multiple threads try to modify
the same data at the same time. It may result in wrong data or
the program may crash.

Visibility Problem
The first issue which happens when a thread changes the shared
data but the changes are not visible (updated) to the other
threads. So different threads will have different views of the
shared data.

Thread-Safe Code
Thread safety in Java ensures that a piece of code works
correctly when executed by multiple threads at the same time.
Writing thread-safe code prevents issues like race conditions,
deadlocks, and data inconsistency, which can arise when multiple
threads access shared resources concurrently.

Now let’s say we have a class `DownloadStatus` which counts the


number of total bytes of all downloaded files of all threads.
public class DownloadStatus {​
private int totalBytes;​

public int getTotalBytes() {​
return totalBytes;​
}​

public void incrementTotalBytes() {​
totalBytes++;​
}​
}
And we have the downloading file task, each file is 10_000 bytes.
So at the end the total bytes of all threads should be 100_000.
public class DownloadFileTask implements Runnable {​
private DownloadStatus status;​

public DownloadFileTask(DownloadStatus status) {​
[Link] = status;​
}​

@Override​
public void run() {​
[Link]("Downloading file task from thread: " +
[Link]().getName());​

for (int i = 0; i < 10_000; i++) {​
if ([Link]().isInterrupted()) return;​
[Link]();​

}​

[Link]("Downloading Completed: " +
[Link]().getName());​
}​
}

Main Demo: we run all the threads and wait for all of them to
finish and then print the number of total bytes downloaded.
public class Main {​
public static void main(String[] args) {​
var status = new DownloadStatus();​
List<Thread> threads = new ArrayList<>();​

for (int i = 0; i < 10; i++) {​
Thread thread = new Thread(new DownloadFileTask(status));​
[Link]();​
[Link](thread);​
}​

for (Thread thread: threads) {​
try {​
[Link]();​
} catch (InterruptedException e) {​
[Link]();​
}​
}​

[Link]([Link]());​
}​
}
When we run this program multiple times, each time we will get a
different number of the total bytes and it’s not 100_000.
This happened because of the race condition and visibility issue
because each thread will increment the totalBytes and this
operation takes a bit of time. So while a thread is running the
increment operation and didn’t yet update the shared totalBytes
with the new value, another thread will take the old value and
also increment it. So both threads will increment the same old
value and increment updates will be lost.

Strategies For Thread Safety

1.​Confinement
It’s a strategy when we don’t share data across threads in the
first place. We restrict each thread to have its own data.

For example, instead of sharing DownloadStatus object across


multiple DownloadFileTask threads, we could have each
DownloadFileTask thread have its own DownloadStatus object. When
all the threads finish, we can combine the results.

2.​Immutability
We share immutable objects across threads, because each time
instead of modifying the object, they create a new one.

3.​Synchronization
We prevent multiple threads from accessing the shared object at
the same time concurrently. We do that by using locks.
We put locks on certain parts of our code, and only one thread at
a time can execute that part. But it can cause deadlocks. Also in
reality, our code will be executed sequentially.

4.​Atomic Objects
If you increment an AtomicInteger object, the JVM will execute
the increment operation as one single atomic operation, instead
of breaking it into 3 smaller operations.

5.​Partitioning
Partitioning data into segments that can be accessed
concurrently.
1.​Confinement
Each DownloadFileTask object will have its own DownloadStatus.
public class DownloadFileTask implements Runnable {​
private DownloadStatus status;​

public DownloadFileTask() {​
[Link] = new DownloadStatus();​
}​

public DownloadStatus getStatus() {​
return status;​
}​

@Override​
public void run() {​
[Link]("Downloading file task from thread: " +
[Link]().getName());​
for (int i = 0; i < 10_000; i++) {​
if ([Link]().isInterrupted()) return;​
[Link]();​

}​
[Link]("Downloading Completed: " +
[Link]().getName());​
}​
}

Main Demo: after all tasks finish, we will combine the totalBytes
of each DownloadStatus object of each DownloadFileTask.
public class Main {​
public static void main(String[] args) {​
List<Thread> threads = new ArrayList<>();​
List<DownloadFileTask> tasks = new ArrayList<>();​

for (int i = 0; i < 10; i++) {​
DownloadFileTask task = new DownloadFileTask();​
[Link](task);​

Thread thread = new Thread(task);​
[Link]();​
[Link](thread);​
}​

for (Thread thread: threads) {​
try {​
[Link]();​
} catch (InterruptedException e) {​
[Link]();​
}​
}​

int totalBytes = [Link]().map(task ->
[Link]().getTotalBytes()).reduce(0, Integer::sum);​
[Link](totalBytes);​
}​
}

2.​Synchronization
We are gonna prevent multiple threads from accessing the
DownloadStatus object at the same time by locking the increment
operation code in the DownloadStatus class using Lock.
public class DownloadStatus {​
private int totalBytes;​
private Lock lock = new ReentrantLock();​

public int getTotalBytes() {​
return totalBytes;​
}​

public void incrementTotalBytes() {​
[Link]();​
totalBytes++;​
[Link]();​
}​
}

The DownloadFileTask class will have a shared status object that


is shared across all threads.
public class DownloadFileTask implements Runnable {​
private DownloadStatus status;​

public DownloadFileTask(DownloadStatus status) {​
[Link] = status;​
}​

public DownloadStatus getStatus() {​
return status;​
}​

@Override​
public void run() {​
[Link]("Downloading file task from thread: " +
[Link]().getName());​
for (int i = 0; i < 10_000; i++) {​
if ([Link]().isInterrupted()) return;​
[Link]();​

}​
[Link]("Downloading Completed: " +
[Link]().getName());​
}​
}

Main Demo: single status object shared across all threads


public class Main {​
public static void main(String[] args) {​
DownloadStatus status = new DownloadStatus();​
List<Thread> threads = new ArrayList<>();​

for (int i = 0; i < 10; i++) {​
DownloadFileTask task = new DownloadFileTask(status);​
Thread thread = new Thread(task);​
[Link]();​
[Link](thread);​
}​

for (Thread thread: threads) {​
try {​
[Link]();​
} catch (InterruptedException e) {​
[Link]();​
}​
}​

[Link]([Link]());​
}​
}
The `synchronized` Keyword

Another way to implement synchronization is the synchronized


keyword. We pass between the parens the object we want to lock
and between the curly braces the operation that we want to lock
while it’s being executed by a thread.

For example if we passed this the synchronized keyword, the whole


object will be locked while the operation is being executed by a
thread. And this is not best practice because you’re locking the
whole object.
We should lock each method independently so that if a method is
being executed by a thread, another thread can access the other
method.

public class DownloadStatus {​


private int totalBytes;​
private int totalFiles;​
private Object totalBytesLock = new Object();​
private Object totalFilesLock = new Object();​

public int getTotalBytes() {​
return totalBytes;​
}​

public int getTotalFiles() {​
return totalFiles;​
}​

public void incrementTotalBytes() {​
synchronized (totalBytesLock) {​
totalBytes++;​
}​
}​

public void incrementTotalFiles() {​
synchronized (totalFilesLock) {​
totalFiles++;​
}​
}​
}
The `volatile` Keyword

It solves the visibility problem but not the race condition. So


it doesn’t prevent 2 threads from modifying the shared data at
the same time but it ensures that if one thread changes some
data, other threads can see the changes.
We declare the isDone variable as volatile so that if a thread
updates its value, other threads will be notified.
public class DownloadStatus {​
private int totalBytes;​
private int totalFiles;​
private Object totalBytesLock = new Object();​
private Object totalFilesLock = new Object();​
private volatile boolean isDone;​

public int getTotalBytes() {​
return totalBytes;​
}​

public int getTotalFiles() {​
return totalFiles;​
}​

public void incrementTotalBytes() {​
synchronized (totalBytesLock) {​
totalBytes++;​
}​
}​

public void incrementTotalFiles() {​
synchronized (totalFilesLock) {​
totalFiles++;​
}​
}​

public boolean isDone() {​
return isDone;​
}​

public void done() {​
isDone = true;​
}​
}
Main Demo
The first thread is the DownloadFileTask.
The second thread is a lambda expression waiting for the status
of the first thread to be done to then print the totalBytes.
public class Main {​
public static void main(String[] args) {​
DownloadStatus status = new DownloadStatus();​

Thread thread1 = new Thread(new DownloadFileTask(status));​
Thread thread2 = new Thread(() -> {​
while (![Link]()) {}​
[Link]([Link]());​
});​

[Link]();​
[Link]();​
}​
}

Thread signaling with `wait()` and `notify()`


`wait()` method will make the thread go to sleep until another
thread notifies this thread (which went to sleep) that the state
of the status variable has changed. So the while loop won’t run
forever.
public class Main {​
public static void main(String[] args) {​
DownloadStatus status = new DownloadStatus();​

Thread thread1 = new Thread(new DownloadFileTask(status));​
Thread thread2 = new Thread(() -> {​
while (![Link]()) {​
synchronized (status) {​
try {​
[Link]();​
} catch (InterruptedException e) {​
throw new RuntimeException(e);​
}​
}​
}​
[Link]([Link]());​
});​

[Link]();​
[Link]();​
}​
}
Here we need to notify the sleeping thread that the state of the
status object is changed.
public class DownloadFileTask implements Runnable {​
private DownloadStatus status;​

public DownloadFileTask(DownloadStatus status) {​
[Link] = status;​
}​

public DownloadStatus getStatus() {​
return status;​
}​

@Override​
public void run() {​
[Link]("Downloading file task from thread: " +
[Link]().getName());​
for (int i = 0; i < 1_000_000; i++) {​
if ([Link]().isInterrupted()) return;​
[Link]();​
}​

[Link]();​
synchronized (status) {​
[Link]();​
}​

[Link]("Downloading Completed: " +
[Link]().getName());​
}​
}

Atomic Objects
Here, we will define the totalBytes variable as AtomicInteger so
that when you increment the totalBytes, the JVM will execute the
increment operation as one single atomic operation, instead of
breaking it into 3 smaller operations.
public class DownloadStatus {​
private AtomicInteger totalBytes = new AtomicInteger();​
private int totalFiles;​
private Object totalBytesLock = new Object();​
private Object totalFilesLock = new Object();​
private volatile boolean isDone;​

public int getTotalBytes() {​
return [Link]();​
}​

public int getTotalFiles() {​
return totalFiles;​
}​

public void incrementTotalBytes() {​
[Link]();​
}​

public void incrementTotalFiles() {​
totalFiles++;​
}​
public boolean isDone() {​
return isDone;​
}​
public void done() {​
isDone = true;​
}​
}

Main Demo
public class Main {​
public static void main(String[] args) {​
DownloadStatus status = new DownloadStatus();​
List<Thread> threads = new ArrayList<>();​

for (int i = 0; i < 10; i++) {​
DownloadFileTask task = new DownloadFileTask(status);​
Thread thread = new Thread(task);​
[Link]();​
[Link](thread);​
}​

for (Thread thread: threads) {​
try {​
[Link]();​
} catch (InterruptedException e) {​
[Link]();​
}​
}​

[Link]([Link]());​
}​
}
Adder
A similar way to Atomic Objects is the Adder classes which also
solves the problems of race condition and visibility but it’s
faster than atomic objects.
public class DownloadStatus {​
private LongAdder totalBytes = new LongAdder();​
private int totalFiles;​
private Object totalBytesLock = new Object();​
private Object totalFilesLock = new Object();​
private volatile boolean isDone;​

public int getTotalBytes() {​
return [Link]();​
}​

public int getTotalFiles() {​
return totalFiles;​
}​

public void incrementTotalBytes() {​
[Link]();​
}​

public void incrementTotalFiles() {​
totalFiles++;​
}​

public boolean isDone() {​
return isDone;​
}​

public void done() {​
isDone = true;​
}​
}
Synchronized Collections
If 2 threads try to modify a regular collection at the same time,
some data will be lost. To avoid that we should use a
synchronized collections which are in the `Collections.` class.

Synchronized collections handle race conditions by locking the


collection until the thread modifies the collection, then opening
the collection for any other thread to modify it.
public class Main {​
public static void main(String[] args) {​
Collection<Integer> collection =
[Link](new ArrayList<>());​

Thread thread1 = new Thread(() -> [Link]([Link](1, 2, 3)));​
Thread thread2 = new Thread(() -> [Link]([Link](4, 5, 6)));​

[Link]();​
[Link]();​

try {​
[Link]();​
[Link]();​
} catch (InterruptedException e) {​
[Link]();​
}​

[Link](collection);​
}​
}

Concurrent Collections are better and faster than synchronized


collections because they don’t lock the collections. Instead they
partition the data into segments so that different threads can
work concurrently with different segments but only one thread at
a time can access a given segment.
public class Main {​
public static void main(String[] args) {​
Map<Integer, String> map = new ConcurrentHashMap<>();​
[Link](1, "A");​
[Link](1);​
[Link](1);​
}​
}

You might also like