BlogTechnology and KnowHow

Imperative to Functional Programming in Java

Symphony logo
Symphony
April 26th, 2022

Many engineers may ask themselves, what are the benefits and why should they use functional programming in Java. From its beginnings, best practices in Java have encouraged object-oriented programming (which is an extension of procedural programming - type of imperative programming). Since version 8, a lot of things have changed and many aspects of functional programming are now available in Java.

The main purpose of this blog is to present the benefits of adopting a functional paradigm (wherever it is possible) to engineers who worked with the older versions of Java (6, 7 or earlier) and to the ones working with legacy code or using newer versions but without switching to functional paradigm. Also, this content can be useful to anyone else interested in the subject.

It should be emphasized that using the functional approach in Java does not replace some good OOP principles and design patterns, and it can be looked at as an enhancement and something complementary. Java is an object-oriented language in its core and that has not changed.

Letโ€™s start with an example. This is the imperative approach of writing a method:

public List<String> getAddresses(List<Person> persons) {

    List<String> addresses = new ArrayList<>();

    for (int i = 0; i < persons.size(); i++) {

        Person person = persons.get(i);

        if (person.hasValidData()) {

            String address = person.getAddress();

            addresses.add(address.trim());

        }

    }

    return addresses;

}

For the same use case the functional approach would be:

public List<String> getAddresses(List<Person> persons) {

    return persons.stream()

            .filter(person -> person.hasValidData())

            .map(person -> person.getAddress())

            .map(address -> address.trim())

            .collect(Collectors.toList());

}

By comparing these, we can say that the last method is more readable, shorter, and that irrelevant code is hidden away from us.

Before seeing more examples and discussing them in more depth, letโ€™s take a look at the definitions of some important concepts that will be used:

Imperative programming - Type of programming using an approach with the sequence of statements, changing the program state and describing how to do something.

Declarative programming - Type of programming which describes what the program should do, without describing how it should be done.

Functional programming - Type of declarative programming which uses functions to describe the logic of what the program has to achieve.

Function - mapping of a set of values into another set of values. Often, these sets have one or zero elements.

In the previous example, we have seen the expression: person -> person.getAddress()

This expression is called the Lambda expression. It is at the core of functional programming. It represents a function which maps one value - person into another value - person.getAddress().

Some lambda expression examples are:

() -> System.out.println("lambda");

() -> handleChanges();

e -> publishEvent(e);

(x, y) -> add(x, y);

As we can see, the number of arguments (the values passed to a function) and the number of return values can vary, and go from zero to many. In essence, the lambda is a function which takes some number of values, does something with them and returns some other values.

Lambda expressions can be passed as method parameters. This was shown in the previous example:

.filter(person -> person.hasValidData())

.map(person -> person.getAddress())

.map(address -> address.trim())

Here we can also see one of the most important aspects of functional programming - composition of functions. Function filter is taking a lambda expression as an argument, then returning a value which is passed as an argument to the next lambda expression in the map function.

In order to explore this approach, it would be good to start with two most frequent ways to practice functional programming and methods chaining in Java, and use predefined functions like filter and map.

The first way is to use the Stream API, introduced in Java 8. It is used to process collections of objects. It supports intermediate operations like filter and map which take lambda expression (function) as an argument and return Stream where T is the return value of the lambda function. It also supports terminal operations like collect or forEach, where collect can be used to make a List from the processed collection of objects in a stream, while forEach is used to iterate through every element of the stream and do something with them.

Letโ€™s take a look at the example of a method using Stream API and a method doing the same thing written using the imperative approach. The list of countries is obtained using:

var countries = getCountries(); // List<Country>

Imperative approach:

for (country: countries) {

    if (country.isInEurope()) {

        print(country.name());

    }

}



The function of this code is to iterate through all countries and call a custom print method in the same class, if a country is in Europe. Compared to the first example, the newer for-each loop is used (introduced in Java 7)ย  for (country: countries) which makes code more readable, compared to the classic for loop: for (int i = 0; i < countries.size(); i++).

Functional approach:

countries.stream()

    .filter(country -> country.isInEurope())

    .map(country -> country.name())

    .forEach(name -> print(name));

Method filter, as the name says itself, is filtering the countries collection (objects in a stream) and leaves only the ones in Europe. After that, the method map takes the country object and maps (changes) it to a String - country.name(). Finally, the forEach function iterates through all the names and prints them using the print method defined in the same class.

This code can be improved and become more concise and readable using the Method Reference:

countries.stream()

    .filter(Country::isInEurope)

    .map(Country::name)

    .forEach(this::print);

Method reference is using a :: operator to make a reference to a function. It can be used when lambda expression calls only an existing method, where an argument of lambda is an object containing this method without parameters (argument is country which is an instance of Country class which contains isInEurope method), or an argument of lambda is the only parameter of an existing function (argument is name and the same class contains print method which takes String as a parameter).

Replacing lambdas with the method reference whenever possible is a good practice which makes code more readable and compact.

Comparing the imperative and functional example, we can see that the code in the second one is more focused on what the program should do instead of how to do it. Other great benefits of this approach are increased readability and lower cyclomatic complexity.

That was an example of using a functional approach via Stream API, when a collection of objects is processed. But what if we have only one object, and want to do something with it - to transform it in some way? This leads us to the second way how the method chaining, lambdas and functional approach can be used in Java - by using Optionals.

Optional is a container object, which may or may not contain an object inside. If it does not contain it, it is an empty Optional. It was introduced in Java 8 and then enhanced in Java 9. By creating an Optional that wraps a single object we can utilize the advantages of functional approach. Letโ€™s take a look at an example, and start with the imperative approach.

Imperative approach:

List<String> largeCitiesWithMetro(Country country) {

    if (isNull(country)) {

        return Collections.emptyList();

    }


    List<City> cities = country.cities();

    if (isEmpty(cities)) {

        return Collections.emptyList();

    }


    List<City> filteredCities = new LinkedList<>();

    for (city: cities) {

        if (city.isLarge() && city.hasMetro()) {

            filteredCities.add(city);

        }

    }


    return filteredCities;

}

This method checks if the country is null, and if it is, returns an empty list. The same happens if there are no cities. If there are cities, the method returns large ones with the metro.

Functional approach:

List<String> largeCitiesWithMetro(Country country) {

    var cities = Optional.ofNullable(country)

            .map(Country::cities)

            .orElseGet(Collections::emptyList);


    return cities.stream()

            .filter(City::isLarge)

            .filter(City::hasMetro)

            .collect(Collectors.toList());

}

Comparing the two approaches, it is obvious that the second is simpler, more readable and without null and empty checks. This is because Optional.ofNullable can wrap null values, and in that case, the empty list will be assigned to the variable cities, by calling orElseGet which is called only if the previous function returns empty Optional. After that, if a stream is called on an empty list, the collect in this case will create an empty list which will be the return value of the method. This way the same functionality as with the imperative approach is maintained, with less code.

If it is certain that a country cannot be null, instead of Optional.ofNullable(country) we would call Optional.of(country) and be able to use map, filter and other predefined or custom methods made for functional composition.

Lambda expressions and method references are defined using functional interfaces. These are the interfaces that have only one abstract method.

Since Java 8, there are some predefined functional interfaces that make functional programming in Java much simpler. Among many, some important ones are:

  • Predicate - function of one argument (one input value) which returns boolean value (maps to the boolean). Example: Predicate p = country -> country.isInEurope();

One of its frequent usages is as the argument of a filter function. This was shown in the example above: filter(country -> country.isInEurope()) or in simpler form filter(Country::isInEurope).

  • Function - function of one argument which returns a result (one value). Example: Function f = country -> country.name();

In this case, it takes an object country and returns a String - country.name().

Frequent usage - as the argument of a map function: map(country -> country.name()) or map(Country::name).

Consumer -ย  function of one argument that returns no value. Example: Consumer c = name -> print(name);

Frequent usage - as the argument of a forEach function: forEach(name -> print(name)) or forEach(this::print).

There is another important aspect of functional programming that should be mentioned. It is considered a good practice to keep functions pure when chaining methods.

Pure function is a function whose return value depends only on its parameters (when it comes to composition - passed to a function from the previous function in the chain) and its execution has no side effects.

Letโ€™s take a look at the example of a function which contains impure (not pure) function:

Map<String, List<String>> getCountryToCities(List<City> cities) {

    Map<String, List<String>> countryToCities = new HashMap<>();


    cities.forEach(city ->

            city.getCountry().forEach(country -> {

                countryToCities.computeIfAbsent(

                    country, ignored -> new LinkedList<>());

                countryToCities.get(country).add(city);

            })

    );


    return countryToCities;

}

In the innermost forEach, the countryToCities map is used and it is not passed via parameter, but used from the โ€œoutsideโ€, which makes this function impure.

The functions provided in the examples above (excluding the last one) were pure since the input argument depended only on the return argument of the previous function in the composition, and there were no side effects. It is not always easy to keep this practice, especially when refactoring or modifying legacy code, or when handling a code where clean code and SOLID principles were not always applied.

This blog has briefly explored the benefits of switching from imperative to functional approach. In order to switch to functional programming in Java, engineers should practice this approach wherever and whenever possible, and explore and read further on the subject. In the beginning, it may feel different and harder than the imperative approach, but once someone gets used to it, and changes the idea about how the code should be written, all the benefits will present themselves, and there will be no turning back.

Since everything in Java is an Object and it is an object-oriented language at its core, introducing functional programming did not change this fact and made Java a truly functional language - it treats lambda expression as an Object. Keeping this in mind, the best combination of both worlds gives newer versions of Java true power - using a functional approach whenever possible in combination with the well known OOP practices and design patterns.

Further exploration and reading

About the author

Boris Radojicic is a Software Engineer working at our engineering hub in Belgrade.

Boris Radojicic gained most of his experience working on mobile (Android) and backend development, but he also worked on developing ATM software. The technologies he used most frequently are Android, Java, Node.js, and C. Projects that he has worked on were developed within the Agile software development framework, and in addition to software engineering, he held a position of a Solution Architect and a Team Lead.ย