Java Advanced: Generics, Streams and Concurrency
Lambdas and Streams
In this lesson you will
- Write lambdas and use the core functional interfaces
- Build stream pipelines with filter, map, sorted and collect
- Group and summarise data and use Optional
Introduction
Traditionally, you tell Java how to do a job step by step: make a list, loop, test each item, add matches. Since Java 8 you can instead describe what you want, using lambdas (small functions you can pass around) and streams (pipelines that process collections). The result is shorter, and often clearer.
Lambdas
A lambda expression is an anonymous function: parameters, an arrow, and a body.
Java
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Function<Integer, Integer> square = n -> n * n;
Predicate<String> isLong = s -> s.length() > 4;
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Supplier<String> greeting = () -> "hello";
Consumer<String> printer = s -> System.out.println("> " + s);
System.out.println(square.apply(7));
System.out.println(isLong.test("banana"));
System.out.println(add.apply(3, 4));
printer.accept(greeting.get());
}
}49
true
7
> helloA lambda can be used wherever Java expects a functional interface: an interface with exactly one abstract method. The standard ones live in java.util.function:
| Interface | Method | Meaning |
|---|---|---|
Function<T, R> | apply(T) -> R | transform a value |
Predicate<T> | test(T) -> boolean | a yes/no test |
Consumer<T> | accept(T) | do something with a value |
Supplier<T> | get() -> T | produce a value |
BiFunction<T, U, R> | apply(T, U) -> R | combine two values |
Quick quiz
What is a functional interface?
Method references
When a lambda only calls an existing method, a method reference is a shorter way to say the same thing.
Java
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("asha", "ben", "chloe");
names.forEach(n -> System.out.println(n));
names.forEach(System.out::println);
System.out.println(names.stream().map(String::toUpperCase).toList());
System.out.println(names.stream().map(String::length).toList());
}
}asha
ben
chloe
asha
ben
chloe
[ASHA, BEN, CHLOE]
[4, 3, 5]Streams: the pipeline idea
A stream is a sequence of items flowing through a pipeline of steps. It has three parts:
- A source (a collection, an array,
IntStream.range). - Zero or more intermediate operations (
filter,map,sorted,distinct,limit), which return a new stream. - One terminal operation (
collect,toList,count,sum,forEach,reduce), which produces the result.
Nothing runs until the terminal operation is called. A stream does not change the original collection.
Java
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(5, 2, 8, 1, 9, 4, 7, 2);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // keep even numbers
.map(n -> n * 10) // multiply each by 10
.distinct() // remove duplicates
.sorted() // sort ascending
.toList();
System.out.println(result);
System.out.println(numbers);
}
}[20, 40, 80]
[5, 2, 8, 1, 9, 4, 7, 2]Read it top to bottom like a sentence: "take the numbers, keep the even ones, multiply by ten, remove duplicates, sort, give me a list".
Numbers: count, sum, average, max
Java
import java.util.List;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
List<Integer> scores = List.of(72, 85, 90, 64);
System.out.println(scores.stream().count());
System.out.println(scores.stream().mapToInt(Integer::intValue).sum());
System.out.println(scores.stream().mapToInt(Integer::intValue).average().getAsDouble());
System.out.println(scores.stream().max(Integer::compare).get());
System.out.println(IntStream.rangeClosed(1, 5).sum());
System.out.println(IntStream.rangeClosed(1, 5).boxed().toList());
System.out.println(scores.stream().anyMatch(s -> s > 89));
System.out.println(scores.stream().allMatch(s -> s >= 60));
}
}4
311
77.75
90
15
[1, 2, 3, 4, 5]
true
truemapToInt turns a Stream<Integer> into an IntStream, which has sum() and average(). average() returns an OptionalDouble, since an empty stream has no average.
Streams of objects
Streams shine on collections of your own objects. Here we use a record (Java 16+).
Java
import java.util.List;
import java.util.stream.Collectors;
record Student(String name, int grade, String house) {}
public class Main {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Asha", 85, "Red"),
new Student("Ben", 72, "Blue"),
new Student("Chloe", 91, "Red"),
new Student("Dev", 64, "Blue"),
new Student("Elena", 78, "Green")
);
List<String> passed = students.stream()
.filter(s -> s.grade() >= 75)
.map(Student::name)
.toList();
System.out.println(passed);
String roll = students.stream()
.sorted((a, b) -> Integer.compare(b.grade(), a.grade()))
.map(s -> s.name() + " (" + s.grade() + ")")
.collect(Collectors.joining(", "));
System.out.println(roll);
}
}[Asha, Chloe, Elena]
Chloe (91), Asha (85), Elena (78), Ben (72), Dev (64)Grouping and collecting
Collectors can turn a stream into a map. groupingBy groups items by a key. We ask for a TreeMap, so that the groups print in a predictable order.
Java
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
record Student(String name, int grade, String house) {}
public class Main {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Asha", 85, "Red"),
new Student("Ben", 72, "Blue"),
new Student("Chloe", 91, "Red"),
new Student("Dev", 64, "Blue"),
new Student("Elena", 78, "Green")
);
Map<String, List<String>> byHouse = students.stream()
.collect(Collectors.groupingBy(
Student::house,
TreeMap::new,
Collectors.mapping(Student::name, Collectors.toList())));
System.out.println(byHouse);
Map<String, Double> average = students.stream()
.collect(Collectors.groupingBy(
Student::house,
TreeMap::new,
Collectors.averagingInt(Student::grade)));
System.out.println(average);
Map<String, Long> counts = students.stream()
.collect(Collectors.groupingBy(Student::house, TreeMap::new, Collectors.counting()));
System.out.println(counts);
}
}{Blue=[Ben, Dev], Green=[Elena], Red=[Asha, Chloe]}
{Blue=68.0, Green=78.0, Red=88.0}
{Blue=2, Green=1, Red=2}Quick quiz
When does a stream pipeline actually do its work?
reduce
reduce combines all items into one value, using a starting value and a combining function.
Java
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4);
int product = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println(product);
String sentence = List.of("Java", "is", "fun").stream().reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
System.out.println(sentence);
}
}24
Java is funOptional: a value that might be missing
Instead of returning null, a method can return an Optional, which forces the caller to think about the empty case.
Java
import java.util.List;
import java.util.Optional;
public class Main {
static Optional<String> findLong(List<String> words) {
return words.stream().filter(w -> w.length() > 5).findFirst();
}
public static void main(String[] args) {
Optional<String> found = findLong(List.of("cat", "elephant", "dog"));
Optional<String> none = findLong(List.of("cat", "dog"));
System.out.println(found.isPresent());
System.out.println(found.get());
System.out.println(none.orElse("nothing found"));
found.ifPresent(w -> System.out.println("Found " + w));
System.out.println(none.map(String::toUpperCase).orElse("n/a"));
}
}true
elephant
nothing found
Found elephant
n/aAvoid calling get() without checking, as it throws if the Optional is empty. Prefer orElse, ifPresent and map.
When not to use streams
Streams are not always better. A simple loop is clearer when you need to modify variables, break early in complicated ways, or handle checked exceptions. Keep lambdas short. If a lambda grows past a few lines, move it into a named method.
Try it yourself
On your computer, take the list of words "stream", "lambda", "java", "optional", "record", "java" and use one pipeline to produce the distinct words longer than 4 letters, in upper case, sorted alphabetically. Then use Collectors.groupingBy with String::length to group the words by length.
Summary
- A lambda is a function value. Functional interfaces such as
Function,PredicateandConsumerdescribe their shape, andClass::methodis shorthand. - A stream pipeline has a source, intermediate steps (
filter,map,sorted) and one terminal step (toList,count,collect,reduce). It does not change the original collection. Collectors.groupingByandjoiningsummarise data. Primitive streams (IntStream) havesumandaverage.Optionalrepresents a maybe-missing value. UseorElse,mapandifPresent.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.