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.
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.
(int a, int b) -> {return a + b} // Parameters -> Function logic (+@ return)
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).
The compiler converts lambda syntax to anonymous classes. In other words, it’s a form where the compiler is delegated to implement the functional interface.
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.
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)
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.
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.
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.
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// AS-ISMember member = memberRepository.findById(1L);
Coord coord =null;
if (member !=null) {
if (member.getAddress() !=null) {
String zipCode = member.getAddress().getZipCode();
if (zipCode !=null) {
coord = coordRepository.findByZipCode(zipCode)
}
}
}
// TO-BEOptional<Member> member = memberRepository.findById(1L);
Coord coord = member.map(Member::getAddress)
.map(address -> address.getZipCode())
.map(zipCode -> coordRepository.findByZipCode(zipCode))
.orElse(null)
Creating empty objects
1
Optional<Member> member = Optional.empty();
Creating non-null objects
1
2
Optional<Member> member = Optional.of(memberRepository.findById(1L));
member.get() // NullPointerException !!!
Calling specific method when value exists
1
2
3
4
5
6
7
8
9
// AS-ISMember member = memberRepository.findById(1L);
if (member !=null) {
System.out.println(member);
}
// TO-BEOptional<Member> member = Optional.ofNullable(memberRepository.findById(1L));
member.ifPresent(System.out::println);
No need to express with ternary operator for value existence cases
1
2
3
4
5
6
7
// AS-ISMember member = memberRepository.findById(1L);
System.out.println(member !=null? member : new Member("Unknown"));
// TO-BEOptional<Member> member = Optional.ofNullable(memberRepository.findById(1L));
member.orElse(new Member("Unknown")).ifPresent(System.out::println);
When you want to perform specific behavior only when certain conditions are met
1
2
3
4
5
6
7
8
9
10
// AS-ISMember member = memberRepository.findById(1L);
if (member !=null&& member.getRating() !=null&& member.getRating() >= 4.0) {
System.out.println(member);
}
// TO-BEOptional<Member> member = Optional.ofNullable(memberRepository.findById(1L));
member.filter(m -> m.getRating() >= 4.0)
.ifPresent(m -> System.out::println)
Java 8 lambdas are implemented using invokedynamic bytecode, allowing the JVM to perform runtime optimization.
1
2
3
4
5
6
7
// Lambdas are different from anonymous classesRunnable r1 = () -> System.out.println("Hello");
// The above code is NOT compiled as:// new Runnable() { ... }// Instead, it uses invokedynamic for JVM optimization
Generally, lambdas perform slightly better or similar to anonymous classes. There may be slight overhead on first invocation, but the difference becomes negligible after JVM optimization.