- Use forEach, removeIf, and replaceAll methods
- Master Map operations like compute and merge
- Understand the basics of Stream API
- Apply lambdas for collection transformations
Lambdas with Collections
You know lambda syntax. You know method references. Time to put them to work with collections—where they really shine.
Sorting with Lambdas
forEach() - Iterate Over Elements
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Traditional loop
for (String name : names) {
System.out.println(name);
}
// With lambda
names.forEach(name -> System.out.println(name));
// With method reference
names.forEach(System.out::println);
Works with all Iterable types:
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5);
numbers.forEach(n -> System.out.println(n * 2));
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25);
ages.forEach((name, age) -> System.out.println(name + ": " + age));
removeIf() - Conditional Removal
Remove elements matching a predicate:
List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "", "Charlie", ""));
// Remove empty strings
names.removeIf(name -> name.isEmpty());
// Or: names.removeIf(String::isEmpty);
System.out.println(names); // [Alice, Bob, Charlie]
More examples:
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
// Remove even numbers
numbers.removeIf(n -> n % 2 == 0);
// Result: [1, 3, 5]
// Remove values less than 3
numbers.removeIf(n -> n < 3);
replaceAll() - Transform All Elements
Replace each element using a function:
List<String> names = new ArrayList<>(Arrays.asList("alice", "bob", "charlie"));
// Convert to uppercase
names.replaceAll(name -> name.toUpperCase());
// Or: names.replaceAll(String::toUpperCase);
System.out.println(names); // [ALICE, BOB, CHARLIE]
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// Double each number
numbers.replaceAll(n -> n * 2);
// Result: [2, 4, 6, 8, 10]
sort() with Comparator
List<String> names = new ArrayList<>(Arrays.asList("Charlie", "Alice", "Bob"));
// Sort alphabetically
names.sort((a, b) -> a.compareTo(b));
// Or: names.sort(String::compareTo);
// Sort by length
names.sort((a, b) -> a.length() - b.length());
// Or: names.sort(Comparator.comparingInt(String::length));
// Sort in reverse
names.sort((a, b) -> b.compareTo(a));
// Or: names.sort(Comparator.reverseOrder());
Using Comparator factory methods:
List<Person> people = new ArrayList<>();
// Sort by name
people.sort(Comparator.comparing(Person::getName));
// Sort by age descending
people.sort(Comparator.comparing(Person::getAge).reversed());
// Multi-level sort
people.sort(Comparator
.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparing(Person::getAge));
Map Operations with Lambdas
compute() - Compute a Value
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 10);
// Add 5 to Alice's score (or set to 5 if not exists)
scores.compute("Alice", (key, value) -> value == null ? 5 : value + 5);
// Alice -> 15
scores.compute("Bob", (key, value) -> value == null ? 5 : value + 5);
// Bob -> 5 (new entry)
computeIfAbsent() - Lazy Initialization
Only compute if key is absent:
Map<String, List<String>> groups = new HashMap<>();
// Traditional approach
if (!groups.containsKey("team1")) {
groups.put("team1", new ArrayList<>());
}
groups.get("team1").add("Alice");
// With computeIfAbsent
groups.computeIfAbsent("team1", key -> new ArrayList<>()).add("Alice");
groups.computeIfAbsent("team1", key -> new ArrayList<>()).add("Bob");
// team1 -> [Alice, Bob]
computeIfPresent() - Update Existing
Only compute if key exists:
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 10);
scores.computeIfPresent("Alice", (key, value) -> value + 5); // 15
scores.computeIfPresent("Bob", (key, value) -> value + 5); // No effect, Bob doesn't exist
merge() - Combine Values
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 10);
// Add 5 to existing value, or set to 5 if absent
scores.merge("Alice", 5, (oldVal, newVal) -> oldVal + newVal); // 15
scores.merge("Bob", 5, (oldVal, newVal) -> oldVal + newVal); // 5
// Simplified with Integer::sum
scores.merge("Alice", 5, Integer::sum);
getOrDefault()
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 10);
int aliceScore = scores.getOrDefault("Alice", 0); // 10
int bobScore = scores.getOrDefault("Bob", 0); // 0 (default)
Introduction to Streams
Streams provide a functional approach to processing collections:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// Traditional approach
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.length() > 4) {
result.add(name.toUpperCase());
}
}
// Stream approach
List<String> result = names.stream()
.filter(name -> name.length() > 4)
.map(String::toUpperCase)
.collect(Collectors.toList());
// [ALICE, CHARLIE, DAVID]
Common Stream Operations
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter - keep elements matching predicate
numbers.stream()
.filter(n -> n % 2 == 0) // Keep even numbers
.forEach(System.out::println); // 2, 4, 6, 8, 10
// map - transform elements
numbers.stream()
.map(n -> n * n) // Square each number
.forEach(System.out::println); // 1, 4, 9, 16, ...
// reduce - combine into single value
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // 55
// Or: .reduce(0, Integer::sum);
// count
long count = numbers.stream()
.filter(n -> n > 5)
.count(); // 5
// findFirst / findAny
Optional<Integer> first = numbers.stream()
.filter(n -> n > 5)
.findFirst(); // Optional[6]
// anyMatch / allMatch / noneMatch
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); // true
Collecting Results
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// To List
List<String> upperList = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// To Set
Set<String> upperSet = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toSet());
// To Map
Map<String, Integer> nameLengths = names.stream()
.collect(Collectors.toMap(
name -> name, // key
name -> name.length() // value
));
// {Alice=5, Bob=3, Charlie=7}
// Joining strings
String joined = names.stream()
.collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
Best Practices
1. Prefer Method References When Possible
// Good
names.forEach(System.out::println);
names.sort(String::compareToIgnoreCase);
// Less clear
names.forEach(n -> System.out.println(n));
2. Keep Lambdas Simple
// Complex lambda - extract to method
list.stream()
.filter(this::isValidItem) // Extracted method
.map(this::transformItem)
.collect(Collectors.toList());
3. Use Appropriate Collection Methods
// Use removeIf instead of iterator
names.removeIf(String::isEmpty); // Good
// vs manual iterator removal
// Use replaceAll for in-place transformation
names.replaceAll(String::toUpperCase); // Good
// vs creating new list
Lambdas make collection operations concise. forEach iterates, removeIf removes conditionally, replaceAll transforms in-place, sort with comparators, compute calculates map values, merge combines values. Streams provide functional pipelines. Method references improve readability. Modern Java collections embrace functional style.
