foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • 5
  • quiz
Java
  • Understand how to create and use streams
  • Master intermediate operations: filter, map, flatMap, sorted
  • Learn terminal operations: collect, reduce, forEach

Stream API Basics

Before Java 8, processing collections meant loops—often nested, with conditionals scattered everywhere. It worked, but was verbose, error-prone, hard to parallelize. The Stream API changed everything with a functional, declarative approach.

Instead of telling Java how to process data (loop, check, add), streams let you describe what you want (filter, transform, collect). More readable, maintainable, often more efficient.

What is a Stream?

A stream is a sequence of elements supporting operations. Like a factory pipeline:

  1. Source: Data enters (collection, array, generator)
  2. Intermediate operations: Processing stations (filter, transform, sort)
  3. Terminal operation: Finished products (collected, counted, reduced)

Streams vs Collections

It's important to understand that streams are NOT collections:

Collections Streams
Store elements in memory Don't store elements - they flow through
Can be accessed multiple times Can only be consumed once
Eager - elements exist now Lazy - computed on demand
You iterate externally (for loop) Iteration happens internally
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// Collection approach: external iteration
for (String name : names) {
    if (name.length() > 3) {
        System.out.println(name.toUpperCase());
    }
}

// Stream approach: internal iteration, declarative
names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

Creating Streams

From Collections

The most common way to create a stream is from a collection:

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

// For parallel processing
Stream<String> parallelStream = list.parallelStream();

From Arrays

String[] array = {"x", "y", "z"};
Stream<String> stream = Arrays.stream(array);

Using Stream.of()

Stream<String> stream = Stream.of("one", "two", "three");

Generating Streams

For special cases, you can generate streams:

// Empty stream
Stream<String> empty = Stream.empty();

// Infinite stream of even numbers: 0, 2, 4, 6, ...
Stream<Integer> evens = Stream.iterate(0, n -> n + 2);

// Infinite stream of random numbers
Stream<Double> randoms = Stream.generate(Math::random);

// Iterate with condition (Java 9+)
Stream<Integer> limited = Stream.iterate(0, n -> n < 100, n -> n + 10);
// Produces: 0, 10, 20, 30, ..., 90

Primitive Streams

To avoid boxing overhead, Java provides specialized streams for primitives:

// IntStream for int values
IntStream numbers = IntStream.of(1, 2, 3, 4, 5);
IntStream range = IntStream.range(1, 10);      // 1 to 9
IntStream rangeClosed = IntStream.rangeClosed(1, 10); // 1 to 10

// Useful for numeric operations
int sum = IntStream.rangeClosed(1, 100).sum();  // 5050
double avg = IntStream.rangeClosed(1, 100).average().getAsDouble(); // 50.5

Understanding Lazy Evaluation

A crucial concept in streams is lazy evaluation. Intermediate operations don't execute immediately - they only set up the pipeline. Actual processing happens only when a terminal operation is called.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

Stream<String> stream = names.stream()
    .filter(name -> {
        System.out.println("Filtering: " + name);
        return name.length() > 3;
    })
    .map(name -> {
        System.out.println("Mapping: " + name);
        return name.toUpperCase();
    });

System.out.println("Stream created, nothing printed yet!");

// Only when we call a terminal operation...
stream.forEach(System.out::println);

This laziness enables optimizations like short-circuiting - if you only need the first result, the stream won't process all elements.

Intermediate Operations

Intermediate operations transform a stream into another stream. They're always lazy.

filter() - Selecting Elements

filter() keeps only elements that match a condition (predicate):

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Keep only even numbers
List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
// Result: [2, 4, 6, 8, 10]

// Multiple filters can be chained
List<Integer> result = numbers.stream()
    .filter(n -> n > 3)     // Greater than 3
    .filter(n -> n < 8)     // Less than 8
    .collect(Collectors.toList());
// Result: [4, 5, 6, 7]

map() - Transforming Elements

map() transforms each element using a function:

List<String> names = Arrays.asList("alice", "bob", "charlie");

// Transform to uppercase
List<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// Result: [ALICE, BOB, CHARLIE]

// Transform to different type
List<Integer> lengths = names.stream()
    .map(String::length)
    .collect(Collectors.toList());
// Result: [5, 3, 7]

// Chain transformations
List<String> result = names.stream()
    .map(String::toUpperCase)
    .map(s -> s + "!")
    .collect(Collectors.toList());
// Result: [ALICE!, BOB!, CHARLIE!]

flatMap() - Flattening Nested Structures

When each element maps to multiple elements (or a stream), flatMap() flattens the result:

// Problem: map() creates Stream of Streams
List<List<Integer>> nested = Arrays.asList(
    Arrays.asList(1, 2, 3),
    Arrays.asList(4, 5),
    Arrays.asList(6, 7, 8)
);

// flatMap flattens to a single stream
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
// Result: [1, 2, 3, 4, 5, 6, 7, 8]

// Practical example: words from sentences
List<String> sentences = Arrays.asList("Hello World", "Java Streams");
List<String> words = sentences.stream()
    .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
    .collect(Collectors.toList());
// Result: [Hello, World, Java, Streams]

sorted() - Ordering Elements

List<String> names = Arrays.asList("Charlie", "Alice", "Bob");

// Natural order
List<String> sorted = names.stream()
    .sorted()
    .collect(Collectors.toList());
// Result: [Alice, Bob, Charlie]

// Reverse order
List<String> reversed = names.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());
// Result: [Charlie, Bob, Alice]

// Custom sorting - by length
List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .collect(Collectors.toList());
// Result: [Bob, Alice, Charlie]

distinct(), limit(), and skip()

// Remove duplicates
List<Integer> unique = Arrays.asList(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .collect(Collectors.toList());
// Result: [1, 2, 3]

// Take first N elements
List<Integer> firstThree = Stream.iterate(1, n -> n + 1)
    .limit(3)
    .collect(Collectors.toList());
// Result: [1, 2, 3]

// Skip first N elements
List<Integer> skipTwo = Arrays.asList(1, 2, 3, 4, 5).stream()
    .skip(2)
    .collect(Collectors.toList());
// Result: [3, 4, 5]

Terminal Operations

Terminal operations trigger the processing and produce a result or side-effect.

forEach() - Performing Actions

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// Print each element
names.stream().forEach(System.out::println);

// With more complex action
names.stream().forEach(name -> {
    System.out.println("Processing: " + name);
    // do something else
});

Note: forEach is for side-effects. Prefer collect() when building a result.

collect() - Gathering Results

collect() is the most versatile terminal operation. The Collectors class provides many useful collectors:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alice");

// To List
List<String> list = names.stream().collect(Collectors.toList());

// To Set (removes duplicates)
Set<String> set = names.stream().collect(Collectors.toSet());

// Join to String
String joined = names.stream().collect(Collectors.joining(", "));
// Result: "Alice, Bob, Charlie, Alice"

String withBrackets = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));
// Result: "[Alice, Bob, Charlie, Alice]"

// To Map
Map<String, Integer> nameLengths = names.stream()
    .distinct()
    .collect(Collectors.toMap(
        name -> name,        // key
        String::length       // value
    ));
// Result: {Alice=5, Bob=3, Charlie=7}

Grouping and Partitioning

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Anna", "Brian");

// Group by first letter
Map<Character, List<String>> byFirstLetter = names.stream()
    .collect(Collectors.groupingBy(name -> name.charAt(0)));
// Result: {A=[Alice, Anna], B=[Bob, Brian], C=[Charlie]}

// Group by length
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));
// Result: {3=[Bob], 4=[Anna], 5=[Alice, Brian], 7=[Charlie]}

// Partition (split into two groups)
Map<Boolean, List<String>> partition = names.stream()
    .collect(Collectors.partitioningBy(name -> name.length() > 4));
// Result: {false=[Bob, Anna], true=[Alice, Brian, Charlie]}

reduce() - Combining Elements

reduce() combines all elements into a single result:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Sum all numbers
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
// Starts with 0, adds each number
// Result: 15

// With method reference
int sum2 = numbers.stream()
    .reduce(0, Integer::sum);

// Find maximum
Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);
// Result: Optional[5]

// Concatenate strings
List<String> words = Arrays.asList("Hello", " ", "World");
String sentence = words.stream()
    .reduce("", String::concat);
// Result: "Hello World"

Finding and Matching

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

// Find first matching
Optional<Integer> firstEven = numbers.stream()
    .filter(n -> n % 2 == 0)
    .findFirst();
// Result: Optional[2]

// Find any matching (useful for parallel)
Optional<Integer> anyEven = numbers.stream()
    .filter(n -> n % 2 == 0)
    .findAny();

// Check if any match
boolean hasEven = numbers.stream()
    .anyMatch(n -> n % 2 == 0);  // true

// Check if all match
boolean allPositive = numbers.stream()
    .allMatch(n -> n > 0);  // true

// Check if none match
boolean noneNegative = numbers.stream()
    .noneMatch(n -> n < 0);  // true

// Count elements
long count = numbers.stream()
    .filter(n -> n % 2 == 0)
    .count();  // 3

A Complete Example

Let's put it all together with a real-world example:

class Employee {
    String name;
    String department;
    double salary;
    // constructor, getters...
}

List<Employee> employees = getEmployees();

// Find the average salary of IT department employees earning over 50000
double avgSalary = employees.stream()
    .filter(e -> e.getDepartment().equals("IT"))
    .filter(e -> e.getSalary() > 50000)
    .mapToDouble(Employee::getSalary)
    .average()
    .orElse(0.0);

// Get names of top 3 highest paid employees
List<String> topEarners = employees.stream()
    .sorted(Comparator.comparing(Employee::getSalary).reversed())
    .limit(3)
    .map(Employee::getName)
    .collect(Collectors.toList());

// Group employees by department with their count
Map<String, Long> countByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.counting()
    ));

Streams don't store data—they process it. Intermediate operations are lazy—they don't execute until you call a terminal operation. Streams can only be consumed once. Use them for data transformation, loops for simple iterations with side effects. Benefits: readable declarative code, easy parallelization with parallelStream(), powerful operations like grouping and reducing, lazy evaluation for efficiency.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service