Posted on: January 18, 2025 Posted by: rahulgite Comments: 0

Java Streams, introduced in Java 8, provide a powerful way to process collections of data in a functional style. They allow operations such as filtering, mapping, and reducing, with the ability to execute these operations sequentially or in parallel.


What is a Stream?

A Stream is a sequence of elements from a source that supports aggregate operations. Streams are not data structures; they work on the data from collections, arrays, or input/output channels. A stream pipeline consists of three stages:

  1. Source: The data source (e.g., a collection, array, or generator).
  2. Intermediate Operations: Transformations (e.g., filter, map).
  3. Terminal Operations: Produces a result (e.g., collect, forEach).

Key Characteristics of Streams

  1. Functional: Supports functional programming through lambdas.
  2. Lazy Evaluation: Intermediate operations are not executed until a terminal operation is invoked.
  3. Parallelizable: Streams can be processed in parallel.
  4. Stateless: Operations do not store state, ensuring immutability.
  5. One-Time Use: A stream cannot be reused once a terminal operation is performed.

Creating Streams

  1. From Collections
List<String> list = Arrays.asList("A", "B", "C");
Stream<String> stream = list.stream();
System.out.println(stream.count());
  1. From Arrays
int[] numbers = {1, 2, 3, 4};
IntStream stream = Arrays.stream(numbers);
System.out.println(stream.sum());
  1. From Static Methods
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
System.out.println(stream.collect(Collectors.toList()));
  1. From Generators or Iterators
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
randoms.forEach(System.out::println);
  1. From Files
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
    lines.forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}

Stream Operations

Intermediate Operations

These operations transform the stream but do not produce results directly.

  1. filter: Filters elements based on a predicate.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> filteredStream = numbers.stream().filter(n -> n % 2 == 0);
filteredStream.forEach(System.out::println); // Output: 2, 4
  1. map: Transforms each element into another object.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> mappedStream = names.stream().map(String::toUpperCase);
mappedStream.forEach(System.out::println); // Output: ALICE, BOB, CHARLIE
  1. flatMap: Flattens nested streams into a single stream.
List<List<Integer>> nested = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4));
Stream<Integer> flatMappedStream = nested.stream().flatMap(Collection::stream);
flatMappedStream.forEach(System.out::println); // Output: 1, 2, 3, 4
  1. distinct: Removes duplicate elements.
Stream<Integer> distinctStream = Stream.of(1, 2, 2, 3).distinct();
distinctStream.forEach(System.out::println); // Output: 1, 2, 3
  1. sorted: Sorts elements (natural or custom order).
Stream<Integer> sortedStream = Stream.of(3, 1, 4, 2).sorted();
sortedStream.forEach(System.out::println); // Output: 1, 2, 3, 4
  1. limit: Limits the number of elements in the stream.
Stream<Integer> limitedStream = Stream.of(1, 2, 3, 4, 5).limit(3);
limitedStream.forEach(System.out::println); // Output: 1, 2, 3
  1. skip: Skips the first N elements.
Stream<Integer> skippedStream = Stream.of(1, 2, 3, 4, 5).skip(2);
skippedStream.forEach(System.out::println); // Output: 3, 4, 5

Terminal Operations

These operations produce a result or side effect and terminate the stream.

  1. forEach: Performs an action for each element.
Stream<String> stringStream = Stream.of("A", "B", "C");
stringStream.forEach(System.out::println); // Output: A, B, C
  1. toArray: Collects elements into an array.
Stream<Integer> intStream = Stream.of(1, 2, 3);
Object[] arr = intStream.toArray();
System.out.println(Arrays.toString(arr)); // Output: [1, 2, 3]
  1. reduce: Reduces elements to a single value.
Stream<Integer> numberStream = Stream.of(1, 2, 3);
int sum = numberStream.reduce(0, Integer::sum);
System.out.println(sum); // Output: 6
  1. collect: Gathers elements into a collection.
Stream<String> collectStream = Stream.of("A", "B", "C");
List<String> list = collectStream.collect(Collectors.toList());
System.out.println(list); // Output: [A, B, C]
  1. count: Counts the elements in the stream.
Stream<Integer> countStream = Stream.of(1, 2, 3);
long count = countStream.count();
System.out.println(count); // Output: 3
  1. anyMatch / allMatch / noneMatch: Tests conditions on elements.
Stream<Integer> matchStream = Stream.of(1, 2, 3);
boolean anyEven = matchStream.anyMatch(n -> n % 2 == 0);
System.out.println(anyEven); // Output: true
  1. findFirst / findAny: Retrieves elements from the stream.
Stream<Integer> findStream = Stream.of(1, 2, 3);
Optional<Integer> first = findStream.findFirst();
first.ifPresent(System.out::println); // Output: 1

Parallel Streams

Parallel streams divide the data and process it in multiple threads.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.parallelStream().forEach(System.out::println);

Note: Use parallel streams carefully to avoid race conditions.


Common Collectors

Collectors are used with collect to gather elements into collections or perform aggregations.

  1. toList / toSet
Stream<Integer> listStream = Stream.of(1, 2, 3);
List<Integer> list = listStream.collect(Collectors.toList());
System.out.println(list); // Output: [1, 2, 3]
  1. toMap: Collects elements into a map.
Stream<Integer> mapStream = Stream.of(1, 2, 3);
Map<Integer, String> map = mapStream.collect(Collectors.toMap(n -> n, n -> "Number: " + n));
System.out.println(map); // Output: {1=Number: 1, 2=Number: 2, 3=Number: 3}
  1. joining: Concatenates elements into a single string.
Stream<String> joinStream = Stream.of("A", "B", "C");
String result = joinStream.collect(Collectors.joining(", "));
System.out.println(result); // Output: A, B, C
  1. groupingBy: Groups elements by a classifier function.
Stream<String> groupStream = Stream.of("one", "two", "three");
Map<Integer, List<String>> grouped = groupStream.collect(Collectors.groupingBy(String::length));
System.out.println(grouped); // Output: {3=[one, two], 5=[three]}
  1. partitioningBy: Partitions elements based on a predicate.
Stream<Integer> partitionStream = Stream.of(1, 2, 3, 4);
Map<Boolean, List<Integer>> partitioned = partitionStream.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println(partitioned); // Output: {false=[1, 3], true=[2, 4]}
  1. summarizingInt / summarizingDouble / summarizingLong: Provides summary statistics.
Stream<Integer> summaryStream = Stream.of(1, 2, 3);
IntSummaryStatistics stats = summaryStream.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum()); // Output: 6
  1. averagingInt / averagingDouble / averagingLong: Calculates the average.
Stream<Integer> averageStream = Stream.of(1, 2, 3);
double average = averageStream.collect(Collectors.averagingInt(Integer::intValue));
System.out.println(average); // Output: 2.0
  1. reducing: Performs a reduction operation.
Stream<Integer> reducingStream = Stream.of(1, 2, 3);
int reducedSum = reducingStream.collect(Collectors.reducing(0, Integer::sum));
System.out.println(reducedSum); // Output: 6
  1. mapping: Applies a function to elements and collects the results.
Stream<String> mappingStream = Stream.of("Alice", "Bob");
List<String> upperCaseNames = mappingStream.collect(Collectors.mapping(String::toUpperCase, Collectors.toList()));
System.out.println(upperCaseNames); // Output: [ALICE, BOB]
  1. collectingAndThen: Transforms the result of a collection operation.
Stream<String> thenStream = Stream.of("A", "B");
List<String> unmodifiableList = thenStream.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
System.out.println(unmodifiableList);

Conclusion

Java Streams simplify data processing through a functional and declarative approach. By leveraging intermediate and terminal operations, you can create complex pipelines for transforming, filtering, and aggregating data efficiently. Understanding streams is crucial for writing modern Java applications.

Leave a Comment