Java 8 made a mini revolution and brought useful features like Nullable, which simplified the everyday life of a developer. It brought also long-awaited powerful features like lambda expressions and streams. Java programmers can use with them a more declarative style of writing code that was only saw in functional programming languages or libraries. Those features were accompanied by syntax changes, which disturbed many developers and sadly continues to.

The goal of this article is to demystify the functional syntax used by the java streams and show you how helpful the streams can be for processing data.

Prerequisites

Before looking at the streams I should present you two prerequisites.

Lambda expression

Lambda expressions, also known as “closures” or “anonymous methods” are similar to the javascript arrow functions. One of the goals of the lambdas is the readability by eliminating the boilerplate code of the function interface.

A lambda expression simply consists of the arguments and the body of the expression separated by an arrow operator:
(arg1, arg2, …) -> { do something; return something; }

For example:
(a, b) -> { System.out.println (“arg1 ”+a); System.out.println (“arg2 ”+b); return a+b }

The arguments type is optional and will be inferred by the compiler. The parentheses are optional for a single parameter. Other is the body braces and the return keyword that are optional for a one-line body.
(String s) -> {return a.toUppercase();}; //can be simplified in s -> s.toUppercase();

You should use, if possible, a one-line constructions instead of a large block of code. Simply remember that lambdas should be an expression, not a narrative and express the functionality they provide. Let’s have a last and concrete example with the forEach() method of an ArrayList:
userList.forEach(user -> System.out.println(user.log()));

Method reference

  • reference to a static method: ContainingClass::staticMethodName
  • reference to an instance method of a particular object: containingObject::instanceMethodName
  • reference to an instance method of an arbitrary object of a particular type: ContainingType::methodName
  • reference to a constructor: ClassName::new

We can simplify the example of the lambda expressions above with the method reference syntax:
s -> s.toUppercase(); // is the same as String:: toUppercase userList.forEach(user -> user.log())); // is the same as userList.forEach(User::log);

Streams

The java streams find their origin in the monads, another concept of functional programming.
A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result. Their main features are:
-A stream is not a data structure but takes input from a Collection, Array or I/O channel.
-Streams don’t change the original data structure. They only provide the result by applying the pipelined methods.
-Each intermediate operation is lazily executed and returns a stream as a result. Thus, various intermediate operations can be pipelined. Terminal operations, usually a loop through the remaining elements or reducing them to a specific value, mark the end of the stream and return the result.

Source > Filter > Map> Transform > … > Filter > Collect

Source

The source of a stream can be either single elements, collections, arrays, or even files. The usual way to create a stream are:
1. use the stream() method of Arrays:
String[] stringArray = new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(stringArray);

2. use the stream() method of the Collection interface:
Collection<String> collection = Arrays.asList("a", "b", "c"); Stream<String> stream = collection.stream();

3. use a Stream Builder:
Stream.Builder<String> streamBuilder = Stream.builder(); streamBuilder.add("a").add("b").add("c"); Stream<String> stream = streamBuilder.build();

4. collect single elements with Stream.of():
Stream<String> stream = Stream.of("a", "b", "c");

Intermediate operation

A stream can take any number of intermediate operations following the creation of the stream. An intermediate operation is often a filter or mapping. I will present here the interesting ones:
-map: map() produces a new stream after applying a lambda function to each element of the original stream. The new stream could have a different type.
List<Employee> employees = Stream.of(empIds) .map(employeeRepository::findById) .collect(Collectors.toList());

-filter: filter() produces a new stream that contains elements of the original stream that pass a given test, specified by a Predicate.
List<Employee> employees = Stream.of(empIds) .map(employeeRepository::findById) .filter(e -> e != null) .filter(e -> e.getSalary() > 200000) .collect(Collectors.toList());

-distinct: distinct() returns the distinct elements in the stream, eliminating the duplicates.
List<Employee> employees = Stream.of(empIds) .map(employeeRepository::findById) .distinct() .collect(Collectors.toList());

-sorted: sorted () sorts the stream elements based on a comparator.
List<Employee> employees = empList.stream() .sorted((e1, e2) -> e1.getName().compareTo(e2.getName())) .collect(Collectors.toList());

-takeWhile/dropWhile: these methods take or drop elements from a stream while a given condition is true.
Stream.iterate(1, i -> i + 1) .takeWhile(n -> n <= 10) .map(x -> x * x) .forEach(System.out::println);

Termination

Every pipeline needs to end with a terminal operation; without this, the pipeline will not be executed. Note that you cannot apply another operation after a terminal operation.
-forEach: forEach() is the simplest and most common terminal operation. It loops over the stream elements, calling the supplied function on each element.
empList.stream().forEach(e -> e.salaryIncrement(10.0));

-collect: collect() is one of the common ways to get stuff out of the stream once all the processing is done. It performs mutable fold operations (join, to collection, summarize, partition by, group by, map, reduce , etc.) on data elements held in the stream. All those methods are mainly provided by the Collectors class.
String empNames = empList.stream() .map(Employee::getName) .collect(Collectors.joining(", ")) .toString(); Set<String> empNames = empList.stream() .map(Employee::getName) .collect(Collectors.toSet()); Map<Character, List<Employee>> groupByAlphabet = empList.stream() .collect(Collectors.groupingBy(e -> new Character(e.getName().charAt(0))));

-reduce: reduce() takes data elements in the stream and combines them into a single summary result by repeated application of a combining operation.
Double sumSal = empList.stream() .map(Employee::getSalary) .reduce(0.0, Double::sum);

-toArray: toArray() delivers the data elements as an array out of the stream.
Employee[] employees = empList.stream().toArray(Employee[]::new);

-min/max: min() and max() return the minimum and maximum element in the stream respectively, based on a comparator.
List<Employee> employees = Stream.of(empIds) .map(Employee::getSalary) .min();

-count: count() returns the number of elements in the stream.
Long empCount = empList.stream() .filter(e -> e.getSalary() > 200000) .count();

-findFirst/findAny: findFirst() returns an Optional for the first entry in the stream.
Employee employee = Stream.of(empIds) .map(employeeRepository::findById) .filter(e -> e != null) .filter(e -> e.getSalary() > 100000) .findFirst() .orElse(null);

-anyMatch/allMatch/noneMatch: These operations take a predicate as test check if the predicate is true for respectivelly any/all/none of the elements in the stream.
List intList = Arrays.asList(2, 4, 5, 6, 8); boolean allEven = intList.stream().allMatch(i -> i % 2 == 0); boolean oneEven = intList.stream().anyMatch(i -> i % 2 == 0); boolean noneMultipleOfThree = intList.stream().noneMatch(i -> i % 3 == 0);

Conclusion

The java streams are a good example of what a functional syntax can bring to your daily task and an open door to other concepts like reactive programming (Observable) or asynchronous programming (CompletableFuture).