Understanding Streams in Java
Introduction to Java Streams
Java Streams, introduced in Java 8, are a powerful and expressive tool for processing sequences of elements, unlike collections, which store and group data, streams are designed to process data declaratively, providing a high-level abstraction for operations such as filtering, mapping, and reducing.
What are Streams?
Streams are sequences of elements that support various operations to process data. A stream does not store data; instead, it conveys elements from a source (such as a collection) through a pipeline of operations. Streams allow for complex data processing while maintaining a clean and readable code structure.
Streams vs Collections
Collections:
Data storage mechanism.
Can be iterated directly.
Eager in nature (process elements as they are added).
Streams:
Data processing mechanism.
Cannot be iterated directly.
Lazy in nature (process elements on-demand).
Stream Operations
Stream operations are classified into intermediate and terminal operations.
Intermediate Operations:
Return a new stream.
Lazy and allow for pipeline construction.
Examples:
filter()
,map()
,sorted()
.
Terminal Operations:
Produce a result or side effect.
Trigger the processing of the stream pipeline.
Examples:
collect()
,forEach()
,reduce()
.
Benefits and Pitfalls of Streams
Benefits:
Declarative and readable code.
Easy parallelization.
Improved performance for large datasets.
Pitfalls:
Learning curve for developers new to functional programming.
Overhead of creating streams.
Misuse can lead to performance degradation.
Types of Streams
Stream<T>: Generic stream.
IntStream, LongStream, DoubleStream: Streams for primitive types.
ParallelStream: Stream that supports parallel processing.
Examples
Stream<T> Examples
Basic Filtering:
List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe"); List<String> filteredNames = names.stream() .filter(name -> name.startsWith("J")) .collect(Collectors.toList()); System.out.println(filteredNames); // [John, Jane, Jack]
names.stream
()
creates a stream from the list of names.filter(name -> name.startsWith("J"))
filters the stream to include only names that start with "J".collect(Collectors.toList())
collects the filtered names into a list.
Mapping:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> squares = numbers.stream() .map(n -> n * n) .collect(Collectors.toList()); System.out.println(squares); // [1, 4, 9, 16, 25]
numbers.stream
()
creates a stream from the list of numbers.map(n -> n * n)
maps each number to its square.collect(Collectors.toList())
collects the squared numbers into a list.
Primitive Stream Examples
IntStream Example:
IntStream.range(1, 10) .filter(n -> n % 2 == 0) .forEach(System.out::println); // 2 4 6 8
IntStream.range(1, 10)
creates an IntStream from 1 to 9.filter(n -> n % 2 == 0)
filters the stream to include only even numbers.forEach(System.out::println)
prints each number.
DoubleStream Example:
DoubleStream.of(1.0, 2.5, 3.6, 4.8) .average() .ifPresent(System.out::println); // 3.0
DoubleStream.of(1.0, 2.5, 3.6, 4.8)
creates a DoubleStream with given values.average()
calculates the average of the stream.ifPresent(System.out::println)
prints the average if it is present.
Parallel Stream Examples
Basic Parallel Stream:
List<String> words = Arrays.asList("one", "two", "three", "four"); words.parallelStream() .forEach(System.out::println);
words.parallelStream()
creates a parallel stream from the list of words.forEach(System.out::println)
prints each word in parallel.
Parallel Stream with Reduction:
List<Integer> values = Arrays.asList(1, 2, 3, 4, 5); int sum = values.parallelStream() .reduce(0, Integer::sum); System.out.println(sum); // 15
values.parallelStream()
creates a parallel stream from the list of values.reduce(0, Integer::sum)
reduces the stream to a sum, starting with 0.System.out.println(sum)
prints the sum.
Complex Stream Examples
Grouping and Counting:
List<String> items = Arrays.asList("apple", "banana", "apple", "orange", "banana", "apple"); Map<String, Long> itemCount = items.stream() .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); System.out.println(itemCount); // {banana=2, orange=1, apple=3}
items.stream
()
creates a stream from the list of items.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
groups the items by their identity and counts them.System.out.println(itemCount)
prints the item count map.
Nested Streams:
List<List<String>> nestedList = Arrays.asList( Arrays.asList("a", "b", "c"), Arrays.asList("d", "e", "f"), Arrays.asList("g", "h", "i") ); List<String> flatList = nestedList.stream() .flatMap(List::stream) .collect(Collectors.toList()); System.out.println(flatList); // [a, b, c, d, e, f, g, h, i]
nestedList.stream
()
creates a stream from the list of lists.flatMap(List::stream)
flattens the nested lists into a single stream.collect(Collectors.toList())
collects the flattened elements into a list.System.out.println(flatList)
prints the flattened list.
Stream of Objects:
class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } } List<Person> people = Arrays.asList( new Person("John", 25), new Person("Jane", 30), new Person("Jack", 20) ); List<String> names = people.stream() .map(person -> person.name) .collect(Collectors.toList()); System.out.println(names); // [John, Jane, Jack]
people.stream
()
creates a stream from the list of people.map(person ->
person.name
)
maps each person to their name.collect(Collectors.toList())
collects the names into a list.System.out.println(names)
prints the list of names.
Examples Without Streams
Filtering Without Streams:
List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe"); List<String> filteredNames = new ArrayList<>(); for (String name : names) { if (name.startsWith("J")) { filteredNames.add(name); } } System.out.println(filteredNames); // [John, Jane, Jack]
Iterates through the list of names.
Checks if each name starts with "J".
Adds the matching names to a new list.
Prints the filtered list.Mapping Without Streams:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = new ArrayList<>();
for (Integer number : numbers) {
squares.add(number * number);
}
System.out.println(squares); // [1, 4, 9, 16, 25]
Iterates through the list of numbers.
Squares each number.
Adds the squared numbers to a new list.
Prints the list of squares.
Examples Using Streams
Filtering With Streams:
List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe"); List<String> filteredNames = names.stream() .filter(name -> name.startsWith("J")) .collect(Collectors.toList()); System.out.println(filteredNames); // [John, Jane, Jack]
Creates a stream from the list of names.
Filters the stream to include only names that start with "J".
Collects the filtered names into a list.
Prints the filtered list### Understanding Java Streams
Mapping With Streams:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> squares = numbers.stream() .map(n -> n * n) .collect(Collectors.toList()); System.out.println(squares); // [1, 4, 9, 16, 25]
Creates a stream from the list of numbers.
Maps each number to its square.
Collects and prints the squares.
By understanding and utilizing Java Streams, developers can write more concise, readable, and efficient code, enhancing their ability to process data in a functional and declarative manner.
Subscribe to my newsletter
Read articles from André Felipe Costa Bento directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
André Felipe Costa Bento
André Felipe Costa Bento
Fullstack Software Engineer.