Introduction
A Stream is a sequence of objects that supports many different methods that can be combined to produce the desired result.
They can be created from numerous data sources, which are most often collections but can also be I/O channels, Arrays
, primitive data types etc.
It's important to emphasize that a stream is not a data structure, since it doesn't store any data. It's just a data source wrapper which allows us to operate with data faster, easier and with cleaner code.
A stream also never modifies the original data structure, it just returns the result of the pipelined methods.
Types of Streams
Streams can be sequential (created with stream()
), or parallel (created with parallelStream()
). Parallel streams can operate on multiple threads, while sequential streams can't.
Operations on streams can be either intermediate or terminal:
Intermediate operations on streams return a stream. This is why we can chain multiple intermediate operations without using semicolons. Intermediate operations include the following methods:
map(op)
- Returns a new stream in which the providedop
function is applied to each of the elements in the original streamfilter(cond)
- Returns a new stream which only contains the elements from the original stream that satisfy the conditioncond
, specified by a predicatesorted()
- Returns the original stream, but with the elements being sorted
Terminal operations are either void or return a non-stream result. They're called terminal because we can't chain any more stream operations once we've used a terminal one, without creating a new stream from it and starting again.
Some of the terminal operations are:
collect()
- Returns the result of the intermediate operations performed on the original streamforEach()
- A void method used to iterate through the streamreduce()
- Returns a single result produced from an entire sequence of elements in the original stream
In this tutorial, we'll go over the map()
operation and how we can use it with Streams to convert/map objects of various types.
Stream.map() Examples
Let's take a look at a couple of examples and see what we can do with the map()
operation.
Stream of Integers to Stream of Strings
Arrays.asList(1, 2, 3, 4).stream()
.map(n -> "Number " + String.valueOf(n))
.forEach(n -> System.out.println(n + " as a " + n.getClass().getName()));
Here, we've made a list of integers and called stream()
on the list to create a new stream of data. Then, we've mapped each number n
in the list, via the map()
method, to a String. The Strings simply consist of "Number"
and the String.valueOf(n)
.
So for each number in our original list, we'll now have a "Number n"
String corresponding to it.
Since map()
returns a Stream
again, we've used the forEach()
method to print each element in it.
Running this code results in:
Number 1 as a java.lang.String
Number 2 as a java.lang.String
Number 3 as a java.lang.String
Number 4 as a java.lang.String
Note: This hasn't altered the original list in the slightest. We've simply processed the data and printed the results. If we wanted to persist this change, we'd collect()
the data back into a Collection
object such as a List
, Map
, Set
, etc:
List<Integer> list = Arrays.asList(1, 2, 3, 4);
List<String> mappedList = list.stream()
.map(n -> "Number " + String.valueOf(n))
.collect(Collectors.toList());
System.out.println(list);
System.out.println(mappedList);
This results in:
[1, 2, 3, 4]
[Number 1, Number 2, Number 3, Number 4]
Stream of Strings into Stream of Integers
Now, let's do it the other way around - convert a stream of strings into a stream of integers:
Arrays.asList("1", "2", "3", "4").stream()
.map(n -> Integer.parseInt(n))
.forEach(n -> System.out.println(n));
As expected, this code will produce the following output:
1
2
3
4
List of Objects into List of Other Objects
Let's say we have a class Point
that represents one point in a cartesian coordinate system:
public class Point {
int X;
int Y;
Point(int x, int y){
this.X=x;
this.Y=y;
}
@Override
public String toString() {
return "(" + this.X + ", " + this.Y + ")";
}
}
Then, say we want to scale a certain shape by a factor of 2
, this means we have to take all the points we have, and double both their X
and Y
values. This can be done by mapping the original values to their scaled counterparts.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Then, since we'd like to use the new coordinates, we'll collect this data into a new list of scaled points:
List<Point> originalPoints = Arrays.asList(new Point(1, 2),
new Point(3, 4),
new Point(5, 6),
new Point(7, 8));
System.out.println("Original vertices: " + originalPoints);
List<Point> scaledPoints = originalPoints
.stream()
.map(n -> new Point(n.X * 2, n.Y * 2))
.collect(Collectors.toList());
System.out.println("Scaled vertices: " + scaledPoints);
This example will produce the following output:
Original vertices: [(1, 2), (3, 4), (5, 6), (7, 8)]
Scaled vertices: [(2, 4), (6, 8), (10, 12), (14, 16)]
Conclusion
In this article, we explained what streams are in Java. We mentioned some of the basic methods used on streams, and focused specifically on the map()
method and how it can be used for stream manipulation.
It is important to mention that streams are not really a mandatory part of programming, however they are more expressive and can significantly improve the readability of your code, which is why they became a common programming practice.