Processing Collections with Java Streams

Understanding Streams in Java

Introduction and Stream Utility

Consider a scenario where you need to filter and process a collection of strings. Given a list of names, the task are: extract names starting with a specific prefix, further filter those by length, and finally display the results.

Example using a traditional approach:

import java.util.ArrayList;

public class TraditionalFilter {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("Zhang Wuji");
        names.add("Zhou Zhiruo");
        names.add("Zhao Min");
        names.add("Zhang Qiang");
        names.add("Zhang Sanfeng");

        ArrayList<String> filteredByPrefix = new ArrayList<>();
        for (String name : names) {
            if (name.startsWith("Zhang")) {
                filteredByPrefix.add(name);
            }
        }

        ArrayList<String> finalFilteredList = new ArrayList<>();
        for (String name : filteredByPrefix) {
            if (name.length() == 3) { // Assuming length refers to character count
                finalFilteredList.add(name);
            }
        }
        System.out.println(finalFilteredList);
    }
}

Equivalent implementation using Streams:

import java.util.ArrayList;

public class StreamFilterExample {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("Zhang Wuji");
        names.add("Zhou Zhiruo");
        names.add("Zhao Min");
        names.add("Zhang Qiang");
        names.add("Zhang Sanfeng");

        names.stream()
             .filter(name -> name.startsWith("Zhang"))
             .filter(name -> name.length() == 3)
             .forEach(System.out::println);
    }
}

Purpose of Streams

Streams, when used with Lambda expressions, provide a declarative and concise way to perform operations on collections and arrays.

Stream Creation Methods

The first step is to obtain a Stream pipeline and place data on it.

1. From a Single-Column Collection (e.g., List, Set):

import java.util.ArrayList;
import java.util.Collections;

public class ListStreamExample {
    public static void main(String[] args) {
        ArrayList<String> itemList = new ArrayList<>();
        Collections.addAll(itemList, "alpha", "beta", "gamma", "delta");
        itemList.stream().forEach(elem -> System.out.println(elem));
    }
}

2. From a Double-Column Collection (e.g., Map):

import java.util.HashMap;

public class MapStreamExample {
    public static void main(String[] args) {
        HashMap<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("Alice", 95);
        scoreMap.put("Bob", 87);
        scoreMap.put("Charlie", 92);
        // Process keys
        scoreMap.keySet().stream().forEach(key -> System.out.println(key));
        // Process entries
        scoreMap.entrySet().stream().forEach(entry -> System.out.println(entry));
    }
}

3. From an Array:

import java.util.Arrays;

public class ArrayStreamExample {
    public static void main(String[] args) {
        int[] numberArray = {10, 20, 30, 40, 50};
        Arrays.stream(numberArray).forEach(num -> System.out.println(num));
    }
}

4. From a Set of Discrete Values:

import java.util.stream.Stream;

public class ValueStreamExample {
    public static void main(String[] args) {
        Stream.of("first", "second", "third").forEach(val -> System.out.println(val));
        Stream.of(100, 200, 300).forEach(val -> System.out.println(val));
        // Note: Stream.of() accepts a variable number of arguments or a reference type array.
        // Passing a primitive array will treat the entire array as a single element.
    }
}

Intermediate and Terminal Operations

Once a Stream is obtained, operations are performed using its API.

Intermediate Operations: These are methods that return a new Stream, allowing further chaining.

  • Filtering (filter, limit, skip):
import java.util.ArrayList;
import java.util.Collections;

public class IntermediateOperationsDemo {
    public static void main(String[] args) {
        ArrayList<String> dataList = new ArrayList<>();
        Collections.addAll(dataList, "Zhang", "Zhou", "Zhao", "Zhang San", "Li");
        // Filter names starting with 'Zhang' and having length 3
        dataList.stream()
                .filter(str -> str.startsWith("Zhang"))
                .filter(str -> str.length() == 3)
                .forEach(str -> System.out.println(str));
        // Get first 2 elements
        dataList.stream().limit(2).forEach(str -> System.out.println(str));
        // Skip the first element
        dataList.stream().skip(1).forEach(str -> System.out.println(str));
    }
}
  • Removing Duplicates (distinct):
import java.util.ArrayList;
import java.util.Collections;

public class DistinctExample {
    public static void main(String[] args) {
        ArrayList<String> repeatedList = new ArrayList<>();
        Collections.addAll(repeatedList, "A", "A", "B", "C", "B");
        repeatedList.stream().distinct().forEach(item -> System.out.println(item));
    }
}
  • Transformation (map):
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;

public class MapExample {
    public static void main(String[] args) {
        ArrayList<String> infoList = new ArrayList<>();
        Collections.addAll(infoList, "John-25", "Jane-30", "Jack-28");
        // Extract and print ages using anonymous class
        infoList.stream().map(new Function<String, Integer>() {
            @Override
            public Integer apply(String record) {
                String[] parts = record.split("-");
                return Integer.parseInt(parts[1]);
            }
        }).forEach(age -> System.out.println(age));
        // Equivalent using lambda
        infoList.stream()
                .map(record -> Integer.parseInt(record.split("-")[1]))
                .forEach(age -> System.out.println(age));
    }
}

Important Notes on Intermediate Operations:

  1. Each intermediate method returns a new Stream. The original Stream object should not be reused.
  2. Operations on a Stream do not modify the source collection or array.

Terminal Operations: These are final operations that consume the Stream and produce a result or side-effect. No further operations can be chained.

  • Iteration (forEach), Counting (count), and Conversion to Array (toArray):
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.IntFunction;

public class TerminalOperationsDemo {
    public static void main(String[] args) {
        ArrayList<String> sampleList = new ArrayList<>();
        Collections.addAll(sampleList, "Cat", "Dog", "Bird", "Fish");
        // forEach with lambda
        sampleList.stream().forEach(element -> System.out.println(element));
        // Count elements
        long total = sampleList.stream().count();
        System.out.println("Count: " + total);
        // Convert to an Object array
        Object[] objArray = sampleList.stream().toArray();
        System.out.println(Arrays.toString(objArray));
        // Convert to a typed String array using IntFunction
        String[] strArray = sampleList.stream().toArray(new IntFunction<String[]>() {
            @Override
            public String[] apply(int size) {
                return new String[size];
            }
        });
        // Equivalent lambda expression
        String[] strArrayLambda = sampleList.stream().toArray(length -> new String[length]);
        System.out.println(Arrays.toString(strArrayLambda));
    }
}

Tags: java Stream API Collections Functional Programming Lambda Expressions

Posted on Thu, 10 Sep 2026 16:12:52 +0000 by jemrys