Deep Dive into Java 8 Lambda Expressions
Started as ‘Project Lambda’ in 2010, it was officially released in Java 8. This article details how functional programming was incorporated into the existing Java language.
Brief Overview of Functional Programming
Before introducing Java’s lambda expressions, we need to briefly understand functional programming. (Functional programming based on lambda calculus is a paradigm, and lambda expressions represent it!)
Functional programming is a paradigm that creates output relying only on function input, avoiding changing external state, minimizing side-effects. Functional programming must satisfy the following conditions:
- Pure Function
A function without side-effects, meaning the function’s execution doesn’t change external state. Pure functions are safe in multi-threaded environments and enable parallel processing. Output is determined only by input, not affected by environment or state.
- Anonymous Function
Ability to define functions without names. Such anonymous functions are expressed as ’lambda expressions’ in most programming languages, with theoretical basis in lambda calculus.
- Higher-order Function
A higher-level function that handles functions. In functional languages, functions are treated as values, and functions can be passed as arguments to other functions. Such functions are considered first-class objects (a.k.a first-class functions).
So let’s briefly see how Java could support functional programming at the language level.
Java doesn’t have the concept of functions. (Java methods are not first-class functions, so they can’t be passed to other methods. In Java, everything is an object. Methods define object behavior and change object state.) For this reason, the existing Java language system couldn’t support functional languages at the language level. (It was possible before if implemented to satisfy functional programming conditions.)
Therefore, Java 8 introduced the concept of functional interfaces (interfaces with only one method declared), and functional interfaces could be expressed as lambda expressions.
Through the functional interface concept and lambda expression in Java 8, ‘pure functions’ could be expressed where output is determined only by input, ‘anonymous functions’ could be defined through lambda expressions, and ‘higher-order functions’ could be defined by allowing functional interface methods to accept other functional interfaces as arguments. In other words, it became possible to satisfy the conditions of functional programming languages.
| |
Looking at what functional interfaces are through examples, Functional1, 2, 3 all satisfy functional interfaces. Notably, Functional3 has @FunctionalInterface annotation, which explicitly tells the compiler it’s a functional interface and generates a compiler error if the interface violates functional interface specifications.
Deep Dive into Lambda Expressions
The basic lambda expression structure in Java is:
| |
Summarized as follows:
Simple lambda syntax may not have braces in the lambda body.
May not have return.
Parameters don’t need explicit type declaration (type inference).
Instead of writing the functional interface implementation yourself, you delegate it to the compiler (more precisely, the runtime). (It isn’t converted to an anonymous class; more on that below.)
| |
Lambda Expression Usage
We’ve looked at Java’s functional programming and lambda expressions in detail. Now let’s summarize the specific specifications of lambda. Think of this as summarizing what syntactic restrictions exist when using lambda expressions and how they can be utilized.
Parameterized Behaviors
By passing data or variables and behavior together to a method, the behavior part of the method can also be separated. The advantages gained can be summarized as:
- Perform control flow by receiving behavior at runtime (cf. Strategy Pattern)
- Method-level abstraction possible
- Higher-order functions in functional languages
| |
The Spring Framework already used behavior parameters using anonymous classes as the ‘Template Callback Pattern’ design pattern, and now it can be used more concisely with lambda expressions.
Immutable Free Variables
Java enabled closures through anonymous classes + free variable capture, forcing explicit use of the final modifier on captured variables. In lambda expressions, final doesn’t need to be explicitly declared on captured variables, but captured variables still can’t be modified, and attempting to modify results in a compile error.
| |
Stateless Object
Class methods (behavior) can freely control member variables (state). In other words, when an object calls a method, output is determined from input + state (properties), so side-effects can occur. Since exclusive function execution isn’t guaranteed, there’s potential exposure to various disadvantages in parallel processing and multi-threaded environments.
On the other hand, when expressed with lambda expressions, it becomes dependent only on input and output, so side-effects can be guaranteed not to occur as much as possible. In the Stream API to be discussed later, we’ll see how parallel processing can be done effectively by maximizing the use of functional interfaces.
Optional + Lambda Combination
The java.util.Optional class is a class for expressing cases where a value exists or doesn’t exist, with higher-order functions like map, filter, and flatMap. Optional’s higher-order functions can be combined for concise expression, potentially freeing from defensive logic due to fear of NullPointerException.
- Liberation from ‘If (obj != Null)’ null checks
| |
- Creating empty objects
| |
- Creating non-null objects
| |
- Calling specific method when value exists
| |
- No need to express with ternary operator for value existence cases
| |
- When you want to perform specific behavior only when certain conditions are met
| |
Standard Functional Interfaces
You don’t have to define a functional interface yourself every time. Java 8 ships the common shapes in the java.util.function package. There are 43 of them, but if you skip the primitive specializations (IntPredicate, LongFunction, etc.), knowing the ones below is mostly enough.
| Interface | Method | Use |
|---|---|---|
Predicate<T> | boolean test(T t) | Condition check (filter, etc.) |
Function<T, R> | R apply(T t) | Value conversion (map, etc.) |
Consumer<T> | void accept(T t) | Consume a value (forEach, etc.) |
Supplier<T> | T get() | Produce a value (lazy creation, factories, etc.) |
UnaryOperator<T> | T apply(T t) | Function whose input and output types match |
BinaryOperator<T> | T apply(T t1, T t2) | Fold two values of one type into one (reduce, etc.) |
They also come with default methods for composition, so small lambdas can be chained together.
| |
PS. Expressions like String::isEmpty and System.out::println above are method references, a shorter syntax for a lambda that just calls one existing method. Static methods (Integer::parseInt), instance methods (String::length), and constructors (ArrayList::new) can all be written as references.
By the way, a lambda is not an anonymous class
I described lambdas like anonymous classes above, but if you look at the compiled bytecode, no anonymous class file (Outer$1.class) is generated. The lambda body is compiled into a private method like lambda$main$0, and the call site gets an invokedynamic instruction that builds the functional interface implementation at runtime on first call. That’s why this inside a lambda refers to the enclosing class instance, not an anonymous class object.
It’s also why names like lambda$main$0 show up in stack traces. If a lambda body gets long enough to be hard to debug, pulling it out into a separate method and passing a method reference is easier both to read and to debug.