The objective of this post is to introduce you to Streams in Java 8 by answering some very basic questions.
What is a stream?
Let us take a slightly abstract view first. Imagine a pipe, a pipe with 2 open ends. One end, the input end, is connected to a source, and the other end is connected to the destination. Let us assume that the source here contains data.The data from the source will then flow through the pipe and possibly undergo transformation. The desired data then flows out from the other end.
Data from the source does not start flowing through the pipe the moment the input end of the pipe is connected to the data source. It only starts flowing and reaches the destination when a knob is turned on.
A stream is nothing but a pipe. The pipe here is an abstraction and so is the stream. A stream enables the flow of data from the source to the destination and it could undergo transformation on its way. A stream does not hold any data.
How do we create a stream/pipe?
The stream API is part of the java.util.stream package. It can be obtained in many ways, some of the common ways include the following:
- From a collection : The stream method is a default method that has been added to the Collection interface.A stream can be attached to any class that implements the Collection interface.The collection of persons here acts as a source of the data.
List<Person> persons = // initialize list of people Stream<Person> persons = persons.stream()
- From static factory methods like Stream.of() :
Stream<Integer> numbers = Stream.of(1,2,3,4,5);
- Files.lines() which returns each line of a file as a stream:
Stream<String> lines = Files.lines(Paths.get("C:\\TestFile.txt"));
What do we do with a Stream?
We saw a few ways above to get a reference to a stream/pipe. But what do we do with a stream? To get an answer to that, let’s think about what do we do with collections in java? We iterate through collection and pick up the relevant data that we need. The relevant data is obtained by applying some conditions, this is nothing but filtering. So in short, as we iterate, we filter it and then collect the data. Pay attention to these 3 words in italics. Iterate, Filter, Collect. These are some of the common operations that we would usually perform on a stream.
- Iterate through a collection/data source:
This is actually done implicitly for us using the stream method we discussed above. Remember that there needs to be a trigger for the data to flow through the pipe. We will discuss this soon but we did refer to this as the knob that makes the data flow through the pipe. Once the knob is turned on, the stream method will push the data down the pipe.
List<Person> persons = // list of people
Once we have a stream/pipe and data begins to flow through it, we usually filter the data that moves through the pipe. This is done by using some conditional logic as we move through the data pipe. In Java 8, we use the filter method on the stream API to do this. The filter method looks like this:
The signature of the filter method:
The return type is a stream. The parameter to the filter method is a Predicate which is a functional interface.The data element will be evaluated using this filter, if it passes the criteria, it moves ahead in the pipe else it gets dropped out.
.filter(person -> person.getCitizenship() == Citizenship.USA)
Here the person-> person.getCitizenship == Citizenship.USA is a lambda expression that is mapped to the Predicate above.
So we created a stream and wrote a filter. Assuming that the data now flows through the pipe and passes through the filter and reaches the end of the pipe, what do we do now? We collect the data that we want. This is done using the collect method. The collect () API does exactly what it means, it collects the data. We can specify the final data structure into which the data needs to be collected.
Remember the knob we spoke about earlier. Unless the knob is turned on, nothing passes through the pipe and hence nothing gets filtered. The collect method is one such example which acts as the knob, it is called the terminal operation. Unless we call a terminal operation nothing happens! This makes the streams extremely lazy.
To summarize, using steps 1, 2 and 3 above, we created a stream, pushed the data down the stream, filtered it and then collected it.
|.filter(p –> p.getCitizenship() == Citizenship.USA)|
The Collectors is a utility class which helps in accumulating elements into a collection as shown below.
Other simple examples to understand stream, filter, collect:
Get a list of all men from a collection of Person objects
|.filter(p –> p.getGender()==Gender.MALE)|
Get a list of all women who are Canadian citizens:
|.filter(p –> (p.getGender() ==Gender.FEMALE) && (p.getCitizenship() == Citizenship.CANADA))|
This topic deserves more attention and will be covered in another article but a short answer to the same would be that they bring functional style of programming to Java. There are many more operations like map, flatMap, sort etc which can be chained together to transform and sort objects. Streams do not create any temporary data structures, it is all 1 pass. So the filter operation does not create any temporary data structure.Streams also open up the gates for parallel processing by exploiting the underlying hardware. This can be done by calling the parallelStream() instead of the stream method above.
There are many other operations than be called on the stream and the same will be covered in other posts.