This content originally appeared on Level Up Coding - Medium and was authored by Ivan Polovyi
The Stream API allows us to manipulate data in various ways. One of the most common use cases is transforming data from one type or form to another—a process known as mapping. The Java Stream API provides a wide range of mapping operations. In this tutorial, we’ll explore each of these operations with examples.
All mapping operations in the Stream API are intermediate operations. Typically, mapping operations are stateless because they process each element of the stream independently. However, in rare and custom scenarios, certain mapping operations might introduce statefulness, though this is uncommon with the standard implementations and generally discouraged because it can lead to unexpected behavior.
I recently published a tutorial on intermediate operations in the Stream API, so I won’t repeat those details here. You can check it out via the provided link if you're interested.
Java Stream API: Exploring Intermediate Operations
Now, without further ado, let’s explore the mapping functions.
Set up
This tutorial will be example-driven, so I’ll create a record class that we’ll use in the upcoming examples.
public record CustomerPurchase(String id,
String customerId,
List<String> productIds,
int quantity,
double price,
String paymentMethod) {
}
If you’d like to learn more about records, follow the link below:
Let's create a list of objects from this record:
private static List<Transaction> transactions = List.of(
new Transaction("1",
"customer1",
List.of("Laptop", "Headset"),
1150.00,
LocalDateTime.of(2024, 8, 20, 10, 19)),
new Transaction("2",
"customer2",
List.of("Smartphone"),
600.99,
LocalDateTime.of(2024, 8, 21, 1, 45)),
new Transaction("3",
"customer1",
List.of("Monitor", "Keyboard", "Mouse"),
380.00,
LocalDateTime.of(2024, 8, 22, 3, 30))
);
The Map Operation
The first operation we’ll explore is, of course, the map() method.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
The map() method accepts a Function that transforms each element of the stream from one type to another, producing a new stream with the mapped elements. For example, we can map a stream of strings to a stream of integers.
Stream<String> exampleStream1 = Stream.of("1", "2", "3", "4", "5");
Stream<Integer> mappedStream1 = exampleStream1
//.map(s -> Integer.parseInt(s)) -- lambda version
.map(Integer::parseInt);
List<Integer> example1 = mappedStream1.toList();
// [1, 2, 3, 4, 5]
It can be used to transform a collection of objects into a collection of values from a specific field of those objects, like this:
Set<String> example2 = transactions.stream()
.map(Transaction::customerId)
.collect(Collectors.toSet());
// [customer2, customer1]
In this example, we mapped the collection of transactions to the customer IDs associated with each transaction and collected them into a set to eliminate duplicates. This resulted in a set of distinct customer IDs.
Numeric Streams
Alongside the Stream interface, the Java Stream API provides specialized stream interfaces for specific numeric types, such as IntStream, LongStream, and DoubleStream. These numeric streams offer methods tailored to their respective types, ensuring efficient processing. For instance, IntStream includes mapToInt, and LongStream has mapToLong, among others. You can learn more about the structure of the Stream API here:
Java Stream API: Exploring Methods for Creating Streams
Map To a Numeric
The Stream interface also provides methods to convert a stream of objects into one of these numeric streams.
IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
A common use case is summing the total values of all transactions. To do this, we create a stream of transactions and map each transaction to its total value. Since the total value is a primitive double, we need to convert the stream from objects to a primitive stream. This is done using mapToDouble, which allows us to sum up the values efficiently.
double sumExample3 = transactions.stream()
.mapToDouble(Transaction::totalAmount)
.sum();
// 2130.99
Similarly, we can use mapToInt and mapToLong to work with streams of int and long values, respectively.
The Flat Map Operation
The flatMap() method in the Java Stream API is a powerful tool that transforms each element of a stream into another stream and then flattens these streams into a single stream. It’s commonly used when you have a stream of collections or arrays and you want to process their elements individually rather than working with nested structures.
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
For example, if you have a stream of lists, flatMap() allows you to transform it into a stream of the individual elements within those lists, enabling further operations on each element directly. This operation is particularly useful for handling complex data structures, making it easier to work with them in a more streamlined and functional manner.
In the example below, we have a stream of lists. The flatMap operation takes a method reference, which is applied to each list, converting it into a stream of its elements. These individual streams are then flattened into a single stream, allowing for seamless processing of all elements together.
Stream<List<Integer>> example4 = Stream.of(List.of(1, 2), List.of(3, 4));
Stream<Integer> flattenedExample4 = example4
.flatMap(List::stream);
List<Integer> listExample4 = flattenedExample4.toList();
// [1, 2, 3, 4]
It is possible to do this with arrays as well:
List<String> example5 = Stream.of(new String[]{"a", "b"},
new String[]{"c", "d"})
.flatMap(Arrays::stream)
.toList();
// [a, b, c, d]
We can even try this wild example:
List<Integer> example5 = Stream.of(Stream.of(1, 2),
Stream.of(3, 4))
.flatMap(s -> s)
.toList();
// [1, 2, 3, 4]
In this example, I created a stream of streams and applied flatMap in the pipeline. Since we’re already dealing with a stream of streams, there’s no need for any additional transformation—each stream is simply passed through. This example is purely for demonstration purposes, and I believe it effectively illustrates how flatMap() works.
We can use a real-life example where we collect all products from all transactions in one list like so:
List<String> example7 = transactions.stream()
.map(Transaction::productIds)
.flatMap(List::stream)
.toList();
// [Laptop, Headset, Smartphone, Monitor, Keyboard, Mouse]
The Multi Map Operation
The mapMulti() method in the Java Stream API is a flexible alternative to flatMap(), introduced in Java 16. It allows you to map each element of a stream to multiple elements, but with greater control over how these elements are generated.
default <R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper) {...}
Unlike flatMap(), which requires you to return a stream for each input element, mapMulti() uses a BiConsumer that allows you to add multiple elements to the resulting stream directly. This approach is more efficient in scenarios where you need to produce multiple outputs from a single input, especially when the number of outputs varies.
mapMulti() is particularly useful for scenarios where you want to transform elements conditionally or generate complex outputs, making it a powerful tool for advanced stream processing
List<String> example8 = Stream.of("a", "b", "c", "d")
.<String>mapMulti((str, consumer) -> {
consumer.accept(str);
consumer.accept(str.toUpperCase());
})
.toList();
// [a, A, b, B, c, C, d, D]
In the example above, each letter is replaced with itself and its upper case version.
The Stream interface offers methods to flatten a stream to a specific numeric stream using the following methods:
IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);
Map a numeric stream to a stream of another type
The IntStream interface provides the following methods:
LongStream mapToLong(IntToLongFunction mapper);
DoubleStream mapToDouble(IntToDoubleFunction mapper);
Using those methods, we can easily change a type of stream:
IntStream example9 = IntStream.of(1, 2, 3);
LongStream longStreamExample9 = example9
.mapToLong(Long::valueOf);
IntStream example10 = IntStream.of(1, 2, 3);
DoubleStream doubleStreamExample10 = example10
.mapToDouble(Double::valueOf);
The LongStream interface provides the following methods:
IntStream mapToInt(LongToIntFunction mapper);
DoubleStream mapToDouble(LongToDoubleFunction mapper);
We can use it like so:
LongStream example11 = LongStream.of(1L, 2L, 3L);
IntStream longStreamExample11 = example11
.mapToInt(n -> (int) n);
LongStream example12 = LongStream.of(1L, 2L, 3L);
DoubleStream doubleStreamExample12 = example12
.mapToDouble(Double::valueOf);
The DoubleStream interface offers the following methods:
IntStream mapToInt(DoubleToIntFunction mapper);
LongStream mapToLong(DoubleToLongFunction mapper);
And we can use it like this:
DoubleStream example13 = DoubleStream.of(1.1, 2.2, 3.3);
IntStream longStreamExample13 = example13
.mapToInt(n -> (int) Math.round(n));
DoubleStream example14 = DoubleStream.of(1.1, 2.2, 3.3);
LongStream doubleStreamExample14 = example14
.mapToLong(Math::round);
The Boxed Operation
Boxing in Java refers to the automatic conversion of primitive types (such as int, char, double) into their corresponding wrapper class objects (Integer, Character, Double). When the compiler automatically does this process, it is known as auto-boxing. The reverse process, where a wrapper class object is converted back into a primitive type, is called unboxing.
Each numeric type stream offers a boxed operation that converts each element of the stream into its corresponding wrapper class object.
IntStream:
Stream<Integer> boxed();
LongStream:
Stream<Long> boxed();
DoubleStream:
Stream<Double> boxed();
Examples of using this operation:
IntStream example15 = IntStream.of(1, 2, 3);
Stream<Integer> boxedExample15 = example15.boxed();
LongStream example16 = LongStream.of(1L, 2L, 3L);
Stream<Long> boxedExample16 = example16.boxed();
DoubleStream example17 = DoubleStream.of(1.1, 2.2, 3.3);
Stream<Double> boxedExample17 = example17.boxed();
Essentially, you can convert an int to an Integer using the boxed method, and convert an Integer back to an int using mapToInt. The same approach applies to long and double with their respective methods (mapToLong and mapToDouble). This allows seamless transitions between primitive types and their wrapper classes in streams.
The Map to Object Operation
We can also convert any numeric stream into a stream of objects using the mapToObj method. Each numeric stream interface (IntStream, LongStream, DoubleStream) provides its own version of this method.
IntStream:
<U> Stream<U> mapToObj(IntFunction<? extends U> mapper);
LongStream:
<U> Stream<U> mapToObj(LongFunction<? extends U> mapper);
DoubleStream:
<U> Stream<U> mapToObj(DoubleFunction<? extends U> mapper);
We can use it like so:
IntStream example18 = IntStream.of(1, 2, 3);
Stream<Integer> integerStreamExample18 = example18
.mapToObj(Integer::valueOf);
LongStream example19 = LongStream.of(1L, 2L, 3L);
Stream<Long> longStreamExample19 = example19
.mapToObj(Long::valueOf);
DoubleStream example20 = DoubleStream.of(1.1, 2.2, 3.3);
Stream<Double> doubleStreamExample20 = example20
.mapToObj(Double::valueOf);
You might wonder if the boxed() method could serve this purpose, and you'd be correct. In fact, the boxed() method internally uses mapToObj to perform the conversion.
The complete code can be found here:
GitHub - polovyivan/java-streams-mapping-operations
Conclusion
If you’re a Java programmer or planning to become one, you’ll definitely encounter the mapping operation. Mastering and understanding how it works is essential. This tutorial is designed to help you do just that.
Thank you for reading! If you enjoyed this post, please like and follow it. If you have any questions or suggestions, feel free to leave a comment or connect with me on my LinkedIn account.
Java Stream API: Exploring Mapping Operations was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Ivan Polovyi
Ivan Polovyi | Sciencx (2024-08-23T11:34:13+00:00) Java Stream API: Exploring Mapping Operations. Retrieved from https://www.scien.cx/2024/08/23/java-stream-api-exploring-mapping-operations/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.