Java Chapter 3 : How to Shift to Functional Programming in Java

Sohee Kim
The Startup
Published in
4 min readFeb 23, 2021

In my previous post, I discussed what Functional Programming is and the benefits of adopting Functional Programming in Object-Oriented languages. Now, let’s get our hands dirty and apply Functional Programming in Java.

Photo by Michael Dziedzic on Unsplash

Before I begin, I want to clarify what I mean by adopting Functional Programming. Some people can say that they’ve adopted Functional Programming by simply implementing first-class functions or pure functions. However, I think it’s only half correct. Adopting Functional Programming is not only using the techniques of Functional Programming but understanding the conceptual difference between Object-Oriented Programming and start programming in a functional way. To understand what it is to program in a functional way, we have to know what declarative and imperative programming is.

  • There are two ways to program: declarative and imperative programming
  • Functional Programming is a type of declarative programming
  • Object-Oriented Programming is a type of imperative programming

Both are simply different approaches to solve the same problem.

Imperative vs Declarative Programming

Imperative programming is programming how to do something. It’s like a sequence of orders to modify the state. Think of how a for-loop works. You initialize a state and perform some kind of operation to change the state for each iteration. I’ll give you an example of imperative programming and compare it with a declarative programming version.

Assume say we have a list of names. We want a new list that only contains names longer than 5 characters after capitalizing them. Probably the most intuitive way is to use a for-loop.

Let’s interpret what we did in the code above. We had to manually implement a way to iterate the list (using for loop), check each name’s length, capitalize and add it to the new list.

In other words, we iterated each element and

  • filtered the elements longer than 5 characters
  • converted the elements to uppercase, and
  • added the elements to a list.

What if there’s a way that automates the process using predefined functions? That’s how declarative programming works. Declarative programming is programming what to do instead of how. Processes such as filtering, converting, and adding can be represented as functions.

Java provides Stream API as a functional approach to process collections. A stream can iterate itself, so we don’t need to implement how to iterate the list. It works like a pipeline that can take in a number of operations.

The example above can be programmed declaratively using Stream API.

Instead of implementing how to filter, convert, and add the elements in imperative programming, we can now use methods provided by the Stream API such as filter(), map(), and collect() and customize them by passing parameters. There are many more methods provided by the Stream API so I recommend looking them up.

I’ll quickly go over filter(), map(), and collect().

Filter()

It takes a predicate as a parameter and returns a stream that matches the predicate. To recap, a predicate is a predefined functional interface that takes in one parameter and returns a boolean.

Here are some examples using filter():

Map()

It takes a Function as a parameter and returns a stream that applied the given function to each element. Here, Function is referring to a predefined functional interface that takes in one parameter and returns a value. Note that filter() and map() are examples of higher-order functions.

Here are some examples using map():

Collect()

It takes Collector as a parameter and puts the iterated elements in the Collector. We can use Collectors which are Collector implementation provided by Java.

Here are some examples using collect:

If you noticed that what filter() and map() do is different from what collect() does, good catch! In fact, there are two types of Stream operations: terminal and non-terminal operation. Filter() and map() are examples of non-terminal operation and collect() is an example of terminal operation.

  • A Terminal operation returns a value.
  • A Non-Terminal operation, also called an intermediate operation, performs certain operations and returns a new stream. Many non-terminal operations take functional interface as their parameter and this is why we can use lambda expressions as used in the examples above.

Conclusion

Functional Programming isn’t simply using lambda expressions or Stream API to implement first-class functions or pure functions, but programming declaratively. According to Functional Thinking by Neal Ford, transitioning from imperative to declarative programming is “learning where to apply the higher-level abstractions and stop going immediately for detailed implementations.” It’s really about where and how to abstract your code to make your code more concise and precise. Indeed, Functional Programming isn’t always the answer to write good code. It is more important to understand the differences and be flexible to use both ways appropriately.

This post was highly inspired by Functional Thinking written by Neal Ford.
I highly recommend reading it.

--

--