Understanding Java Streams - Part One

A stream is a collection, that allows data processing declaratively. It is a (possibly never ending) series of Objects.

Understanding Java Streams - Part One
Java Streams

Hi there, in this part one we will have quick look at the Java Streams, in part two we will go through the Streams API with more examples. Before you move on, you must be aware of lambda expressions.

Streams API:

The Streams API was added in 2014 with the release of Java 8. It is used to pass a series of objects through certain chain of operations, so we can program in more functional style than plain iteration allows.

What's a Stream?

So, a stream is a collection, that allows data processing declaratively. It is a (possibly never ending) series of Objects. It is available in java.util.stream - which contains classes for processing sequence of elements and supports different kind of operations to perform computation upon those elements. The central API class is Stream<T>.

A stream starts from source. The objects in Stream flow through Intermediate Operations, each of which results in another stream, and are gathered up at the end in a Terminal Operation. The Stream operations are either intermediate or terminal. Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons. Terminal operations are either void or return a non-stream result.

                   

Streams are lazy, which means the source and intermediate operations do nothing until objects are needed by the terminal operation.

Stream creation:

Streams can be created from different element sources e.g. collection or array with the help of stream() and of() methods.

String[] array = new String[]{"Java", "Python", "Go"};
Stream<String> stringStream = Arrays.stream(array);
stringStream.forEach(System.out::println);

Stream<String> stream = Stream.of("React", "Vue", "Angular");
stream.forEach(System.out::println);

stream() default method is added to the Collection interface and allows creating a Stream<T> using any collection as an element source:

Stream<String> stream = someList.stream();

Multi-threading With Streams:

Stream API also simplifies multithreading by providing the parallelStream() method that runs operations over stream's elements in parallel mode

The code below allows to run method executeWork() (some custom method) in parallel for every element of the stream.

someList.parallelStream().forEach(element -> executeWork(element));

Let's discuss some of the basic Stream API Operations:

Stream Operations:

There are many useful operations that we can perform on stream. As I said earlier, the stream operations are either intermediate (return Stream<T>) or terminal operations (return a result of definite type). Intermediate operations allow chaining. It is important to note that operations on stream don't change the source.
See the below example:

long count = someList.stream().distinct().count();

The distinct() method represents an intermediate operation which creates a new stream of unique elements of the previous stream. The count() method is terminal operation wich returns stream's size.

Iterating:

Stream API helps us to iterate over the collection and helps us to concentrate on logic part, instead of thinking of iteration over sequence of elements. For exmaple:

for (String item: someList) {
	if (item.contains("x")) {
		return true;
	}
}

The above code can be simplified with just one line of code:

boolean isExist = someList.stream().anyMatch(item -> item.contains("x"));

Filtering:

We can use filter() method to pick a stream of elements that satisfy a predicate. For example:

ArrayList<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("Go");
languages.add("Python");
languages.add("JavaScript");
Stream<String> languageStream = languages.stream()
		.filter(item -> item.contains("a"));
languageStream.forEach(System.out::println); // You will get Java and JavaScript as output

Mapping:

We can use the map() method to convert elements of a Stream by applying a special function to them and to collect these new elements into a Stream. For example:

If you have a stream where every element contains its own sequence of elements and you want to create a stream of these inner elements, you should use the flatMap() method:

Matching:

Stream API has given methods to validate elements of a sequence according to some predicate. To do that, we can use one of the following methods:
anyMatch(), allMatch(), noneMatch(). These are terminal operations that return boolean value.

boolean anyMatchBool = someList.stream().anyMatch(element -> element.contains("x"));
boolean allMatchBool = someList.stream().allMatch(element -> element.contains("y"));
boolean noneMatchBool = someList.stream().noneMatch(element -> element.contains("z"));

For empty streams, the allMatch() method with any given predicate will return true:

Stream.empty().allMatch(Objects::nonNull); // true

Similarly, the anyMatch() method always returns false for empty streams:

Stream.empty().anyMatch(Objects::nonNull); // false

Reduction:

Stream API allows reducing a sequence of elements to some value according to a specified function with the help of the reduce() method of the type Stream. This method takes two parameters: first – start value, second – an accumulator function.

Imagine that you have a List<Integer> and you want to have a sum of all these elements and some initial Integer (in this example 29). So, you can run the following code and result will be 32 (29 + 1 + 1 + 1).

List<Integer> integers = Arrays.asList(1, 1, 1);
Integer reduced = integers.stream().reduce(29, (a, b) -> a + b);

Collecting:

The reduction can also be provided by the collect() method of type Stream. This operation is handy in case of converting a stream to a Collection or a Map and representing a stream in the form of a single string. There is a utility class Collectors which provide a solution for almost all typical collecting operations. For some, not trivial tasks, a custom Collector can be created.

List<String> resultList = languages
		.stream()
		.map(String::toUpperCase).collect(Collectors.toList());
resultList.forEach(System.out::println);

This code uses the terminal collect() operation to reduce a Stream<String> to the List<String>.