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.

0:00
/

🔵   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.

public static<T> Stream<T> ofNullable(T t) {
        return t == null ? Stream.empty()
                         : StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}
Stream.java

🔹  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.

0:00
/


🔵  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.


💡
Don't miss out on more posts like this! Subscribe to our free newsletter!
💡
Currently I am working on a Java Interview e-book designed to successfully get you through any Java technical interview you may take.
Stay tuned! 🚀