Java Streams
What are Java Streams? #
Java Streams was introduced in Java 8 as a way to process collections of data in a functional style. Streams allow you to perform operations on collections, such as filtering, mapping, and reducing, in a declarative way. This can make your code more concise and easier to read.
A Stream is like a pipeline that processes data.
Think of a stream as a pipeline that processes data. You start with a source of data, such as a collection, and then apply a series of operations to transform the data. Each operation in the pipeline is a step in the process, and the final result is the output of the pipeline.
Imperative vs Declarative Programming #
Imperative Programming #
In the imperative programming style, you focus on how to perform a task.
Imperative programming is the traditional way of writing code, where you specify the exact steps that the computer should take to solve a problem. For example, if you wanted to sum the elements of a list in an imperative style, you might write code like this:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (int number : numbers) {
if (number % 2 != 0) {
sum += number * number;
}
}
System.out.println(sum);
This code requires step-by-step analysis to comprehend its function. While it’s clear that a sum is being calculated, it might not be immediately obvious that it’s summing the squares of the odd numbers in the list.
Declarative Programming #
In the declarative programming style, you focus on what you want to achieve.
Declarative programming, on the other hand, focuses on what you want to achieve, rather than how you want to achieve it. With streams, you can write code that describes the operations you want to perform on a collection, without specifying the exact steps to take. For example, you could sum the elements of a list using streams like this:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(number -> number % 2 != 0)
.map(number -> number * number)
.mapToInt(Integer::intValue)
.sum();
System.out.println(sum);
In this code, you are declaring that you want to sum the elements of the list, rather than specifying the exact steps to take. You can see that the end result is a sum, and that a filter is applied to select only the odd numbers, and that the numbers are squared before being summed.
This enhances code readability and comprehension, allowing for a quick grasp of the code’s function without the need to trace the precise steps.
Flow of Stream Operations #
There are 3 main stages in a Stream pipeline:
- Stream Creation
- Intermediate Operations
- Terminal Operations
e.g. filter(),map(),distinct()"] C --> D["Terminal Operations
e.g. collect(), forEach(), reduce()"] D --> E[Result]
How to create a Stream #
There are multiple ways to create a stream in Java. Here are some of the most common ways:
From a Collection #
You can create a stream from a collection using the stream() method:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream();
From an Array #
You can create a stream from an array using the Arrays.stream() method:
String[] array = {"a", "b", "c"};
Stream<String> streamFromArray = Arrays.stream(array);
// Or
Stream<String> streamFromArray2 = Stream.of(array);
From a Range of Values #
You can create a stream from a range of values using the IntStream.range() method:
IntStream streamOfRange = IntStream.range(1, 5);
From a File #
You can create a stream from a file using the Files.lines() method:
try (Stream<String> streamFromFile = Files.lines(Paths.get("path/to/file.txt"))) {
streamFromFile.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
From a Stream Builder #
You can create a stream using a Stream Builder:
Stream<String> streamFromBuilder = Stream.<String>builder()
.add("a")
.add("b")
.add("c")
.build();
Inifinate Streams #
You can create infinite streams using Stream.iterate() or Stream.generate().
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);
infiniteStream.limit(10).forEach(System.out::println);
Stream<Double> randomNumbers = Stream.generate(Math::random);
randomNumbers.limit(5).forEach(System.out::println);
From a Pattern (Regex) #
You can create a stream from a pattern using the Pattern.splitAsStream() method:
String str = "a,b,c";
Stream<String> streamFromPattern = Pattern.compile(",").splitAsStream(str);
streamFromPattern.forEach(System.out::println);
Intermediate Operations #
Intermediate operations are operations that transform the elements of a stream. Some common intermediate operations include:
Filtering and Mapping #
These operations allow you to transform and filter elements in a stream:
| Method | Description |
|---|---|
filter(Predicate<T> predicate) |
Filters elements based on a given predicate. |
map(Function<T, R> mapper) |
Transforms each element using a provided function. |
flatMap(Function<T, Stream<R>> mapper) |
Flattens nested streams produced by mapping each element. |
Distinct and Sorted #
These operations help manage the order and uniqueness of stream elements:
| Method | Description |
|---|---|
distinct() |
Removes duplicate elements from the stream. |
sorted() |
Sorts elements in their natural order. |
sorted(Comparator<T> comparator) |
Sorts elements using a specified comparator. |
Limiting and Skipping #
These operations control the number of elements processed in the stream:
| Method | Description |
|---|---|
limit(long maxSize) |
Limits the stream to a maximum number of elements. |
skip(long n) |
Skips the first n elements in the stream. |
Peeking #
This operation is useful for debugging and observing stream elements:
| Method | Description |
|---|---|
peek(Consumer<T> action) |
Performs an action on each element without modifying the stream. |
Primitive Stream Mappings #
These operations convert objects to primitive streams for specialized processing:
| Method | Description |
|---|---|
mapToInt(ToIntFunction<T> mapper) |
Transforms elements into an IntStream. |
mapToDouble(ToDoubleFunction<T> mapper) |
Transforms elements into a DoubleStream. |
mapToLong(ToLongFunction<T> mapper) |
Transforms elements into a LongStream. |
Flattening Primitive Streams #
These operations flatten nested primitive streams into a single stream:
| Method | Description |
|---|---|
flatMapToInt(Function<T, IntStream> mapper) |
Flattens nested IntStreams. |
flatMapToDouble(Function<T, DoubleStream> mapper) |
Flattens nested DoubleStreams. |
flatMapToLong(Function<T, LongStream> mapper) |
Flattens nested LongStreams. |
The filter() method is used to filter elements based on a predicate:
Terminal Operations #
Streams are lazy, meaning that they don’t perform any operations until a terminal operation is called.
Java Streams use a lazy evaluation strategy. This means that intermediate operations (such as filter(), map(), etc.) are not executed immediately. They are stored and executed only when a terminal operation (such as collect(), forEach(), reduce(), etc.) is called.
Consider the following example code to illustrate this concept:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Stream<String> nameStream = names.stream()
.filter(name -> {
System.out.println("Filtering: " + name);
return name.startsWith("A");
})
.map(name -> {
System.out.println("Mapping: " + name);
return name.toUpperCase();
});
// Stream pipeline defined. no processing yet
// Stream pipeline starts processing when terminal operation is called
nameStream.forEach(System.out::println);