Declarative programming in Java using Streams and Lambdas

Piyush Aggarwal
5 min readApr 7, 2021

--

Declarative Programming → Making the life of a developer easier

In this article, we will see how streams and lambdas help in code understandability and make the programs more readable as compared to the imperative style of coding.

Streams are meant to be flown

This is in continuation of my previous article on functional interfaces, https://piyush5807.medium.com/functional-interfaces-in-a-nutshell-for-java-developers-54268e25324

Streams are a collection of objects on which various methods can be applied while they are in the pipeline of execution. In simple layman terms, we can convert a collection of items into a stream (does not change the structure of objects) and then apply various types of methods to the items in the stream

Lambdas are nothing but just the programming constructs that remove the dependency of writing anonymous functions

First, let’s see what is an imperative style of coding by taking an example

Question: Given a list of integers, find the squares of all the even numbers in the list and return them in a new list.

Let’s try to solve in an imperative way first:

public static List<Integer> squareEvens(List<Integer> list){
List<Integer> squareList = new ArrayList<>();
for(Integer element : list){
if(element % 2 == 0){
squareList.add(element * element);
}
}

return squareList;
}

Although it’s a fairly simple function still the amount of work that we are doing in this approach can be reduced and the function becomes more readable using a declarative approach

Now let’s try to solve this using a declarative approach (streams) but without lambdas

public static List<Integer> squareEvens(List<Integer> list){
return list.stream().filter(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
return integer % 2 == 0;
}
}).map(new Function<Integer, Integer>() {

@Override
public Integer apply(Integer integer) {
return integer * integer;
}
}).collect(Collectors.toList());
}

Now, this code might look more confusing, but actually, it is clearly stating what operations each part of the stream function is doing.

Filter filtering out the even numbers, map function maps the numbers to their powers and then the collect function collects into the list

But having said that, we can still improve our code quality by using lamdas in places of functional interfaces wherever we are using functional interfaces.

Below is the code for the same problem using lambdas

public static List<Integer> squareEvens(List<Integer> list){
return list.stream().filter(x -> x % 2 == 0).map(y -> y * y).collect(Collectors.toList());
}

Simple, readable, and done in one line only 😃. That’s the beauty of lambdas. We can carry out almost any operation with the lambdas.

In the previous article we have looked at how to use functional interfaces, now let’s see how to use those functional interfaces using lambdas

Stream functions

  1. Filter → Used to filter out elements from the stream on the basis of some conditions. Only the elements which follow the condition will be passed to the subsequent function in the pipeline of the stream. It’s a non-terminal function(i.e, this function will pass some stream of elements(≥0) forward to some next function of the streams API). The filter function of stream API accepts a Predicate in the argument
List<Integer> numbers = Arrays.asList(2, 1, 4, 1, 3, 5, 12, 32, 76);
numbers.stream()
.filter(x -> x % 2 == 0)
.forEach(System.out::println);

The above code filters the stream elements and only allow even numbers to pass to the next method which is forEach where the elements are printed

2. Map → Used to transform an element into some other element using some transformation function. It receives a stream of elements and for each element, it performs a transformation function to convert that element into something else. It’s also a non-terminal function. The map function of Streams API accepts a Function in the argument

List<String> cities = Arrays.asList("Delhi", "Tamil Nadu", "Bengaluru", "Mumbai");
cities.stream()
.map(x -> x.toLowerCase())
.forEach(System.out::println);

The above code takes a list of cities and prints the lower case of every city. Every city is converted to lowercase using a function. Here the transformation function is toLowerCase.

3. ForEach → This function runs for every element coming along the stream. It does not return anything or passes any element further. This is also known as terminal function (i.e, after this forEach function there will not be any stream left). ForEach function takes a Consumer in the argument.

List<Integer> numbers = Arrays.asList(2, 1, 4, 1, 3, 5, 12, 32, 76);
numbers.stream()
.filter(x -> x % 2 == 0)
.map(x -> x*x)
.forEach(System.out::println);

The above code prints the square of every number in the stream.

4. Collect → This function collects every element coming in the stream to another collection (it can be a list or a set or a map). This is also a terminal function. The collect function of the Streams API takes a Collector in the argument

List<String> cities = Arrays.asList("Delhi", "Tamil Nadu", "Bengaluru", "Mumbai");

Set<String> citiesWithEvenLength =
cities.stream()
.filter(x -> x.length() % 2 == 0)
.collect(Collectors.toSet());

System.out.println(citiesWithEvenLength);

The above code collects all the even length cities into a set.

These four are the most important functions in the Streams API. There are some other functions that we discuss in short in the following section

5. Distinct → This function takes a stream of elements and returns a stream discarding the repetitive elements in the stream (i.e, after this function, every element is unique in the subsequent stream). It’s a non-terminal function.

6. Sorted → This function takes a stream of elements and returns the stream of elements according to some comparator, if no arguments are provided it will return elements in ascending order, and if some comparator is provided in the arguments, then it sorts according to that. It’s an overloaded function and a non-terminal one

7. Limit → This function limits / restricts the length of the stream by discarding some elements from the stream depending on what’s the limit given in the argument. Again a non-terminal function

There are many more functions related to the stream which you can explore from the official Java documentation.

Important Note: There should be exactly one terminal operation in the stream, without a terminal operation the stream wouldn’t flow

Thank you for reading the article, feel free to comment below in case of any doubts or suggestions for the next articles :)

--

--

Piyush Aggarwal

Software Developer who likes to work in fast paced start up with sound knowledge of backend technologies and infrastructure.