Post

Java Collections + Lambda Expressions — Practical Guide with Code Examples

Learn Java Collections with Lambda expressions and Streams API. Practical before/after code examples for filtering, sorting, grouping, mapping, and reducing. Complete cheat sheet for Java developers.

Java Collections + Lambda Expressions — Practical Guide with Code Examples

Programming Simplify your code with Collections + Lambda

What’s This About?

If you’re still writing for loops for everything — filtering lists, transforming data, finding items — there’s a better way. Java Collections combined with Lambda expressions let you write shorter, cleaner, more readable code.

This guide shows real, everyday examples. No theory dumps. Just “before and after” code you can use immediately.


The Basics: What is a Lambda?

A lambda is just a short way to write a function. Think of it as: “here’s what to do with each item.”

1
2
3
4
5
6
7
8
9
10
// Old way — anonymous class
Comparator<String> comp = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
};

// Lambda way — same thing, one line
Comparator<String> comp = (a, b) -> a.compareTo(b);

That’s it. Same logic, less ceremony.


Filtering a List

The task: Get all names that start with “A” from a list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<String> names = List.of("Anupam", "Bob", "Alice", "Charlie", "Amit");

// Old way — 5 lines
List<String> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name);
    }
}

// Lambda way — 1 line
List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .toList();

// Result: [Anupam, Alice, Amit]

Read it as: “From names, keep only those where name starts with A.”


Transforming Items (Map)

The task: Convert all names to uppercase.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<String> names = List.of("anupam", "bob", "alice");

// Old way
List<String> upper = new ArrayList<>();
for (String name : names) {
    upper.add(name.toUpperCase());
}

// Lambda way
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

// Result: [ANUPAM, BOB, ALICE]

Read it as: “From names, transform each one to uppercase.”


Finding One Item

The task: Find the first name longer than 4 characters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<String> names = List.of("Bob", "Alice", "Anupam", "Charlie");

// Old way
String found = null;
for (String name : names) {
    if (name.length() > 4) {
        found = name;
        break;
    }
}

// Lambda way
Optional<String> found = names.stream()
    .filter(name -> name.length() > 4)
    .findFirst();

// Result: Optional[Alice]

Optional means “it might not exist” — no more NullPointerException surprises.


Sorting

The task: Sort a list of people by age.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Anupam", 35),
    new Person("Bob", 28),
    new Person("Alice", 32)
);

// Lambda way
List<Person> sorted = people.stream()
    .sorted(Comparator.comparingInt(Person::age))
    .toList();

// Result: [Bob(28), Alice(32), Anupam(35)]

Sort by name instead?

1
2
3
people.stream()
    .sorted(Comparator.comparing(Person::name))
    .toList();

Reverse order?

1
2
3
people.stream()
    .sorted(Comparator.comparingInt(Person::age).reversed())
    .toList();

Removing Duplicates

1
2
3
4
5
6
7
List<String> cities = List.of("Singapore", "Tokyo", "Singapore", "Mumbai", "Tokyo");

List<String> unique = cities.stream()
    .distinct()
    .toList();

// Result: [Singapore, Tokyo, Mumbai]

Grouping Items

The task: Group people by their city.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
record Employee(String name, String city) {}

List<Employee> employees = List.of(
    new Employee("Anupam", "Singapore"),
    new Employee("Bob", "Tokyo"),
    new Employee("Alice", "Singapore"),
    new Employee("Charlie", "Tokyo")
);

Map<String, List<Employee>> byCity = employees.stream()
    .collect(Collectors.groupingBy(Employee::city));

// Result:
// Singapore -> [Anupam, Alice]
// Tokyo -> [Bob, Charlie]

Read it as: “Group all employees by their city.”


Counting Items

The task: Count how many people are in each city.

1
2
3
4
Map<String, Long> countByCity = employees.stream()
    .collect(Collectors.groupingBy(Employee::city, Collectors.counting()));

// Result: {Singapore=2, Tokyo=2}

Summing Values

The task: Calculate total salary.

1
2
3
4
5
6
7
8
9
10
11
12
13
record Employee(String name, double salary) {}

List<Employee> team = List.of(
    new Employee("Anupam", 8000),
    new Employee("Bob", 6000),
    new Employee("Alice", 7500)
);

double totalSalary = team.stream()
    .mapToDouble(Employee::salary)
    .sum();

// Result: 21500.0

Average salary?

1
2
3
4
5
6
double avg = team.stream()
    .mapToDouble(Employee::salary)
    .average()
    .orElse(0);

// Result: 7166.67

Joining Strings

The task: Create a comma-separated string of names.

1
2
3
4
5
6
List<String> names = List.of("Anupam", "Bob", "Alice");

String joined = names.stream()
    .collect(Collectors.joining(", "));

// Result: "Anupam, Bob, Alice"

With a prefix and suffix:

1
2
3
4
String joined = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));

// Result: "[Anupam, Bob, Alice]"

Checking Conditions

Are ALL items matching?

1
2
3
4
List<Integer> scores = List.of(75, 82, 91, 68, 88);

boolean allPassed = scores.stream().allMatch(score -> score >= 60);
// true — everyone scored 60+

Is ANY item matching?

1
2
boolean hasTopper = scores.stream().anyMatch(score -> score >= 90);
// true — 91 exists

Does NONE match?

1
2
boolean noneFailed = scores.stream().noneMatch(score -> score < 40);
// true — nobody below 40

Converting Between Collection Types

List to Set (remove duplicates)

1
2
3
List<String> list = List.of("A", "B", "A", "C", "B");
Set<String> set = new HashSet<>(list);
// [A, B, C]

List to Map

1
2
3
4
5
6
7
8
9
10
11
12
record Product(int id, String name) {}

List<Product> products = List.of(
    new Product(1, "Laptop"),
    new Product(2, "Phone"),
    new Product(3, "Tablet")
);

Map<Integer, String> productMap = products.stream()
    .collect(Collectors.toMap(Product::id, Product::name));

// {1=Laptop, 2=Phone, 3=Tablet}

Map to List

1
2
3
4
5
6
7
8
Map<String, Integer> scores = Map.of("Alice", 90, "Bob", 85);

List<String> toppers = scores.entrySet().stream()
    .filter(entry -> entry.getValue() >= 90)
    .map(Map.Entry::getKey)
    .toList();

// [Alice]

Chaining Operations

Coding Chain multiple operations for powerful data processing

The real power comes from chaining — combine filter, map, sort in one flow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
record Order(String customer, double amount, String status) {}

List<Order> orders = List.of(
    new Order("Alice", 250.0, "COMPLETED"),
    new Order("Bob", 100.0, "PENDING"),
    new Order("Alice", 450.0, "COMPLETED"),
    new Order("Charlie", 300.0, "COMPLETED"),
    new Order("Bob", 200.0, "COMPLETED")
);

// Get top 3 completed orders by amount, show customer names
List<String> topCustomers = orders.stream()
    .filter(o -> o.status().equals("COMPLETED"))
    .sorted(Comparator.comparingDouble(Order::amount).reversed())
    .limit(3)
    .map(Order::customer)
    .toList();

// Result: [Alice, Charlie, Alice]

Read it as: “From orders → keep completed → sort by amount descending → take top 3 → get names.”


Working with Maps

Iterate a Map

1
2
3
4
5
Map<String, Integer> ages = Map.of("Anupam", 35, "Bob", 28, "Alice", 32);

ages.forEach((name, age) -> 
    System.out.println(name + " is " + age + " years old")
);

Filter a Map

1
2
3
4
5
Map<String, Integer> seniors = ages.entrySet().stream()
    .filter(e -> e.getValue() >= 30)
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

// {Anupam=35, Alice=32}

Useful Collection Factory Methods (Java 9+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Immutable List
List<String> names = List.of("Anupam", "Bob", "Alice");

// Immutable Set
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5);

// Immutable Map
Map<String, String> config = Map.of(
    "host", "localhost",
    "port", "8080",
    "env", "production"
);

// Map with more than 10 entries
Map<String, String> bigMap = Map.ofEntries(
    Map.entry("key1", "value1"),
    Map.entry("key2", "value2"),
    Map.entry("key3", "value3")
);

These collections are immutable — you can’t add or remove items. Use new ArrayList<>(List.of(...)) if you need a mutable copy.


Quick Reference Cheat Sheet

What You WantMethodExample
Keep matching items.filter().filter(x -> x > 5)
Transform items.map().map(String::toUpperCase)
Sort.sorted().sorted(Comparator.comparing(...))
Remove duplicates.distinct().distinct()
Take first N.limit(n).limit(5)
Skip first N.skip(n).skip(2)
Find first match.findFirst().findFirst()
Check all match.allMatch().allMatch(x -> x > 0)
Check any match.anyMatch().anyMatch(x -> x > 100)
Count items.count().count()
Sum numbers.mapToInt().sum().mapToInt(x -> x).sum()
Collect to List.toList().toList()
Group by.collect(groupingBy()).collect(Collectors.groupingBy(...))
Join strings.collect(joining()).collect(Collectors.joining(", "))
To Map.collect(toMap()).collect(Collectors.toMap(...))

Common Mistakes to Avoid

1. Don’t reuse a stream

1
2
3
4
5
6
7
8
// This will throw IllegalStateException
Stream<String> stream = names.stream();
stream.filter(n -> n.startsWith("A")).toList();
stream.filter(n -> n.startsWith("B")).toList(); // BOOM!

// Create a new stream each time
names.stream().filter(n -> n.startsWith("A")).toList();
names.stream().filter(n -> n.startsWith("B")).toList();

2. Don’t modify the source list inside a lambda

1
2
3
4
5
6
7
// Bad — ConcurrentModificationException risk
names.stream().forEach(n -> names.remove(n));

// Good — collect to a new list
List<String> filtered = names.stream()
    .filter(n -> !n.startsWith("A"))
    .toList();

3. Use method references when possible

1
2
3
4
5
6
// These are the same:
.map(name -> name.toUpperCase())
.map(String::toUpperCase)  // Cleaner

.forEach(item -> System.out.println(item))
.forEach(System.out::println)  // Cleaner

When NOT to Use Streams

  • Simple loop with index — if you need the index, a regular for loop is fine
  • Single operation — list.contains(x) is better than list.stream().anyMatch(...)
  • Performance-critical tight loops — streams have overhead; measure first
  • Complex mutable state — if your logic builds complex state, a loop can be clearer

The Pattern

The combination of Collections + Lambda follows one pattern:

1
2
3
4
5
collection.stream()
    .filter(...)   // what to keep
    .map(...)      // how to transform
    .sorted(...)   // how to order
    .collect(...)  // what to produce

Start using these in your daily code. Once it clicks, you’ll wonder how you ever wrote all those for loops.


Further Reading

ResourceLink
Stream API — Oracle DocsJava SE 21 Stream
Collectors — Oracle DocsJava SE 21 Collectors
Lambda Expressions — Oracle TutorialOracle Tutorial
Method References — Oracle TutorialOracle Tutorial
Baeldung — Java Streamsbaeldung.com/java-streams

This post is licensed under CC BY 4.0 by the author.