Functional Programming With Java: An Introduction

 · 9 min
William Daigneault on Unsplash

Java is a general-purpose programming language with class-based object-orientation at its core.

But, with the release of version 8, a more functional programming style became viable.

This article tries to shed some light on the depth of functional programming possible.


What Is Functional Programming?

The functional programming (FP) paradigm evolved from the invention of Lambda calculus by Alonzo Church in 1936, “a formal system in mathematical logic for expressing computation based on function abstraction and application using variable binding and substitution” (Wikipedia).

“par·​a·​digm | \ ˈper-ə-ˌdīm
a philosophical and theoretical framework of a scientific school or discipline within which theories, laws, and generalizations and the experiments performed in support of them are formulated.”
Merriam-Webster

With the help of a declarative programming style, FP tries to bind our code in pure mathematical functions to build evaluable expressions, instead of statements.

Core Concepts of Functional Programming

A short overview of the pillars that functional programming is built upon, so we can later check if Java fits in, or not.

Pure functions

A pure function has two elemental properties:

  • The same input generates the same output.
  • No side-effects, e.g., affecting global state or changing argument values.

These two properties guarantee that pure functions can safely be used in any environment, even in a parallel/concurrent fashion.

Referential transparency

Due to the predictable result for non-changing input arguments, we could replace a once-run function with its return value. This is called referential transparency.

It’s a result of pure expressions and can be used as an optimization technique called memoization, or “function call caching”, to save unnecessary re-calculations of already evaluated expressions.

Immutability

Mutating data structures can create unexpected side effects, and we must check against them. With immutability, we can keep our data structures predictable and side-effect free, and easier to reason with.

Data structures shouldn’t be modifiable after being initialized. To change/mutate a data structure, we must create a new variable containing the changed value.

Just like pure functions, we can safely use any data structure without side effects, even in parallel/concurrent environments, without uncertain results or issues due to unsynchronized access, or changes to out-of-scope state.

Recursion

A self-calling function that acts like a loop, executing the same expression multiple times until an end condition is reached and returns a “non-self-function call”. This is important, or we easily end up with infinite recursions, until we get a java.lang.StackOverflowError.

First-class and higher-order

Functions are supposed to be a first-class citizens like any other value, so they need to be able to be assigned to variables, used as arguments by other functions, and be able to be returned by functions.

A higher-order function is defined by accepting one or more functions as arguments, or, returning a function as its result, or both. This is necessary for the next pillar.

Functional composition

The combination of pure functions creates a more complex expression. This way, the initial functions can be small, on-topic, and reusable, but the composed function will perform a more complete task in a data structure.

“In mathematics, function composition is an operation that takes two functions f and g and produces a function h such that h(x) = g(f(x))
Wikipedia


Functional Programming With Java

The addition of lambdas makes it possible to use a more functional-like programming style. Especially the Streams and Optionals make heavy use of it.

For more on Optionals, you can check out my article Better Null-handling with Java Optionals.

But do the new features hold up against the actual paradigm of FP? Or is it just syntactic sugar for anonymous classes et al.?

Syntactic sugar

The general syntax of a lambda function is: (<argument-list>) -> <body>. From Java’s point of view, it’s represented by a functional interface, which could also be represented by an anonymous class.

java
// LAMBDA EXPRESSION
Predicate<String> lambda = (input) -> input != null;

// ANONYMOUS CLASS
Predicate<String> anonymous = new Predicate<String>() {

    @Override
    public boolean test(String t) {
        return t != null
    }
};

Both Predicates are doing the same thing, so it looks like really nice syntactic sugar. But the interesting question is what the compiler and the JVM do behind the scenes. Wrapped in a simple main method, this is the result:

// ANONYMOUS CLASS
0: new           #2 // class Anonymous$1
3: dup
4: invokespecial #3 // Method Anonymous$1."<init>":()V
7: astore_1
8: return

// LAMBDA
0: invokedynamic #2,  0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
5: astore_1
6: return

The anonymous version is creating a new object of the anonymous class Anonymous$1, resulting in three opcodes. We ignore 7: astore_1 because the lambda also needs to save the reference to the local variable.

The lambda version only needs a single opcode, the newly introduced opcode invokedynamic.

Of course, the comparison of just opcode count isn’t fair, because a lot is going on behind the invokedynamic call. We don’t see in the Bytecode how the JVM is actually handling the lambda.

The opcode invokedynamic was actually created for Java 7 to enable more dynamic method invocation by the JVM, improving dynamic languages like Groovy or JRuby.

Instead of linking dynamic methods, like lambdas, at compile-time, the JVM links a dynamic call site with the actual target method just before the first execution.

A bootstrap method is used once on first call to link it and return a method handle. It’s a little bit like when we use reflection in our code, but safer, and directly by the JVM.

There are different strategies for lambda creation (dynamic proxies, anonymous inner classes, MethodHandle), the decision is made at runtime by the JVM, not statically at compile-time. And the bootstrap method is only called once.

As we can see, it’s not just syntactic sugar. Lambda expressions enable the JVM to optimize our functional-style code in new ways, even allowing the reuse of lambdas at the JVM’s discretion.


Functional Core Concepts and Java

So much for the technical difference of using lambdas vs. anonymous classes, let’s see how the FP paradigm holds up.

Pure functions

Check. Same input results in the same output. No side-effects.

java
double circumference(double radius) {
    return 2.0 * Math.PI * radius;
}

Referential transparency

In theory, referential transparency is possible. We got pure functions, and we could cache function calls.

But there are no language constructs or helpers for doing so. The JVM might optimize calls, inlining results, etc., but it’s not as easy as in other languages like Clojure.

Immutability

There are a lot of immutable and de-facto immutable classes available in JDK:

  • String (only de-facto, hashCode is lazily computed).
  • Value type wrapper, e.g., Integer, Boolean, Long.
  • java.time classes.
  • Immutable collections with Java 9.

We can make our own data structures immutable, either manually, or by using a library like Immutables. But it’s still an implementation detail, not a language feature like in other programming languages.

Recursion

Check. Although the Java compiler doesn’t support tail call optimization.

java
long factorial(long n) {

    // Additional checks omitted...

    // End condition
    if (n == 1) {
        return 1;
    }

    // Recursive call
    return n * factorial(n-1);
}

First-class and higher-order

Check.

But Java was always a verbose language, so it’s not as elegant as in other languages.

java
// FIRST-CLASS
Supplier<String> lambda = myObject::toString;

// HIGHER-ORDER
Supplier<String> higherOrder(Supplier<String> fn) {
    String result = fn.get();
    return () -> result;
}

Functional composition

Check.

The interface java.util.function.Function provides methods for functional composition:

java
Function<Integer, Integer> square = (input) -> input * input;
Function<Integer, Integer> multiplyByTen = (input) -> input * 10;

// COMPOSE: argument will be run first
Function<Integer, Integer> multiplyByTenAndSquare = square.compose(multiplyByTen);

// ANDTHEN: argument will run last
Function<Integer, Integer> squareAndMultiplyByTen = square.andThen(multiplyByTen);

Is Java a Functional Language?

Not really. But it was never its intent.

Even though Java ticks a lot of boxes of the functional programming paradigm on the surface, it’s not a functional language at its core.

It still is a general-purpose programming language with class-based object-orientation, but now with support for functional constructs. Nevertheless, lambdas and a more functional-like programming style are worthwhile additions to our tool belts.

Object-oriented vs. functional. It’s not either-or.

Instead of being able to rely on language features, most of the functional concepts must be adhered to by convention and developer discipline, and are not enforced by the compiler.

We should still adopt the core concepts and constraints provided by the functional paradigm to our codebase, where appropriate.

Immutability
Always an excellent idea, not just for functional programming. Thanks to third-party libraries, we can use immutable data structures without much additional code.

No illegal state
Design your state to be always legal. Avoid nulls, exceptions, and locking/synchronization.

Generics
Dealing with the correct type, and not relying on Object or instanceof, helps to identify incorrect usage at compile-time.

No magic
Any unpredictable behavior or side effects are contrary to the functional paradigm. Using libraries that utilize runtime tomfooleries with reflection and/or proxying should be avoided, if possible.

The bottom line, we should know all the concepts and how to use them. But also know when it’s appropriate to apply them to our code.

Hopefully, Java keeps evolving into a more functional territory, providing even more advantages with reduced verbosity and more concise code.

The next part will highlight the different types of available functional interfaces and how to use them.


Comparison With Other JVM Functional Languages

Just to give you a better overview, here are some other languages running on the JVM with included functional principles.

Scala

Scala, like Java, is a general-purpose language, and not a specialized functional programming language.

But in contrast to Java, it was designed with functional programming in mind from the start, with language-level features like immutability. And with great Java interop in both directions.

Clojure

Clojure is an LISP-dialect on the JVM, a real functional programming language.

Where Scala is quite easy to grasp as a Java developer, Clojure is a different kind of beast. Code is data (homoiconicity), everything is immutable, data structures are persistent, null-handling is done right, built-in memoization, and lots and lots of parentheses.

One downside, at least in my opinion, is the Java interop if you want to use Clojure from Java. The other way around is excellent.

Kotlin

Kotlin is another general-purpose language, with functional programming ingrained in it. Good Java interop, first-class language for Android. Definitely worth a try.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources