Streams and Lambdas
Java's functional-style tools for processing collections declaratively instead of with manual loops.
2 min read
Since Java 8, the language has offered a genuinely functional-programming-flavored way to process collections: lambdas for compact inline functions, and the Stream API for chaining operations like filter, map, and reduce without hand-writing loops.
Lambda expressions
A lambda is a short, anonymous function — useful anywhere Java expects an object implementing a single-method interface (a "functional interface").
Runnable task = () -> System.out.println("Running!");
task.run();
Comparator<String> byLength = (a, b) -> a.length() - b.length();The general shape is (parameters) -> expression-or-block. Compare this to writing a full anonymous class for the same job — lambdas exist specifically to remove that boilerplate:
// Before lambdas
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};The Stream API
A stream represents a sequence of elements you can process through a pipeline of operations, without writing explicit loops or intermediate variables:
import java.util.List;
import java.util.stream.Collectors;
List<String> names = List.of("Ada", "Grace", "Alan", "Barbara");
List<String> shortNamesUpper = names.stream()
.filter(name -> name.length() <= 4)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(shortNamesUpper); // [ADA, ALAN]Reading it top to bottom: turn the list into a stream, keep only names of length 4 or less, convert each surviving name to uppercase, then collect the results back into a List. Each step is declarative — you describe what transformation happens, not the loop mechanics of how.
Common stream operations
filter(predicate)— keeps only elements matching a condition.map(function)— transforms each element into something else.sorted()— sorts the stream.collect(...)— gathers the stream back into a collection (or other result).reduce(...)— combines all elements into a single value.forEach(...)— performs an action on each element (mainly for side effects like printing).
int totalLength = names.stream()
.mapToInt(String::length)
.sum();Method references
String::toUpperCase above is a method reference — shorthand for a lambda that just calls an existing method: name -> name.toUpperCase() and String::toUpperCase do the same thing, the second is just more concise when the lambda body is nothing more than a single method call.
Streams don't replace loops entirely
Streams shine for transforming and querying data — filtering, mapping, aggregating. For anything involving complex branching logic, mutating external state, or performance-critical tight loops, a plain for loop is often clearer and sometimes faster. Reach for streams when they make the intent of a data transformation clearer, not as a rule to apply everywhere loops used to be.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.