Mastering Lambda Expressions, Streams, and Reactive Programming Principles for Java Productivity
Lambda Expressions
Quick Example
Java 8 introduced lambda expressions, a compact way to represent anonymous functions tied to functional interfaces.
First, define a @FunctionalInterface:
@FunctionalInterface
public interface InstanceCreator<T> {
T make();
}
A sample Customer entity:
public class Customer {
private String fullName;
private int age;
public Customer() {}
public Customer(String fullName, int age) {
this.fullName = fullName;
this.age = age;
}
// Getters and setters omitted for brevity
}
Compare implemantation approaches:
public class LambdaDemo {
public static void main(String[] args) {
// 1. Concrete implementation
InstanceCreator<Customer> concrete = new CustomerCreator();
Customer cust1 = concrete.make();
System.out.println(cust1);
// 2. Anonymous inner class
InstanceCreator<Customer> anonymous = new InstanceCreator<>() {
@Override
public Customer make() {
return new Customer("Alice Chen", 24);
}
};
Customer cust2 = anonymous.make();
System.out.println(cust2);
// 3. Lambda expression
InstanceCreator<Customer> lambda = () -> new Customer("Alice Chen", 24);
Customer cust3 = lambda.make();
System.out.println(cust3);
}
static class CustomerCreator implements InstanceCreator<Customer> {
@Override
public Customer make() {
return new Customer();
}
}
}
Syntax Simplifications
- Single statement body: omit braces and
return// Before (String input) -> { return "Received: " + input; } // After input -> "Received: " + input - Single parameter: omit parentheses and type (if inferable)
num -> num * 10 - Multiple parameters: omit types if inferable
(a, b) -> a + b
Functional Interface Basics
A valid functional interface has exactly one abstract method, optionally marked with @FunctionalInterface for compile-time checks.
Built-in Java Functional Interfaces
Runnable
public class RunnableLambda {
public static void main(String[] args) {
new Thread(() -> {
String threadName = Thread.currentThread().getName();
System.out.printf("%s executing task%n", threadName);
}).start();
}
}
Supplier<T>
Produce a value without input:
import java.util.Arrays;
import java.util.function.Supplier;
public class SupplierLambda {
public static void main(String[] args) {
int[] nums = {9, 18, 3, 27, 12};
int peak = findExtreme(() -> {
int temp = nums[0];
for (int n : nums) {
if (n > temp) temp = n;
}
return temp;
});
System.out.println("Peak value: " + peak);
}
private static int findExtreme(Supplier<Integer> supplier) {
return supplier.get();
}
}
Consumer<T>
Accept a value without return, chainable via andThen:
import java.util.function.Consumer;
public class ConsumerLambda {
public static void main(String[] args) {
processText("Java 8+ Features", System.out::println);
processDual("Java 8+ Features",
s -> System.out.println(s.toUpperCase()),
s -> System.out.println(s.toLowerCase())
);
}
private static void processText(String input, Consumer<String> handler) {
handler.accept(input);
}
private static void processDual(String input, Consumer<String> first, Consumer<String> second) {
first.andThen(second).accept(input);
}
}
Predicate<T>
Test a boolean condition, chainable with and, or, negate:
import java.util.function.Predicate;
public class PredicateLambda {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;
System.out.println(isEven.and(isPositive).test(12));
}
}
Function<T, R>
Transform a value, chainable with andThen or compose:
import java.util.function.Function;
public class FunctionLambda {
public static void main(String[] args) {
String input = "Bob,32";
int adjustedAge = extractAge(input,
s -> s.split(",")[1],
Integer::parseInt,
n -> n + 5
);
System.out.println("Adjusted age: " + adjustedAge);
}
private static int extractAge(String raw,
Function<String, String> extract,
Function<String, Integer> parse,
Function<Integer, Integer> adjust) {
return extract.andThen(parse).andThen(adjust).apply(raw);
}
}
Lambda Internals
Lambda expressions are syntactic sugar. The JVM generates a synthetic class implementing the target functional interface, creates a singleton instance, and calls a synthetic static method containing your logic when the interface method is invoked.
Method References
Quick Example
Method references reuse existing methods where lambda logic is already implemented:
@FunctionalInterface
public interface TextDisplayer {
void show(String text);
}
public class MethodRefDemo {
public static void main(String[] args) {
// Lambda
display("Hello Stream", s -> System.out.println(s));
// Method reference
display("Hello Stream", System.out::println);
}
private static void display(String text, TextDisplayer displayer) {
displayer.show(text);
}
}
Syntax
Use :: operator:
| Type | Example |
|---|---|
| Class::static | Integer::parseInt |
| Class::new | Customer::new |
| Object::instance | "test"::toUpperCase |
| Array::new | int[]::new |
Streams
Quick Example
Filter and process collections declaratively:
import java.util.ArrayList;
import java.util.List;
public class StreamDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Zhang San");
names.add("Zhang San Feng");
names.add("Zhang Xue You");
names.add("Zhang Wei");
names.stream()
.filter(n -> n.startsWith("Zhang"))
.filter(n -> n.split(" ").length == 3)
.forEach(System.out::println);
}
}
Stream Pipeline
Streams follow source → intermediate operations → terminal operation. Intermediate operations return a new stream and are lazy; terminal operations trigger computation and close the stream.
Common Operations
Source Ceration
import java.util.*;
import java.util.stream.Stream;
public class StreamSources {
public static void main(String[] args) {
// Collections
List<String> list = List.of("a", "b");
Stream<String> listStream = list.stream();
Set<Integer> set = Set.of(1, 2, 3);
Stream<Integer> setStream = set.parallelStream();
Map<String, Integer> map = Map.of("x", 1, "y", 2);
Stream<String> keyStream = map.keySet().stream();
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
// Arrays
Double[] arr = {1.1, 2.2};
Stream<Double> arrStream = Arrays.stream(arr);
// Factory methods
Stream<String> ofStream = Stream.of("m", "n");
}
}
Intermediate Operations
filter: Keep elements matching a predicatemap: Transform elementssorted: Sort elementsskip: Drop first N eleemntslimit: Keep first N elementsdistinct: Remove duplicates
Terminal Operations
forEach: Execute action per elementcount: Get element countcollect: Gather elements to a collectionreduce: Accumulate elements to a single valueanyMatch/allMatch/noneMatch: Check conditions
import java.util.*;
import java.util.stream.Collectors;
public class StreamOperations {
public static void main(String[] args) {
String[] names = {"Zhang San", "Zhang San Feng", "Zhang Xue You", "Zhang Wei", "Li Si", "Zhang San"};
List<String> filtered = Arrays.stream(names)
.distinct()
.filter(n -> n.startsWith("Zhang"))
.sorted(Comparator.comparingInt(String::length))
.skip(1)
.limit(2)
.collect(Collectors.toList());
System.out.println(filtered);
Optional<Integer> maxLen = Arrays.stream(names)
.map(String::length)
.reduce(Integer::max);
maxLen.ifPresent(len -> System.out.println("Max name length: " + len));
}
}