Stream API
End-to-End with Intermediate Operations
IP
ND
SA
Notes By Sandip Vargale
Intermediate Operation
🔹 An intermediate operation is a method in the Java Stream API that transforms a
stream into another stream. It does not trigger processing of the data
immediately—instead, it builds up a pipeline of operations that will run later
when a terminal operation (like collect(), forEach(), etc.) is invoked..
🔹 Key Characteristics:
● Lazy Evaluation: Intermediate operations don’t do anything until a terminal
operation is called.
● Chainable: You can link multiple intermediate operations together (fluent
style).
● Return a Stream: They always return a new stream, allowing further operations.
Operations
🔹 1.filter()
✦ Purpose: Filters elements based on a predicate or given condition.
IP
✦ Syntax: Stream<T> filter(Predicate<? super T> predicate)
🔸 Example_1:
ND
List<String> names = Arrays.asList("John", "Jane", "Tom", "Jake");
names.stream()
.filter(name -> name.startsWith("J"))
.forEach(System.out::println);
🔸 Example_2:
SA
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(10, 15, 20, 25, 30);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // Output: [10, 20, 30]
}
}
🔸 Example_3:
List<Integer> result = Stream.of(1, 3, 6, 8, 2, 5)
.filter(n -> n > 5)
.toList();
System.out.println(result); // Output: [6, 8]
Notes By Sandip Vargale
🧠 Real-time Use:
● Filtering out users whose names start with "J.
● Filtering out employees earning above a certain salary.
🔹 2.map()
✦ Purpose:
● The map() method is an intermediate operation in the Java Stream API.It is
used to transform (or map) each element of the stream from type T to
another type R.
● It returns a new stream consisting of the results of applying the given
Function to the elements of the original stream.
✦ Syntax: <R> Stream<R> map(Function<? super T, ? extends R> mapper)
► Breakdown of the Signature:
● <R> – This denotes a generic type of the result. It means that after
IP
mapping, each element in the new stream will be of type R.
● Function<? super T, ? extends R> – This is the functional interface used
to perform the transformation:
● T is the input type (type of elements in the original stream).
● R is the output type (type of elements in the resulting stream).
ND
● ? super T allows the function to accept T or any of its supertypes.
● ? extends R allows the result to be a subtype of R.
🔸 Example_1: Convert Strings to Uppercase
List<String> names = Arrays.asList("alice", "bob", "charlie");
SA
List<String> upperNames = names.stream()
.map(String::toUpperCase) // Function<String, String>
.toList();
System.out.println(upperNames); // [ALICE, BOB, CHARLIE]
► Note:
● String::toUpperCase is a function that takes a String and returns a
String.
● Here, T = String, R = String.
🔸 Example_2: Convert List of Strings to List of Lengths (Integer)
List<String> names = Arrays.asList("apple", "banana", "kiwi");
List<Integer> nameLengths = names.stream()
.map(String::length) // Function<String, Integer>
.toList();
Notes By Sandip Vargale
System.out.println(nameLengths); // [5, 6, 4]
► Note:
● This converts each String to its length.
● Here, T = String, R = Integer.
🔸 Example_3: Convert List of Objects to List of Fields
public class Employee {
String name;
int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
}
IP
}
List<Employee> employees = Arrays.asList(
new Employee("Alice", 30),
ND
new Employee("Bob", 25)
);
List<String> employeeNames =
employees.stream()
.map(Employee::getName) // Function<Employee, String>
.collect(Collectors.toList());
SA
System.out.println(employeeNames); // [Alice, Bob]
🧠 Real-World Use Cases
● Transforming data before saving it to the database.
● Extracting specific fields from complex objects.
● Converting one data structure into another (e.g., DTO mapping).
🔹 3.flatMap()
✦ Purpose: Flattens nested structures/stream into a single stream.
flatMap() is a method in Java's Stream API that:
● Transforms each element in the stream into another stream (or collection).
● Then flattens all those nested streams into a single stream.
● In simple terms: map + flatten = flatMap
Notes By Sandip Vargale
✦ Syntax:
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> m);
● Applies this function to each element of the original stream (type T),
● Gets multiple streams of R,
● Flattens those into one single stream of R.
🔸 Explanation
● For each element T in the original stream, apply a function that returns a
stream of R, then flatten all those streams into one stream of R."
🧠 Real-time Example:
● Extracting all subjects from multiple student records.
● Reading nested lists from JSON files and flattening them.
● Merging multiple lists of user roles into a single list.
🎯 Real-World Analogy (SQL vs flatMap)
IP
● Imagine a database with two tables: like having RelationShip (OneToMany)
One Departments can have multiple Employees ()or Many Employees belongs to
ND
One Department
i.e
🗃️ Departments Tables 🗃️Employees Tables
id name id name dept_id
1 IT 1 Ram 1
SA
2 HR 2 Shyam 1
3 Seeta 2
🎯 SQL Query (Join with Flattened Result):
● SELECT d.name AS department, e.name AS employee
FROM Departments d
JOIN Employees e ON d.id = e.dept_id;
● This query gives:
department employee
IT Ram
IT Ram
HR Seeta
Notes By Sandip Vargale
► Note:
You joined departments with their employees and flattened all employee
lists into one single result set.
🔸 Java Equivalent Using flatMap
class Department {
String name;
List<Employee> employees;
// constructor, getter
}
class Employee {
String name;
// constructor, getter
}
IP
🔸 Stream + flatMap Example:
List<Department> departments =
ND
new ArrayList<>(Arrays.asList(
new Department("IT",
new ArrayList<>(Arrays.asList(new Employee("Ram"),
new Employee("Shyam")))),
new Department("HR", new ArrayList<>(Arrays.asList(
new Employee("Seeta"))))
));
SA
departments.stream()
.flatMap(dept -> dept.getEmployees().stream())//flatten employee list
.map(Employee::getName) // get names only
.forEach(System.out::println);
🧾 Output:
Ram
Shyam
Seeta
🎨 Visual:
● Before flatMap: [ [Ram, Shyam],[Seeta] ]
● After flatMap: => [Ram, Shyam, Seeta]
● Here, each department has its own list of employees (just like a subquery or
subtable).
● flatMap collects all these sublists into a single flattened stream — similar to
how SQL JOIN combines rows.
Notes By Sandip Vargale
🧠 In Summary:
Concept SQL Java (Stream API)
Table Table / Row Object / Collection
Nested Data Subquery / JOIN List inside List (e.g.,
List<List<T>>)
Flattening Data JOIN or Subquery Result flatMap()
Transformation SELECT column map()
Combined SELECT + JOIN flatMap().map()
► When to Use flatMap?
You have a stream of collections (e.g., List<List<String>>).
You want to combine them into a single stream before processing.
🔸 Example 1:
IP
List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("apple", "banana"),
ND
Arrays.asList("cherry", "date")
);
List<String> allItems = listOfLists.stream()
.flatMap(List::stream)
.toList();
System.out.println(allItems); // Output: [apple, banana, cherry, date]
🔸 Example 2:
SA
List<List<String>> data = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d")
);
data.stream()
.flatMap(Collection::stream)
.forEach(System.out::println);
🔸 Example 3:
List<String> sentences = Arrays.asList("hello world", "java streams");
List<String> words = sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.collect(Collectors.toList());
System.out.println(words); [hello, world, java, streams]
Notes By Sandip Vargale
🔹 4.distinct()
✦ Purpose: Removes duplicate elements ,distinct() works based on equals() and
hashCode() methods of your objects
✦ Syntax: Stream<T> distinct()
🔸 Example 1: Remove Duplicate Students
public class Student {
private int id;
private String name;
// Constructor, Getters, Setters
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IP
Student student = (Student) o;
return id == student.id && Objects.equals(name, student.name);
}
@Override
ND
public int hashCode() {
return Objects.hash(id, name);
}
}
List<Student> students = Arrays.asList(
new Student(1, "Alice"),
new Student(2, "Bob"),
SA
new Student(1, "Alice") // duplicate
);
List<Student> uniqueStudents = students.stream()
.distinct()
.toList();
uniqueStudents.forEach(s -> System.out.println(s.getName()));
🔸 Example 2 : 💡 Alternative (Custom Uniqueness)
If you want to define "duplicate" based only on one field (like id), use a
custom collector or filter like:
List<Student> uniqueById =
students.stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(Student::getId, s -> s, (s1, s2) -> s1),
m -> new ArrayList<>(m.values())
));
Notes By Sandip Vargale
This removes duplicates based only on the id.
🔸 Example 3 :
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 2, 3, 4, 4, 5);
List<Integer> uniqueNumbers =
numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNumbers); // Output: [1, 2, 3, 4, 5]
}
}
🧠 Real-World Use Case: Removing duplicate email addresses from a list.
🔹 5.sorted()
IP
✦ Purpose: Sorts the elements in natural order or using a comparator.
✦ Syntax:
ND
● Stream<T> sorted(); // Default Natural Sorting
● Stream<T> sorted(Comparator<? super T> comparator); // Customise Sorting
► Note : This sorts elements in natural order, which means:
● For numbers: ascending order (e.g., 1, 2, 3)
● For strings: lexicographic order (e.g., "apple", "banana", "zebra")
● For custom objects: the class must implement Comparable<T>
SA
🔸 Example 1 with integers:
List<Integer> numbers = Arrays.asList(5, 1, 3);
List<Integer> sorted = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sorted); // Output: [1, 3, 5]
🔸 Example 2 : Stream<T> sorted(Comparator<? super T> comparator)
This allows you to define a custom sort order using a Comparator.
List<Student> students = Arrays.asList(
new Student(1, "Bob"),
new Student(2, "Alice"),
new Student(3, "Charlie")
);
Notes By Sandip Vargale
List<Student> sortedByName = students.stream()
.sorted(Comparator.comparing(Student::getName))
.collect(Collectors.toList());
sortedByName.forEach(s -> System.out.println(s.getName()));
🔸 Example 3: Custom Sorting of Students
public class Student {
private int id;
private String name;
private double gpa;
// Constructor, Getters, Setters
}
🔸 Example 4:
Code with Collectors and custom sort:
IP
Sort students by GPA (highest first), then by name (A–Z)
ND
List<Student> students = Arrays.asList(
new Student(1, "Alice", 3.5),
new Student(2, "Bob", 3.9),
new Student(3, "Charlie", 3.9),
new Student(4, "David", 3.2)
);
SA
List<Student> sorted = students.stream()
.sorted(
Comparator.comparingDouble(Student::getGpa).reversed()
.thenComparing(Student::getName)
)
.toList();
sorted.forEach(s -> System.out.println(s.getName() + " - " + s.getGpa()));
🧾 Output:
Bob - 3.9
Charlie - 3.9
Alice - 3.5
David - 3.2
🧠 Explanation:
● Comparator.comparingDouble(Student::getGpa).reversed() → sort by GPA descending
● thenComparing(Student::getName) → if GPAs are equal, sort by name ascending
● collect(Collectors.toList()) → collect the sorted stream into a list
Notes By Sandip Vargale
🔸 Example 5: You Can Also Sort By:
.id → Comparator.comparing(Student::getId)
.name length → Comparator.comparingInt(s -> s.getName().length())
🔸 Example 6:
List<String> names = Arrays.asList("John", "Alice", "Bob");
List<String> sortedNames = names.stream()
.sorted()
..toList();
System.out.println(sortedNames); // Output: [Alice, Bob, John]
🔸 Example 7:
Stream.of("banana", "apple", "cherry")
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
🔹 6.peek()
without modifying the stream.
IP
✦ Purpose: Performs an action on each element as it is consumed from the stream ,
✦ Syntax: Stream<T> peek(Consumer<? super T> action)
ND
🔸 Example_1 :
public class StreamExample {
public static void main(String[] args) {
Stream.of("apple", "banana", "cherry")
SA
.peek(System.out::println)
.collect(Collectors.toList());
}
}
🔸 Example_2 :
Stream.of("apple", "banana", "cherry")
.peek(System.out::println)
.map(String::toUpperCase)
.forEach(System.out::println);
🧠 Real-World Use Case: Logging each transaction in a stream for debugging
purposes.
Notes By Sandip Vargale
🔹 7.limit(long maxSize)
✦ Purpose: Limits the number of elements in the stream.
✦ Syntax: Stream<T> limit(long maxSize)
🔸 Example 1:
Stream.of(1, 2, 3, 4, 5)
.limit(3)
.forEach(System.out::println);
🧠 Real-time Use: Fetching only top 3 search results.
Displaying only the top 5 trending products.
🔹 8.skip(long n)
✦ Purpose: Skips the first N elements of the stream.
✦ Syntax: Stream<T> skip(long n);
🔸 Example 1 :
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
IP
List<Integer> skipped = numbers.stream()
.skip(3)
.collect(Collectors.toList());
System.out.println(skipped); // Output: [4, 5, 6]
ND
🧠 Real-World Use Case:
● Paginating through a list of records by skipping the first n items.
e.g. Implementing pagination (e.g., page 2 starts after skipping 10 elements).
🔹 9.mapToInt()
✦ Purpose: Converts elements to an IntStream. Specialized mapping for primitive
SA
types.
✦ Syntax: IntStream mapToInt(ToIntFunction<? super T> mapper);
🔸 Example :
List<String> numbers = Arrays.asList("1", "2", "3");
int sum = numbers.stream()
.mapToInt(Integer::parseInt)
.sum();
System.out.println(sum); // Output: 6
🧠 Real-World Use Case: Calculating the total of a list of transaction amounts.
🔹 10.mapToLong()
✦ Purpose : Converts elements to a LongStream.
✦ Syntax : LongStream mapToLong(ToLongFunction<? super T> mapper);
Notes By Sandip Vargale
🔸 Example:
List<String> numbers = Arrays.asList("10000000000", "20000000000");
long sum = numbers.stream()
.mapToLong(Long::parseLong)
.sum();
System.out.println(sum); // Output: 30000000000
🧠 Real-World Use Case: Summing large transaction amounts in a financial
application.
🔹 11.mapToDouble()
✦ Purpose : Converts elements to a DoubleStream.
✦ Syntax : DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
🔸 Example:
IP
List<String> numbers = Arrays.asList("1.1", "2.2", "3.3");
double average = numbers.stream()
.mapToDouble(Double::parseDouble)
.sum();
🔹
ND
What are IntStream, LongStream, and DoubleStream?
● They are specialized stream types in Java’s Stream API designed to work
efficiently with primitive data types: int, long, and double, respectively.
🔸 Why specialized streams?
● Regular streams like Stream<Integer> involve boxing and unboxing (wrapping
SA
primitives in objects), which adds overhead.
● Primitive streams avoid this by working directly with primitives, improving
performance and memory usage.
🔸 Example:
IntStream intStream = IntStream.of(1, 2, 3, 4);
int sum = intStream.sum(); // 10
LongStream longStream = LongStream.range(1, 5);
long max = longStream.max().orElse(0); // 4
DoubleStream doubleStream = DoubleStream.of(1.5, 2.5, 3.5);
double avg = doubleStream.average().orElse(0.0); // 2.5
Notes By Sandip Vargale
IP
ND
SA
💡 Food for Thought!
If mapToInt() gives you an IntStream,
👉 How would you convert it back into a Stream<Integer>?
💡 Hint: There's an intermediate method that does exactly this.
THANK YOU
Notes By Sandip Vargale