
The Streams API is one of the most powerful features introduced in Java 8. It allows you to process collections (Lists, Sets, Maps, etc.) in a functional and declarative way.
1. What is a Stream?
A Stream is a sequence of data elements that supports functional operations such as filtering, mapping, and reducing, without modifying the original data.
✅ Key Characteristics of Streams:
- Declarative: You specify what to do, not how to do it.
- Lazy Evaluation: Operations are executed only when needed.
- Parallel Processing: Supports multithreading for better performance.
- Immutable: Streams do not modify the original collection.
2. Creating a Stream
Example: Creating a Stream from a List
import java.util.List;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
// Convert List to Stream
Stream<String> nameStream = names.stream();
// Process and print each element
nameStream.forEach(System.out::println);
}
}
✅ The stream()
method converts a List<String>
into a Stream<String>
, allowing us to process elements efficiently.
3. Stream Operations
Streams support two types of operations:
- Intermediate Operations (Transform data – return a new stream)
- Terminal Operations (End the stream processing – return a result)
4. Common Stream Operations
Operation | Type | Description | Example |
---|---|---|---|
filter(Predicate<T>) | Intermediate | Removes elements that don’t match a condition | list.stream().filter(n -> n > 10) |
map(Function<T, R>) | Intermediate | Transforms each element | list.stream().map(n -> n * 2) |
sorted(Comparator<T>) | Intermediate | Sorts elements | list.stream().sorted() |
distinct() | Intermediate | Removes duplicates | list.stream().distinct() |
forEach(Consumer<T>) | Terminal | Performs an action on each element | list.stream().forEach(System.out::println) |
collect(Collectors.toList()) | Terminal | Converts stream back to a collection | list.stream().collect(Collectors.toList()) |
reduce(BinaryOperator<T>) | Terminal | Reduces elements to a single value | list.stream().reduce(0, Integer::sum) |
5. Example: Filtering a List
Let’s say we want to filter names that start with “A”.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Amanda", "Charlie");
// Filter names starting with 'A'
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(filteredNames); // Output: [Alice, Amanda]
}
}
✅ The filter()
method removes names that don’t start with “A”.
6. Example: Transforming Data (map()
)
Let’s convert a list of names to uppercase.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
// Convert names to uppercase
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseNames); // Output: [ALICE, BOB, CHARLIE]
}
}
✅ The map()
method transforms each element in the stream.
7. Example: Sorting Data (sorted()
)
Let’s sort numbers in ascending order.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(5, 2, 8, 1, 9);
// Sort numbers
numbers.stream()
.sorted()
.forEach(System.out::println);
}
}
✅ The sorted()
method sorts elements naturally (ascending order for numbers).
8. Example: Removing Duplicates (distinct()
)
Let’s remove duplicate numbers from a list.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(5, 2, 8, 2, 9, 5);
// Remove duplicates
List<Integer> uniqueNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNumbers); // Output: [5, 2, 8, 9]
}
}
✅ The distinct()
method removes duplicate values.
9. Example: Summing Numbers (reduce()
)
Let’s sum all numbers in a list.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Sum numbers
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum); // Output: Sum: 15
}
}
✅ The reduce()
method combines elements into a single value.
10. Example: Collecting Results (collect()
)
Let’s collect processed data into a List.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
// Convert names to lowercase and collect
List<String> lowerCaseNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toList());
System.out.println(lowerCaseNames); // Output: [alice, bob, charlie]
}
}
✅ The collect(Collectors.toList())
method converts a Stream back into a List.
Lesson Reflection
- Why do you think Streams are useful compared to traditional loops?
- How does lazy evaluation improve performance in large data processing?
- Can you think of a real-world scenario where you would use
map()
,filter()
, orreduce()
?
Here are my answers to the reflection questions:
1. Why do you think Streams are useful compared to traditional loops?
Streams provide several advantages over traditional for
or while
loops:
✅ More Readable & Concise – Traditional loops require explicit iteration, whereas streams allow declarative programming.
Traditional Loop (Verbose)
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> filtered = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
filtered.add(name);
}
}
System.out.println(filtered);
Stream Approach (Concise)
List<String> filtered = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(filtered);
✅ Easier Parallel Processing – Streams can process large data in parallel using .parallelStream()
without manual thread handling.
✅ Less Boilerplate Code – No need for temporary lists, explicit counters, or iterators.
✅ More Functional & Declarative – Focus on what to do, not how to do it.
2. How does lazy evaluation improve performance in large data processing?
Lazy evaluation means operations on a stream are only executed when a terminal operation is invoked.
✅ Efficiency – Unnecessary computations are skipped.
✅ Short-Circuiting – The stream stops processing as soon as a result is found.
✅ Better Memory Usage – Streams don’t store intermediate results, making them memory-efficient.
Example: Lazy Evaluation vs. Immediate Execution
Imagine a large dataset, and we want the first even number greater than 10:
🔴 Without Lazy Evaluation (Traditional Approach – Processes All Elements)
List<Integer> numbers = List.of(5, 12, 3, 8, 19, 14);
for (int num : numbers) {
if (num > 10 && num % 2 == 0) {
System.out.println(num);
break; // Manually stopping
}
}
🔵 With Lazy Evaluation (Stream Approach – Stops at First Match)
List<Integer> numbers = List.of(5, 12, 3, 8, 19, 14);
numbers.stream()
.filter(n -> n > 10) // ✅ Filters first
.filter(n -> n % 2 == 0) // ✅ Then finds the first even
.findFirst() // 🚀 Stops after the first match
.ifPresent(System.out::println);
✅ The stream stops processing as soon as it finds 12
, improving performance.
3. Can you think of a real-world scenario where you would use map()
, filter()
, or reduce()
?
✅ map()
(Data Transformation) – Converting Currency in an E-Commerce App
Imagine we have a list of prices in USD and we want to convert them to EUR:
List<Double> pricesUSD = List.of(100.0, 50.0, 75.5);
double exchangeRate = 0.85;
List<Double> pricesEUR = pricesUSD.stream()
.map(price -> price * exchangeRate)
.collect(Collectors.toList());
System.out.println(pricesEUR); // Output: [85.0, 42.5, 64.175]
✅ map()
is used to convert each price from USD to EUR.
✅ filter()
(Data Selection) – Filtering Active Users from a Database
Assume we have a list of users, and we want to filter only active users:
class User {
String name;
boolean isActive;
User(String name, boolean isActive) {
this.name = name;
this.isActive = isActive;
}
}
List<User> users = List.of(new User("Alice", true), new User("Bob", false), new User("Charlie", true));
List<User> activeUsers = users.stream()
.filter(user -> user.isActive)
.collect(Collectors.toList());
✅ filter()
removes inactive users, returning only active ones.
✅ reduce()
(Aggregation) – Summing Up Employee Salaries
Let’s say we need to calculate the total salary of employees:
List<Integer> salaries = List.of(5000, 7000, 6000);
int totalSalary = salaries.stream()
.reduce(0, Integer::sum);
System.out.println("Total Salary: $" + totalSalary); // Output: $18000
✅ reduce()
aggregates the salaries into a single value.
Parallel Streams (parallelStream()
) – Java 8 Examples
By default, streams process data sequentially, but with parallelStream()
, Java can divide the workload across multiple threads to improve performance, especially for large datasets.
1. When to Use parallelStream()
?
✅ Useful for large datasets where processing time is high.
✅ Ideal for CPU-intensive operations like complex calculations or sorting.
✅ Works best when tasks are independent (no shared state).
❌ Not recommended for small lists – thread creation overhead may slow it down.
2. Basic Example – Sequential vs. Parallel Stream
Let’s compare sequential and parallel processing for summing a large list of numbers.
🔴 Sequential Stream (One Thread)
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
long start = System.nanoTime();
int sum = numbers.stream()
.reduce(0, Integer::sum);
long end = System.nanoTime();
System.out.println("Sequential Sum: " + sum);
System.out.println("Time taken: " + (end - start) + " ns");
}
}
✅ The stream runs sequentially, processing elements one by one.
🟢 Parallel Stream (Multiple Threads)
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
long start = System.nanoTime();
int sum = numbers.parallelStream()
.reduce(0, Integer::sum);
long end = System.nanoTime();
System.out.println("Parallel Sum: " + sum);
System.out.println("Time taken: " + (end - start) + " ns");
}
}
✅ The parallel stream runs on multiple threads, dividing work among CPU cores.
3. Checking Thread Execution
Since parallelStream() uses multiple threads, let’s print the thread names to see it in action.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers.parallelStream()
.forEach(num -> System.out.println(Thread.currentThread().getName() + " processing: " + num));
}
}
Example Output (Threads May Vary)
ForkJoinPool.commonPool-worker-1 processing: 3
ForkJoinPool.commonPool-worker-3 processing: 8
ForkJoinPool.commonPool-worker-2 processing: 5
main processing: 1
main processing: 2
ForkJoinPool.commonPool-worker-1 processing: 6
...
✅ The work is divided among multiple threads, improving efficiency.
4. When Should You Avoid parallelStream()
?
❌ For Small Collections – Parallelism overhead can slow down execution.
❌ If Data Has Dependencies – Shared states can cause race conditions.
❌ For I/O Operations – Parallelism may slow down disk/network I/O instead of improving performance.
5. Real-World Example – Processing a Large Dataset in Parallel
Imagine we have a list of words and we want to convert them to uppercase in parallel.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("apple", "banana", "cherry", "date", "elderberry", "fig", "grape");
List<String> upperCaseWords = words.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseWords);
}
}
✅ This example efficiently converts words to uppercase using multiple CPU cores.
Key Takeaways
✔ parallelStream()
can significantly improve performance for large datasets.
✔ Best for CPU-intensive tasks (e.g., calculations, sorting).
✔ Avoid for small collections or shared state operations.
Final Thoughts
- Streams are more concise and efficient than traditional loops.
- Lazy evaluation optimizes performance by stopping early when possible.
map()
,filter()
, andreduce()
solve real-world problems like currency conversion, user filtering, and salary aggregation.
The next Java 8 feature: Default & Static Methods in Interfaces