Generics, Lambdas and Streams · Lesson 35 of 42
Lambdas, Functional Interfaces and Method References
Java 8 added lambda expressions: short, unnamed functions you can pass around like data. They replaced pages of anonymous classes, made collection code far shorter, and unlocked the Stream API. Modern Java – and every Spring Boot codebase – uses them everywhere.
In this lesson
- Lambda syntax and where lambdas can be used
- The core functional interfaces in
java.util.function - Method references
Optionalfor values that may be missing
From anonymous class to lambda
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Chitra", "Om", "Bela"));
// Before Java 8: an anonymous class
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println(names);
// With a lambda: same meaning, one line
names.sort((a, b) -> b.compareTo(a));
System.out.println(names);
}
}
[Om, Bela, Chitra] [Om, Chitra, Bela]
Lambda syntax
- (a, b)parameters – types are inferred
- ->arrow
- b.compareTo(a)body: one expression (returned automatically) or { statements; return …; }
A lambda can be used wherever a functional interface is expected – an interface with exactly one abstract method, like Comparator, Runnable or your own PriceRule from chapter 10.
The core functional interfaces
| Interface | Method | Shape | Typical use |
|---|---|---|---|
Predicate<T> | test(T) | T → boolean | filters, validation |
Function<T,R> | apply(T) | T → R | conversions, mapping |
Consumer<T> | accept(T) | T → nothing | printing, saving |
Supplier<T> | get() | nothing → T | lazy values, factories |
BiFunction<T,U,R> | apply(T, U) | (T, U) → R | combining two values |
UnaryOperator<T> | apply(T) | T → T | transformations |
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
Predicate<String> isEmail = s -> s.contains("@") && s.contains(".");
Predicate<String> isShort = s -> s.length() < 6;
System.out.println(isEmail.test("asha@mail.com") + " " + isEmail.and(isShort.negate()).test("a@b.c"));
Function<Double, Double> addGst = p -> p * 1.18;
Function<Double, Long> round = Math::round;
System.out.println(addGst.andThen(round).apply(999.0));
Consumer<String> greet = n -> System.out.println("Hello, " + n);
greet.accept("Kabir");
Supplier<String> otp = () -> "OTP-" + 482913;
System.out.println(otp.get());
BiFunction<Integer, Integer, Integer> power = (b, e) -> (int) Math.pow(b, e);
System.out.println(power.apply(2, 10));
}
}
true false 1179 Hello, Kabir OTP-482913 1024
Predicates combine with and, or, negate; functions chain with andThen and compose.
Variables captured by a lambda
import java.util.List;
public class Main {
public static void main(String[] args) {
double discount = 0.1; // effectively final: never reassigned
List.of(100.0, 250.0).forEach(p -> System.out.println(p * (1 - discount)));
// discount = 0.2; → error: variables used in a lambda must be effectively final
}
}
90.0 225.0
Method references
When a lambda only calls an existing method, a method reference says the same thing more briefly:
| Kind | Lambda | Method reference |
|---|---|---|
| static method | s -> Integer.parseInt(s) | Integer::parseInt |
| instance method of the argument | s -> s.toUpperCase() | String::toUpperCase |
| method of a specific object | x -> System.out.println(x) | System.out::println |
| constructor | () -> new ArrayList<>() | ArrayList::new |
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> raw = List.of("12", "7", "30");
List<Integer> nums = new ArrayList<>();
raw.stream().map(Integer::parseInt).forEach(nums::add);
System.out.println(nums);
List.of("java", "sql").stream().map(String::toUpperCase).forEach(System.out::println);
}
}
[12, 7, 30] JAVA SQL
Optional: no more surprise nulls
Optional<T> is a box that either contains a value or is empty. Returning it from a method makes "this might not exist" visible in the type, instead of returning null and hoping callers check.
import java.util.Map;
import java.util.Optional;
public class Main {
static final Map<Integer, String> USERS = Map.of(1, "asha", 2, "ben");
static Optional<String> findUser(int id) {
return Optional.ofNullable(USERS.get(id));
}
public static void main(String[] args) {
System.out.println(findUser(1).map(String::toUpperCase).orElse("unknown"));
System.out.println(findUser(9).map(String::toUpperCase).orElse("unknown"));
findUser(2).ifPresent(u -> System.out.println("Welcome back, " + u));
System.out.println(findUser(9).isPresent());
try {
findUser(9).orElseThrow(() -> new IllegalArgumentException("No user 9"));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
ASHA unknown Welcome back, ben false No user 9
Common mistake: Optional.get() without checking
optional.get() on an empty Optional throws NoSuchElementException – just a different crash. Use orElse, orElseGet, orElseThrow, map or ifPresent. Use Optional for return types, not for fields or parameters.
Practice
Try it yourself
Create a
Predicate<Integer>for "even and greater than 10" and test 12, 7 and 8.Show solution
import java.util.function.Predicate; public class Main { public static void main(String[] args) { Predicate<Integer> even = n -> n % 2 == 0; Predicate<Integer> big = n -> n > 10; Predicate<Integer> both = even.and(big); for (int n : new int[]{12, 7, 8}) System.out.println(n + " " + both.test(n)); } }12 true 7 false 8 false
Quick check
What makes an interface "functional"?
It has exactly one abstract method.
Which functional interface takes nothing and returns a value?
Supplier<T>
Rewrite s -> s.length() as a method reference.
String::length
Summary
- Lambdas implement functional interfaces:
(params) -> body. - Know Predicate, Function, Consumer, Supplier, BiFunction; compose with and/or/andThen.
- Method references shorten simple lambdas; Optional makes missing values explicit.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.