Java Lambda Expressions: Essential Guide for Collections and Dates
Lambda expressions are one of the most important features introduced in Java 8. They let you write more concise and readable code, especially when working with collections and dates. I use them daily, and this guide covers the lambda patterns I reach for most.
What are lambda expressions
A lambda expression is an anonymous function that you can pass as an argument to methods. It treats functionality as data, which makes code more flexible and expressive.
Basic syntax
The syntax of a lambda has three parts:
(parameters) -> { function body }Simple example:
// Traditional way with anonymous class
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Hello World");
}
};
// Lambda way (more concise)
Runnable r2 = () -> System.out.println("Hello World");Traversing collections with lambdas
Traversing arrays
Arrays can be elegantly traversed using lambdas and Streams:
String[] names = {"Ana", "Carlos", "Beatriz", "David"};
// Traversal with forEach and lambda
Arrays.stream(names)
.forEach(name -> System.out.println("Hello " + name));
// Or with method reference
Arrays.stream(names)
.forEach(System.out::println);Traversing lists
Lists are where I use lambda expressions most frequently:
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Strawberry");
// Basic traversal
fruits.forEach(fruit -> System.out.println("I like " + fruit));
// Traversal with position numbers
IntStream.range(0, fruits.size())
.forEach(i -> System.out.println(i + ": " + fruits.get(i)));
// Filter and traverse
fruits.stream()
.filter(fruit -> fruit.length() > 6)
.forEach(fruit -> System.out.println("Long fruit: " + fruit));Traversing maps
Maps benefit enormously from lambdas too:
Map<String, Integer> ages = new HashMap<>();
ages.put("Ana", 25);
ages.put("Carlos", 32);
ages.put("Beatriz", 28);
// Traverse entry set (key-value)
ages.forEach((name, age) ->
System.out.println(name + " is " + age + " years old")
);
// Traverse only keys
ages.keySet().forEach(key ->
System.out.println("Person: " + key)
);
// Traverse only values
ages.values().forEach(value ->
System.out.println("Age: " + value)
);Working with dates using lambdas
The date and time API introduced in Java 8 pairs naturally with lambdas.
Working with LocalDate
LocalDate represents a date without time:
// Create dates
LocalDate today = LocalDate.now();
LocalDate christmas = LocalDate.of(2025, 12, 25);
// Operations with dates using streams
List<LocalDate> importantDates = Arrays.asList(
LocalDate.of(2025, 1, 1),
LocalDate.of(2025, 5, 10),
LocalDate.of(2025, 9, 16),
LocalDate.of(2025, 12, 25)
);
// Filter future dates
importantDates.stream()
.filter(date -> date.isAfter(today))
.forEach(date -> System.out.println("Future date: " + date));
// Format dates
importantDates.forEach(date ->
System.out.println(date.format(DateTimeFormatter.ofPattern("MMMM dd, yyyy")))
);Working with LocalTime
LocalTime represents a time without date:
// Create times
LocalTime now = LocalTime.now();
LocalTime lunch = LocalTime.of(14, 30);
// List of times of day
List<LocalTime> times = Arrays.asList(
LocalTime.of(8, 0), // Start of work
LocalTime.of(12, 0), // Lunch
LocalTime.of(14, 30), // Return from lunch
LocalTime.of(18, 0) // End of work
);
// Filter morning times
times.stream()
.filter(time -> time.isBefore(lunch))
.forEach(time -> System.out.println("Morning: " + time));
// Convert to minutes of the day
times.forEach(time ->
System.out.println("Minutes of day: " + time.toSecondOfDay() / 60)
);Working with LocalDateTime
LocalDateTime combines date and time:
// Create dates with time
LocalDateTime nowFull = LocalDateTime.now();
LocalDateTime meeting = LocalDateTime.of(2025, 11, 15, 10, 30);
// List of events
List<LocalDateTime> events = Arrays.asList(
LocalDateTime.of(2025, 11, 8, 9, 0), // Today, morning meeting
LocalDateTime.of(2025, 11, 15, 10, 30), // Next meeting
LocalDateTime.of(2025, 12, 25, 0, 0) // Christmas
);
// Filter events in current month
events.stream()
.filter(event -> event.getMonth() == nowFull.getMonth())
.forEach(event -> System.out.println("Event this month: " + event));
// Sort events by date
events.stream()
.sorted()
.forEach(event -> System.out.println(
"Event: " + event.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm"))
));Practical combined examples
Processing a list of people with dates
// Person class
class Person {
private String name;
private LocalDate birthDate;
// constructor, getters...
}
List<Person> people = Arrays.asList(
new Person("Ana", LocalDate.of(1995, 6, 15)),
new Person("Carlos", LocalDate.of(1990, 12, 3)),
new Person("Beatriz", LocalDate.of(1998, 8, 20))
);
// Calculate ages and filter adults
LocalDate today = LocalDate.now();
people.stream()
.filter(p -> Period.between(p.getBirthDate(), today).getYears() >= 18)
.forEach(p -> {
int age = Period.between(p.getBirthDate(), today).getYears();
System.out.println(p.getName() + " is " + age + " years old");
});Grouping dates by month
// Group dates by month using lambdas
List<LocalDate> dates = Arrays.asList(
LocalDate.of(2025, 1, 15),
LocalDate.of(2025, 1, 20),
LocalDate.of(2025, 2, 10),
LocalDate.of(2025, 2, 25),
LocalDate.of(2025, 3, 5)
);
Map<Month, List<LocalDate>> datesByMonth = dates.stream()
.collect(Collectors.groupingBy(LocalDate::getMonth));
datesByMonth.forEach((month, dateList) -> {
System.out.println("Month: " + month);
dateList.forEach(date ->
System.out.println(" - " + date.format(DateTimeFormatter.ofPattern("MM/dd")))
);
});Best practices
Method references
When a lambda only calls an existing method, use a method reference instead:
// Instead of: name -> System.out.println(name)
names.forEach(System.out::println);
// Instead of: date -> date.getMonth()
dates.stream().map(LocalDate::getMonth).collect(Collectors.toList());
// Instead of: (String s) -> s.length()
words.stream().mapToInt(String::length).sum();Avoid side effects
Keep lambdas pure when possible:
// ✅ Good: Pure lambda
List<Integer> doubled = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
// ❌ Avoid: Lambda with side effects
List<Integer> result = new ArrayList<>();
numbers.forEach(n -> {
n = n * 2;
result.add(n); // Modifying external state
});Exception handling
Lambdas cannot directly throw checked exceptions:
// ✅ Handling with try-catch inside
dates.forEach(date -> {
try {
// Code that can throw exception
LocalDate.parse(date.toString());
} catch (DateTimeParseException e) {
System.err.println("Invalid date: " + date);
}
});
// ✅ Create helper method
private static void processDate(LocalDate date) throws DateTimeParseException {
// Code that can throw exception
}
dates.forEach(LambdaGuide::processDate); // Wraps the exceptionWrapping up
Lambda expressions changed how I write Java. They make code more concise, more readable, and open the door to functional programming within Java. Getting comfortable with them on collections and dates is essential for any modern Java developer.
Start with simple examples and work toward more complex cases. The patterns compound: once you internalize map, filter, and collect, everything else falls into place.
This guide is based on Oracle's official documentation and best practices established in the Java community. To deepen your knowledge, consult Oracle's official tutorials on Lambda Expressions and Stream API.