Functional Programming Principles
Pure functions, immutability, higher-order functions, and using Optional correctly.
Deutsche Übersetzung in Arbeit
Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.
Auf dieser Seite
Lambdas are the tool; functional programming is the mindset. It's a style that favors pure functions, immutability, and composing small pieces of behavior. Java isn't a purely functional language, but adopting these principles makes code clearer, safer, and easier to test.
Pure functions
A pure function always returns the same output for the same input and has no side effects (it doesn't change anything outside itself).
// PURE: depends only on inputs, changes nothing external
int add(int a, int b) {
return a + b;
}
// IMPURE: reads and mutates external state
int total = 0;
void addToTotal(int x) {
total += x; // side effect!
}Put in the same coins and press the same button, and you always get the same snack - no surprises, nothing else in the room changes. A pure function is that predictable: same input, same output, no hidden effects. Predictability makes it trivial to test and reason about.
Immutability
Functional style avoids changing data in place. Instead of mutating an object, you
create a new one with the change. You saw this with Strings, records, and
java.time.
List<Integer> nums = List.of(1, 2, 3); // immutable list
List<Integer> doubled = nums.stream()
.map(n -> n * 2)
.toList(); // a NEW list; nums is untouchedWhy immutability helps
Immutable data can't be changed behind your back, is safe to share across threads, and makes bugs easier to trace (a value never mysteriously changes). Prefer creating new values over mutating existing ones.
Higher-order functions
A higher-order function takes a function as an argument or returns one. This is how you compose behavior:
// Takes a function as a parameter
static List<Integer> transform(List<Integer> list, Function<Integer,Integer> op) {
return list.stream().map(op).toList();
}
transform(List.of(1,2,3), x -> x * 10); // [10, 20, 30]
transform(List.of(1,2,3), x -> x + 1); // [2, 3, 4]Functions can also be composed - chained together:
Function<Integer,Integer> doubleIt = x -> x * 2;
Function<Integer,Integer> addOne = x -> x + 1;
Function<Integer,Integer> combined = doubleIt.andThen(addOne);
System.out.println(combined.apply(5)); // (5*2)+1 = 11Optional: banishing null
null is a frequent source of NullPointerException. Optional<T> is a
container that clearly represents "a value, or nothing" - forcing you to handle
the empty case:
Optional<String> found = users.stream()
.filter(u -> u.id() == 42)
.map(User::name)
.findFirst();
String name = found.orElse("Unknown"); // safe default
found.ifPresent(n -> System.out.println("Found: " + n));Instead of handing you a value that might secretly be null (a trap), a method
hands you a labeled box. You must open it and check whether anything's inside
before using it. Optional makes "there might be nothing here" impossible to
ignore.
Use Optional well
Optional is designed for return values that might be absent - not for
fields or method parameters. And don't just call .get() blindly (it throws if
empty); use orElse, orElseThrow, map, or ifPresent to handle both cases
safely.
Declarative vs. imperative
Functional style is declarative - you say what you want. The traditional style is imperative - you spell out how, step by step.
// Imperative: HOW
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.length() > 3) {
result.add(name.toUpperCase());
}
}
// Declarative: WHAT
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.toList();The declarative version reads like a description of the goal - clearer and less error-prone. That's the Stream API, which we'll master next.
Quick check
What makes a function 'pure'?
Key takeaways
- Functional programming favors pure functions: same input -> same output, no side effects.
- Prefer immutability - create new values instead of mutating existing ones.
- Higher-order functions take or return functions, letting you compose behavior (andThen).
- Optional<T> represents 'a value or nothing', replacing error-prone null; handle it with orElse/map/ifPresent.
- Declarative code says WHAT you want (streams); imperative code spells out HOW (manual loops).
Now let's put functional style to work on real data with Java's most beloved modern feature: the Stream API.