1. Lambda expressions and collections
Let’s recall what collection processing looked like before lambda expressions. Suppose we have a list of strings and we want to print them to the screen:
List<String> list = Arrays.asList("cat", "dog", "ox");
for (String s : list) {
System.out.println(s);
}
That’s simple, but if we want, for example, to remove all empty strings from a list, we have to write a loop with a condition and sometimes even use an iterator (otherwise you’ll get a ConcurrentModificationException). Or, say, sorting by string length:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Even for such a simple task — already 5 lines of code and lots of “noisy” braces. How to optimise it? You already know the answer: lambda expressions.
Using lambda expressions in collection methods
Since Java 8, collection interfaces have gained new methods that accept functional interfaces — which means we can pass lambda expressions to them. Here are the most popular ones:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
Example: forEach
Print all list elements (the old way):
for (String s : list) {
System.out.println(s);
}
Now — with a lambda:
list.forEach(s -> System.out.println(s));
Or even shorter, if you want to play the “Java guru”:
list.forEach(System.out::println); // method reference, we'll cover it later
Example: removeIf
Remove all empty strings from a list:
List<String> animals = new ArrayList<>(Arrays.asList("cat", "", "dog", "ox", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [cat, dog, ox]
Example: sort
Sort a list by string length:
List<String> animals = Arrays.asList("cat", "dog", "ox", "elephant");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [ox, cat, dog, elephant]
Example: replaceAll
Convert all strings to upper case:
List<String> animals = new ArrayList<>(Arrays.asList("cat", "dog", "ox"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [CAT, DOG, OX]
2. Stream API and lambda expressions
With Java 8, the Stream API was introduced — a powerful tool for processing collections in a functional style. Streams let you filter, transform, sort, and collect using method chains. And all those methods accept lambda expressions!
Important: A full deep dive into the Stream API will come later; for now — just basic examples to understand the role of lambdas.
Example: filtering
Keep only strings longer than 3 characters:
List<String> animals = Arrays.asList("cat", "elephant", "ox", "crocodile");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // elephant, crocodile
Example: transformation (map)
Make all strings uppercase:
List<String> animals = Arrays.asList("cat", "elephant", "ox");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [CAT, ELEPHANT, OX]
Example: sorting
Get a length-sorted list (without modifying the original):
List<String> animals = Arrays.asList("cat", "elephant", "ox", "crocodile");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [ox, cat, elephant, crocodile]
3. Comparison with anonymous classes
Let’s compare how the same code would look with an anonymous class versus a lambda.
Sorting by string length
Anonymous class:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Lambda expression:
animals.sort((a, b) -> a.length() - b.length());
Takeaway:
A lambda expression saves lots of lines and makes code more readable. Fewer braces, less noise — more essence!
Removing empty strings
Anonymous class:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
Lambda expression:
animals.removeIf(s -> s.isEmpty());
4. Practice: small tasks with lambda expressions
Let’s try using lambda expressions in a mini-program that works with a list of users.
Example 1: Filtering users by age
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Bob (25)
Example 2: Sorting users by name
List<User> users = Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alice (17), Bob (25), Charlie (15)]
Example 3: Transform a list of users into a list of names
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Alice, Bob, Charlie]
Example 4: Remove all minors
List<User> users = new ArrayList<>(Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]
5. Useful nuances
Nuances: scope and “effectively final” variables
A lambda expression can use variables from the enclosing method, but only if they are final or “effectively final” (that is, not modified after initialisation).
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
If you try to modify minAge after using it in a lambda, the compiler will produce an error.
Table: key collection and stream methods with lambda expressions
| Collection/stream method | What it does | Lambda type | Example |
|---|---|---|---|
|
For each element | |
|
|
Removes elements by a condition | |
|
|
Sorts elements | |
|
|
Replaces each element | |
|
|
Filters the stream | |
|
|
Transforms elements | |
|
|
Iterates over the stream | |
|
|
Sorting in the stream | |
|
7. Common mistakes
Error #1: Lambda is too long. If your lambda already has 5 lines of code, conditionals, loops, and try-catch, you should probably extract that code into a separate method. Lambdas are best for short logic.
Error #2: Using mutable variables. If you try to modify a variable from the enclosing method inside a lambda (for example, a counter), the compiler won’t allow it. The variable must be final or effectively final.
Error #3: Forgetting that collection/stream methods do not always modify the original collection. For example, stream().filter(...) does not modify the original list — it returns a new stream. If you want a collection, use collect(Collectors.toList()).
Error #4: Lambda does not match the expected type. If a method expects, for example, a Comparator<T>, and you try to pass a lambda with one parameter (instead of two), you’ll get a compilation error.
Error #5: Losing readability with nested lambdas. If you have a chain of map, filter, forEach, and inside each lambda there’s another lambda, the code becomes unreadable. In such cases, it’s better to split expressions into separate steps or extract parts into methods.
GO TO FULL VERSION