Java is mainly an object-oriented programming language. Since its introduction in the 1990s, this paradigm has been used and is still the most popular flavor of coding Java applications.
Programming paradigms, like trends, evolve. Functional programming adoption with languages like Scala and Clojure happens in different industries, with the Nubank team’s use of Clojure being a tangible example.
Incorporating these functional programming concepts into our codebases promotes significant benefits. These include enhanced readability and easier reasoning about code, more testable components, and better separation of concerns.
At e-Core, we serve diverse customers across various projects, many of which utilize Java in different contexts. We strongly encourage exploring and implementing the concepts presented in this article within these varied scenarios. Doing so can substantially improve the quality of our project codebases, ultimately delivering superior results to our customers.
This approach not only elevates our work’s technical excellence but also aligns with our commitment to providing innovative and efficient solutions tailored to each client’s unique needs. It also increases our team’s Java knowledge and functional programming toolset, which can be used in different languages as well.
Let’s examine how Java has implemented some of the spices from functional programming and how we can prepare our own recipes using them. We’ll also briefly cover the fundamental concepts of functional programming from a high-level view.
What are the functional programming concepts?
Functional programming stands in 3 major pillars:
- Purity of functions and elimination of side-effects.
- Immutability
- Declarative programming style
Let’s quickly go through each concept.
- Purity of functions and elimination of side effects
We have all seen code and functions that depend on state or have unwanted side effects. Let’s take this simple example:
If we pass the same parameters (e.g., 1 and 2), we should get the same result every time. However, since the counter variable gets incremented every time, the result changes even when passing the same parameters. This consistency is essential in functional programming, where we aim to eliminate impurities (presence of side effects or reliance on external state).
Functions should produce the same output for the same input (deterministic), avoiding side effects like variable mutations. This means that functions should not mutate state, making it easier for us to reason with.
- Immutability
Immutability can be illustrated with Java’s String implementation. When concatenating a value, the original string remains unchanged; instead, a new string is created. This ensures data remains consistent and simplifies debugging.
As we can see, the contents of the variable name are the same even after we use the concat operation. The same would happen if we used the + operator. In the end, we get a new variable with the concatenated strings.
We should strive for immutable data, as it makes our programs easier to debug and reason with. Sure, there are some scenarios where mutable data is required, but it is a good exercise to think about data and flows in an immutable manner.
For a deeper understanding, explore how to use records in Java. They make concepts of immutable data and data modeling easier and cleaner. Additionally, these resources provide valuable insights:
- The declarative style of programming
Finally, we come to the topic of declarative style versus imperative style of programming. Well, what does this mean?
In simple terms, declarative programming focuses on what needs to be done, while imperative programming details how to achieve it with the language features.
For example, to filter users born in January and extract their last names, a declarative approach specifies the desired outcome, whereas an imperative approach defines each step on how to achieve it.
There isn’t a better or correct approach. What is clear is that we have two distinct ways to express what we want: in the imperative version, lastNameInJanOld, we code each step in how we want to achieve the requirement. For the declarative, lastNamesInJan, we are stating what we want to achieve. We are also using a private method to make things a bit cleaner, and passing each of the steps we want to happen as lambdas (Predicate and Function). These are core concepts, and we will dive into them in a bit.
How is Java leveraging functional programming concepts?
A big jump happened when Java 8 was introduced. It brought a slew of new features, with the most important ones for this topic being:
- Stream API
- Lambdas
- Method reference
- Optionals
We also need to mention Generics, which were introduced in Java 1.5 and also helped in building a more functional style of programming.
Let’s go through an example of how the Stream API made filtering a List cleaner while leveraging the concepts of functional programming:
In the first example, we are using the original imperative style of filtering a list of integer numbers. We initialize a new resulting list, and in a for loop, we check if each of the numbers is odd. If the current number is odd, we add it to the list, and in the end, we return this list.
Now, let’s check the example using the feature introduced in Java >= 8.
We can see that the operation can be expressed in a single line, or better yet, a single expression. Here, we’re also using a declarative style of programming.
Streams are usually commonly coupled with Lambdas. These are inline functions, or expressions where the value is an actual function. Let’s take a look at the previous scenario, using different implementations:
One thing becomes clear: we are replacing the implementation of Predicate<Integer> with a single function, be it anonymous or declared in a variable.
But why does this happen? If you take a look at the definition of the Predicate interface, you will notice that it is annotated with @FunctionalInterface. What this means is that the interface has exactly one abstract method, as in methods without an implementation (this excludes default methods in interfaces). In the anonymous implementation of Predicate for the example, we only have a single abstract method: test. This test method that we override in the first example, getOddNumbers.
This makes things a bit clearer: Lambdas are functional interfaces, and the lambda itself is the single abstract method of that interface, which we implement inline, anonymously. Java has several functional interfaces that we can leverage, feel free to check them here. You might be familiar with several of them.
Method references are tied to lambdas. It is an easy-to-read way to call an existing method in the context of a lambda. In the example getOddNumbersReference we are passing the method reference of isOdd in the place of the lambda. You can find more information about it here.
The final piece of the Java 8 features is the Optional container. Java is famously known for its lack of null safety, due to backward compatibility concerns. To mitigate it, the Optional class was introduced to represent an object that may or may not contain a non-null value. This is a better way to represent a scenario where a computation might return a value, instead of simply returning null. Let’s take the following scenario:
The old way to represent a possible return value was to return null when the result wasn’t able to be computed. This works but is error-prone and does not explicitly tell the developer if a method might not return a value.
When using Optional, we explicitly make that a method might or might not return a value. We also guarantee that if we want to use that value, we need to handle the Optional itself. To check which methods are available for Optional, please take a look at this resource.
It’s crucial to note that a reference to an Optional container can be null, which undermines the purpose of using Optional. For example, a method could be declared to return Optional<String> but actually return null. This scenario highlights a potential pitfall in Java’s type system, one that can only be mitigated through good development practices. For comprehensive guidelines on using the Optional class effectively, refer to this article.
In the future, many of these issues may be addressed by Project Valhalla, which aims to introduce value objects into Java. Another popular JVM language, Kotlin, was built with type-safe nullability in mind. For more information, visit the Project Valhalla page and the Kotlin documentation.
Functional Programming in Java: why one when we can use both?
In Java, writing strictly functional code is impractical and perhaps undesirable. Different problems and domains require tailored solutions. For instance, in large enterprise projects, expressing all business logic purely functionally can be challenging. Conversely, handling mutability and imperative-style code may become overly complex and time-consuming on a larger scale. For that reason, a more pragmatic approach is probably better suited.
A balanced approach is to define system boundaries – meaning the layers and packages that compose an application – using object-oriented programming (OOP). This means modeling domain objects, services, and external interfaces conventionally. Within these boundaries, however, functional concepts can be applied. For example, we would use concepts of immutable data and pure function to implement the business logic inside the boundaries. This hybrid approach allows us to leverage immutability and declarative programming while providing an API that integrates seamlessly with traditional OOP. If you are interested in diving deeper into applying and understanding this concept, I recommend watching this talk.
There are real-world examples of large companies employing this strategy. For example, Netflix’s Mantis is a Java library built to handle real-time streams of data. Mantis uses a syntax that borrows the same concepts used in Java Streams and Lambdas, as these features make it easier to write data processing pipelines. You can find more details in the documentation here
Another example of a large project that uses functional concepts is Kafka Streams. It is used to build stream processing applications within the Kafka ecosystem. The Java API for this library makes heavy use of the Stream API idiom, adopting the usages of lambda expressions when defining a topology and processing results. Companies like Netflix and Uber use this library to process a large amount of data in real-time. Here is a brief code snippet using Kafka Streams:
In conclusion, with the introduction of new features within the language, Java provides the toolset to write functional-like code. It is important to understand that Java is still predominantly an OOP language. But we can combine each approach when writing our own software. That can be done by using immutable data structures, making use of Streams and lambda expressions, or libraries that expose APIs with a functional flavor. In the end, it is better to know these tools and have them at your disposal!