Java Interview Questions (series) - Streams
Still using for
loops? Maybe it's time for something new!
Brief
Having spoken about Functional Interfaces and Lambda Expressions, it's only right we continue our series talking about Streams.
Another major new feature coming with Java 8 is the Stream
functionality.
Its enclosing package java.util.stream contains classes used for processing sequences of elements.
As you've probably noticed from the other posts, I'm not one to only talk theory.
Let's see them play and understand how they work. 🚀
Implementation
💈 The best analogy for streams is a pipe
, this is how it is actually referred to in some places. In this pipe you have elements
coming one by one.
🔵 Initializing streams
There are multiple ways to creating and initializing streams.
🔹 Empty stream
Stream<Integer> emptyStream = Stream.empty();
You can create an empty stream, but this doesn't really makes sense, except you want to create a Stream
some other way around and it fails, or a condition is false.
For example, you can take a look at the ofNullable
method on the Stream
class. The second part doesn't really matter, here you can see how an empty Stream
might be used.
🔹 Stream.builder()
Stream<Integer> streamUsingBuilder = Stream.<Integer>builder()
.add(1234)
.build();
The builder approach can be useful when you want to add different values to the stream on different stages of the application.
🔹 Stream.of()
Stream<Integer> stringStream = Stream.of(1, 2, 3, 4, 5);
You can also initialize the stream inline, using the static of
method.
🔹 Arrays.stream()
You can create a stream from an array, using the java.util.Arrays
class.
Integer[] intArray = new Integer[] {1,2,3,4,5};
Stream<Integer> intStream = Arrays.stream(intArray);
🔹 Collections.stream()
The Collection
interface received a new method to interact with the newly added Stream
class.
Because of this, any object implementing this interface can be transformed into a stream using the stream()
method.
List<Integer> intList = new ArrayList<>();
Stream<Integer> intStreamFromList = intList.stream();
Set<Integer> intSet = new HashSet<>();
Stream<Integer> intStreamFromSet = intSet.stream();
Once created, the instance will not modify its source, therefore allowing the creation of multiple streams from a single source.
🔵 Intermediate operations
We've seen how we can initialize a stream, let's now play around with the elements.
While they are in the pipe you can:
🔸 change their state
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Stream<String> stringStream = integerStream.map(val -> "Element: " + val); // Element: 1, Element: 2, Element: 3, Element: 4, Element: 5
Stream<Stream<Integer>> streamOfStreams = Stream.of(Stream.of(1), Stream.of(2,3), Stream.of(4,5,6));
Stream<String> integerStreams = streamsOfStreams.flatMap(val -> "Element: " + val); // Element: 1, Element: 2, Element: 3, Element: 4, Element: 5, Element: 6
🔸 filter out some of them
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
integerStream.filter(val -> val%2 == 0); // 2, 4
🔸 skip some elements
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
integerStream.skip(4) // 5
🔸 sort the elements
Stream<Integer> integerStream = Stream.of(3, 2, 1, 4, 5);
integerStream.sorted(); // 1, 2, 3, 4, 5
🔸 apply side effects
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
integerStream.peek(val -> System.out.println("Element: " + val)); // Element: 1, Element: 2, Element: 3, Element: 4, Element: 5
🔵 Terminal operations
At the end of the pipe you can:
🔸 collect the items into another dataset
Stream<Integer> integerStream = Stream.of(1, 2, 1, 2, 1);
integerStream.collect(Collectors.toSet()); // 1, 2
🔸 return a single result
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Integer sum = integerStream.reduce(0, (a, b) -> a+b); // 15
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Optional<Integer> result = integerStream.findFirst(); // Optional.of(1)
🔸 return void
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
integerStream.forEach(val -> System.out.println("Element: " + val)); // Element: 1, Element: 2, Element: 3, Element: 4, Element: 5
❗️ In a stream, the intermediate operations are "lazy".
You can apply as many as you want on a stream, but until you apply a terminal operation, the intermediate ones won't be called.
The reason for this is quite simple, you don't need to allocate and consume resources until you actually need the result.
🔵 Closing streams
Streams
implement the AutoCloseable
interface, that means that they implement the close()
method.
❗️ You should be careful when closing a stream, because once closed, it will throw an IllegalStateException
if you try to operate on it again.
Usually, you don't need to close the streams, as their sources are collections
or arrays
which do not require special resource management.
❗️ One exception might be when you operate on IO
resources, like streaming the lines of a File
. This use-case will require closing the stream. ❗️
✅ To do it in the cleanest way, you should open the stream in a try-with-resources
block which will automatically handle closing the stream after all operations are completed.
Stay tuned! 🚀