Understanding Java Lambda Expressions, Method References, and Stream API
This article explains Java's functional programming features—including Lambda expressions, method references, and the Stream API—by detailing their syntax, underlying mechanisms, and practical usage examples such as thread creation, data processing, and parallel streams.
To use the Stream API you must first understand its companions Lambda expressions and method references, which together enable a functional programming style in Java where functions are represented by method references and lambda syntax.
Lambda Expressions
Lambda expressions are anonymous functions derived from the mathematical λ calculus; they can be written with or without parameters and return values, for example:
// no parameters, no return value
() -> log.info("Lambda");
// with parameters, with return value
(int a, int b) -> { return a + b; }These are equivalent to traditional anonymous inner class implementations, but they are compiled to invoke the JVM invokedynamic instruction rather than generating a new class.
Method References
Method references allow assigning a method to a variable or passing it as a parameter using the :: operator. Examples include referencing Integer::parseInt or Integer::compare and assigning them to functional interfaces such as Function , Comparator , or IntBinaryOperator :
Function
f = Integer::parseInt;
Integer i = f.apply("10");
Comparator
c = Integer::compare;
int result = c.compare(100, 10);
IntBinaryOperator op = Integer::compare;
int r = op.applyAsInt(10, 100);Any accessible method can be referenced, and the method’s parameter and return types must match the abstract method of the target functional interface, which is marked with @FunctionalInterface .
Custom Functional Interface Example
A custom functional interface KiteFunction is defined with a single abstract method run that takes two generic parameters and returns a generic result:
@FunctionalInterface
public interface KiteFunction
{
R run(T t, S s);
}It can be used with a method reference such as FunctionTest::DateFormat or with a lambda expression to format dates:
KiteFunction
functionDateFormat = FunctionTest::DateFormat;
String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");Stream API Overview
The Stream API provides a fluent, chainable way to process collections without modifying the source. Common operations include filter , map , collect , reduce , and many others, each accepting functional interfaces such as Predicate , Function , or Consumer .
Examples of frequently used terminal and intermediate operations:
// Create a stream
Stream
s = Stream.of("a", "b", "c");
// Filter and collect to list
List
list = s.filter(e -> e.startsWith("a")).collect(Collectors.toList());
// Find max using Comparator
Integer max = Stream.of(2, 2, 100, 5).max(Integer::compareTo).get();
// Parallel stream example
users.parallelStream().forEach(u -> System.out.println(u));Parallel streams leverage the ForkJoinPool to execute operations concurrently, but they should be used only when the workload is CPU‑bound, the data set is large, and order‑dependent operations (e.g., limit , findFirst ) are not required.
Common Stream Operations
Below are brief descriptions of several core Stream methods, each illustrated with concise code snippets:
// of – create a stream from values
Stream
ofStream = Stream.of("a", "b", "c");
// empty – create an empty stream
Stream
empty = Stream.empty();
// concat – combine two streams
Stream
combined = Stream.concat(Stream.of("a"), Stream.of("b", "c"));
// max/min – find extreme values using a Comparator
Integer maxVal = Stream.of(1, 5, 3).max(Integer::compareTo).get();
Integer minVal = Stream.of(1, 5, 3).min(Integer::compareTo).get();
// findFirst / findAny – retrieve elements
String first = Stream.of("x", "y").findFirst().orElse(null);
String any = Stream.of("x", "y").findAny().orElse(null);
// count – number of elements
long cnt = Stream.of(1, 2, 3).count();
// peek – perform an action without consuming the stream
Stream.of("a", "b").peek(e -> System.out.println(e)).collect(Collectors.toList());
// forEach – consume the stream
Stream.of("a", "b").forEach(System.out::println);
// limit / skip – sub‑range selection
Stream.of("a", "b", "c").limit(2).forEach(System.out::println);
Stream.of("a", "b", "c").skip(2).forEach(System.out::println);
// distinct – remove duplicates
Stream.of("a", "b", "b").distinct().forEach(System.out::println);
// sorted – natural or custom order
Stream.of("c", "a", "b").sorted().forEach(System.out::println);
Stream.of("a1", "c6", "b3").sorted((x, y) -> Integer.compare(Integer.parseInt(x.substring(1)), Integer.parseInt(y.substring(1)) ).forEach(System.out::println);
// flatMap – flatten nested collections
List
> nested = Arrays.asList(list1, list2);
List
dtos = nested.stream()
.flatMap(List::stream)
.map(this::dao2Dto)
.collect(Collectors.toList());
// collect – gather results into collections or maps
List
filtered = Stream.of(1, 2, 5, 7, 8, 12, 33)
.filter(i -> i > 7)
.collect(Collectors.toList());
Map
> byId = users.stream()
.collect(Collectors.groupingBy(User::getUserId));
Map
countById = users.stream()
.collect(Collectors.groupingBy(User::getUserId, Collectors.counting()));
// toArray – convert to array
User[] arr = users.stream().toArray(User[]::new);
// reduce – aggregate values
int sum = Stream.of(1, 2, 5, 7).reduce(0, Integer::sum);Understanding these operations and their appropriate use cases enables developers to write concise, efficient, and readable data‑processing code in Java.
Wukong Talks Architecture
Explaining distributed systems and architecture through stories. Author of the "JVM Performance Tuning in Practice" column, open-source author of "Spring Cloud in Practice PassJava", and independently developed a PMP practice quiz mini-program.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.