Functional Programming in Java
Master lambdas, streams, and functional interfaces for data processing
Objectives
By the end of this practical work, you will be able to:
- Write and use lambda expressions effectively
- Process collections using the Stream API
- Use filter, map, reduce, and collect operations
- Group and partition data with Collectors
- Apply functional programming to real-world data problems
- Choose between streams and loops appropriately
Prerequisites
- Solid understanding of Java Collections
- Familiarity with interfaces
- Understanding of anonymous classes (helpful)
Quick Reference: Stream Operations
| Operation | Type | Description |
|---|---|---|
filter() | Intermediate | Keep elements matching condition |
map() | Intermediate | Transform each element |
flatMap() | Intermediate | Transform and flatten nested structures |
sorted() | Intermediate | Sort elements |
distinct() | Intermediate | Remove duplicates |
limit()/skip() | Intermediate | Pagination |
collect() | Terminal | Gather results into collection |
reduce() | Terminal | Combine elements into single result |
forEach() | Terminal | Execute action for each element |
count() | Terminal | Count elements |
anyMatch()/allMatch() | Terminal | Test conditions |
Exercise 1: Employee Salary Analysis
Problem
Analyze employee data to generate salary reports and statistics.
Data Model
package functional.employee;
public record Employee(
String id,
String name,
String department,
int yearsOfService,
double salary,
boolean isManager
) {}
Sample Data
List<Employee> employees = List.of(
new Employee("E001", "Alice Chen", "Engineering", 5, 85000, false),
new Employee("E002", "Bob Smith", "Engineering", 8, 95000, true),
new Employee("E003", "Carol White", "Marketing", 3, 65000, false),
new Employee("E004", "David Brown", "Engineering", 2, 72000, false),
new Employee("E005", "Eva Martinez", "HR", 6, 70000, true),
new Employee("E006", "Frank Wilson", "Marketing", 10, 88000, true),
new Employee("E007", "Grace Lee", "Engineering", 4, 82000, false),
new Employee("E008", "Henry Taylor", "Finance", 7, 92000, true),
new Employee("E009", "Iris Johnson", "HR", 1, 55000, false),
new Employee("E010", "Jack Davis", "Finance", 5, 78000, false)
);
Tasks
public class EmployeeAnalysis {
// Task 1: Get names of all managers, sorted alphabetically
public List<String> getManagerNames(List<Employee> employees) {
return employees.stream()
.filter(Employee::isManager) // (#1:Method reference)
.map(Employee::name)
.sorted()
.toList();
}
// Task 2: Calculate average salary by department
public Map<String, Double> avgSalaryByDepartment(List<Employee> employees) {
return employees.stream()
.collect(Collectors.groupingBy(
Employee::department, // (#2:Grouping by field)
Collectors.averagingDouble(Employee::salary) // (#3:Averaging)
));
}
// Task 3: Find highest paid non-manager in each department
public Map<String, Optional<Employee>> topNonManagerByDept(List<Employee> employees) {
return employees.stream()
.filter(e -> !e.isManager())
.collect(Collectors.groupingBy(
Employee::department,
Collectors.maxBy(Comparator.comparingDouble(Employee::salary))
));
}
// Task 4: Calculate total payroll cost
public double totalPayroll(List<Employee> employees) {
return employees.stream()
.mapToDouble(Employee::salary) // (#4:Primitive stream)
.sum();
}
// Task 5: Get salary statistics
public DoubleSummaryStatistics getSalaryStats(List<Employee> employees) {
return employees.stream()
.mapToDouble(Employee::salary)
.summaryStatistics(); // (#5:Built-in statistics)
}
// Task 6: Group employees by salary range
public Map<String, List<Employee>> bySalaryRange(List<Employee> employees) {
return employees.stream()
.collect(Collectors.groupingBy(e -> {
if (e.salary() < 60000) return "Entry";
if (e.salary() < 80000) return "Mid";
if (e.salary() < 90000) return "Senior";
return "Executive";
}));
}
// Task 7: Find employees eligible for promotion (>3 years, not manager)
public List<Employee> promotionCandidates(List<Employee> employees) {
return employees.stream()
.filter(e -> e.yearsOfService() > 3)
.filter(e -> !e.isManager())
.sorted(Comparator.comparingInt(Employee::yearsOfService).reversed())
.toList();
}
// Task 8: Create formatted salary report
public String salaryReport(List<Employee> employees) {
return employees.stream()
.sorted(Comparator.comparingDouble(Employee::salary).reversed())
.map(e -> String.format("%s (%s): $%,.2f",
e.name(), e.department(), e.salary()))
.collect(Collectors.joining("\n")); // (#6:Join to string)
}
}
Your Tasks
Implement these additional methods:
countByDepartment()- Count employees per departmentgetHighEarners(double threshold)- Get employees earning above thresholdpartitionByManagerStatus()- Split into managers and non-managersfindLongestServingEmployee()- Return Optional of longest-serving
Exercise 2: Word Frequency Counter
Problem
Process text to analyze word frequencies, find patterns, and generate statistics.
Implementation
package functional.text;
import java.util.*;
import java.util.function.Function;
import java.util.stream.*;
public class WordAnalyzer {
private static final Set<String> STOP_WORDS = Set.of(
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
"of", "with", "is", "are", "was", "were", "be", "been", "being"
);
// Extract words from text
public List<String> extractWords(String text) {
return Arrays.stream(text.toLowerCase()
.replaceAll("[^a-zA-Z\\s]", "") // Remove punctuation
.split("\\s+"))
.filter(w -> !w.isEmpty())
.toList();
}
// Task 1: Count word frequencies
public Map<String, Long> wordFrequencies(String text) {
return extractWords(text).stream()
.collect(Collectors.groupingBy(
Function.identity(), // (#1:Word itself as key)
Collectors.counting()
));
}
// Task 2: Get top N most frequent words (excluding stop words)
public List<Map.Entry<String, Long>> topNWords(String text, int n) {
return wordFrequencies(text).entrySet().stream()
.filter(e -> !STOP_WORDS.contains(e.getKey())) // (#2:Filter stop words)
.filter(e -> e.getKey().length() > 2) // Ignore very short words
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(n)
.toList();
}
// Task 3: Group words by length
public Map<Integer, List<String>> wordsByLength(String text) {
return extractWords(text).stream()
.distinct()
.collect(Collectors.groupingBy(String::length)); // (#3:Group by length)
}
// Task 4: Find longest words
public List<String> longestWords(String text, int count) {
return extractWords(text).stream()
.distinct()
.sorted(Comparator.comparingInt(String::length).reversed())
.limit(count)
.toList();
}
// Task 5: Calculate average word length
public double averageWordLength(String text) {
return extractWords(text).stream()
.mapToInt(String::length)
.average()
.orElse(0.0);
}
// Task 6: Find words containing substring
public List<String> wordsContaining(String text, String substring) {
return extractWords(text).stream()
.distinct()
.filter(w -> w.contains(substring.toLowerCase()))
.sorted()
.toList();
}
// Task 7: Generate word length histogram
public String lengthHistogram(String text) {
Map<Integer, Long> lengthCounts = extractWords(text).stream()
.collect(Collectors.groupingBy(
String::length,
TreeMap::new, // (#4:Sorted map)
Collectors.counting()
));
return lengthCounts.entrySet().stream()
.map(e -> String.format("%2d: %s (%d)",
e.getKey(),
"*".repeat(Math.min(e.getValue().intValue(), 50)),
e.getValue()))
.collect(Collectors.joining("\n"));
}
}
Test Data
String sampleText = """
Java is a high-level, class-based, object-oriented programming language
that is designed to have as few implementation dependencies as possible.
It is a general-purpose programming language intended to let programmers
write once, run anywhere, meaning that compiled Java code can run on all
platforms that support Java without the need to recompile.
Java applications are typically compiled to bytecode that can run on any
Java virtual machine regardless of the underlying computer architecture.
The syntax of Java is similar to C and C++, but has fewer low-level
facilities than either of them. The Java runtime provides dynamic capabilities
such as reflection and runtime code modification that are typically not
available in traditional compiled languages.
""";
Exercise 3: Transaction Processing
Problem
Process financial transactions to generate reports, detect patterns, and calculate balances.
Data Model
package functional.finance;
import java.time.LocalDateTime;
public record Transaction(
String id,
String accountId,
TransactionType type,
double amount,
String category,
LocalDateTime timestamp,
String description
) {}
public enum TransactionType { CREDIT, DEBIT }
Implementation
public class TransactionProcessor {
// Task 1: Calculate net balance for an account
public double calculateBalance(List<Transaction> transactions, String accountId) {
return transactions.stream()
.filter(t -> t.accountId().equals(accountId))
.mapToDouble(t -> t.type() == TransactionType.CREDIT
? t.amount()
: -t.amount()) // (#1:Conditional mapping)
.sum();
}
// Task 2: Get transactions by category, sorted by amount descending
public Map<String, List<Transaction>> byCategory(List<Transaction> transactions) {
return transactions.stream()
.collect(Collectors.groupingBy(
Transaction::category,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingDouble(Transaction::amount).reversed())
.toList()
) // (#2:Post-processing collected list)
));
}
// Task 3: Total spending by category
public Map<String, Double> spendingByCategory(List<Transaction> transactions) {
return transactions.stream()
.filter(t -> t.type() == TransactionType.DEBIT)
.collect(Collectors.groupingBy(
Transaction::category,
Collectors.summingDouble(Transaction::amount)
));
}
// Task 4: Find largest transaction per type
public Map<TransactionType, Optional<Transaction>> largestByType(
List<Transaction> transactions) {
return transactions.stream()
.collect(Collectors.groupingBy(
Transaction::type,
Collectors.maxBy(Comparator.comparingDouble(Transaction::amount))
));
}
// Task 5: Partition into high-value (>1000) and normal transactions
public Map<Boolean, List<Transaction>> partitionByValue(
List<Transaction> transactions, double threshold) {
return transactions.stream()
.collect(Collectors.partitioningBy(
t -> t.amount() > threshold // (#3:Partitioning)
));
}
// Task 6: Generate monthly summary
public Map<String, DoubleSummaryStatistics> monthlySummary(
List<Transaction> transactions) {
return transactions.stream()
.collect(Collectors.groupingBy(
t -> t.timestamp().getYear() + "-" +
String.format("%02d", t.timestamp().getMonthValue()),
Collectors.summarizingDouble(Transaction::amount)
));
}
// Task 7: Find suspicious transactions (multiple large debits same day)
public List<Transaction> findSuspicious(List<Transaction> transactions,
double threshold, int minCount) {
// Group by account and date
Map<String, List<Transaction>> grouped = transactions.stream()
.filter(t -> t.type() == TransactionType.DEBIT)
.filter(t -> t.amount() > threshold)
.collect(Collectors.groupingBy(
t -> t.accountId() + "_" + t.timestamp().toLocalDate()
));
// Return transactions from groups with count >= minCount
return grouped.values().stream()
.filter(list -> list.size() >= minCount)
.flatMap(List::stream) // (#4:Flatten nested lists)
.toList();
}
// Task 8: Generate account statement
public String generateStatement(List<Transaction> transactions, String accountId) {
List<Transaction> accountTx = transactions.stream()
.filter(t -> t.accountId().equals(accountId))
.sorted(Comparator.comparing(Transaction::timestamp))
.toList();
StringBuilder sb = new StringBuilder();
sb.append("Account: ").append(accountId).append("\n");
sb.append("=".repeat(60)).append("\n");
double runningBalance = 0;
for (Transaction t : accountTx) {
double change = t.type() == TransactionType.CREDIT
? t.amount() : -t.amount();
runningBalance += change;
sb.append(String.format("%s | %s | %+10.2f | %10.2f | %s%n",
t.timestamp().toLocalDate(),
t.type() == TransactionType.CREDIT ? "CR" : "DR",
change,
runningBalance,
t.description()));
}
sb.append("=".repeat(60)).append("\n");
sb.append(String.format("Final Balance: $%,.2f%n", runningBalance));
return sb.toString();
}
}
Exercise 4: Log File Analysis
Problem
Parse and analyze application log files to identify errors, patterns, and performance issues.
Data Model
package functional.logs;
import java.time.LocalDateTime;
public record LogEntry(
LocalDateTime timestamp,
LogLevel level,
String source,
String message
) {}
public enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL }
Implementation
public class LogAnalyzer {
// Parse log line: "2024-01-15 10:30:45 [ERROR] UserService: Login failed"
public Optional<LogEntry> parseLine(String line) {
try {
String[] parts = line.split(" ", 4);
if (parts.length < 4) return Optional.empty();
LocalDateTime timestamp = LocalDateTime.parse(
parts[0] + "T" + parts[1]);
LogLevel level = LogLevel.valueOf(
parts[2].replaceAll("[\\[\\]]", ""));
String[] sourceMsg = parts[3].split(": ", 2);
return Optional.of(new LogEntry(
timestamp,
level,
sourceMsg[0],
sourceMsg.length > 1 ? sourceMsg[1] : ""
));
} catch (Exception e) {
return Optional.empty(); // (#1:Handle malformed lines)
}
}
// Parse multiple lines
public List<LogEntry> parseLog(List<String> lines) {
return lines.stream()
.map(this::parseLine)
.flatMap(Optional::stream) // (#2:Filter out empty optionals)
.toList();
}
// Task 1: Count entries by level
public Map<LogLevel, Long> countByLevel(List<LogEntry> entries) {
return entries.stream()
.collect(Collectors.groupingBy(
LogEntry::level,
Collectors.counting()
));
}
// Task 2: Get all errors from a specific source
public List<LogEntry> errorsFromSource(List<LogEntry> entries, String source) {
return entries.stream()
.filter(e -> e.level() == LogLevel.ERROR || e.level() == LogLevel.FATAL)
.filter(e -> e.source().contains(source))
.toList();
}
// Task 3: Find most active sources
public List<Map.Entry<String, Long>> mostActiveSources(
List<LogEntry> entries, int top) {
return entries.stream()
.collect(Collectors.groupingBy(
LogEntry::source,
Collectors.counting()
))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(top)
.toList();
}
// Task 4: Group errors by hour
public Map<Integer, Long> errorsByHour(List<LogEntry> entries) {
return entries.stream()
.filter(e -> e.level() == LogLevel.ERROR)
.collect(Collectors.groupingBy(
e -> e.timestamp().getHour(),
TreeMap::new,
Collectors.counting()
));
}
// Task 5: Find error bursts (multiple errors within minutes)
public List<List<LogEntry>> findErrorBursts(List<LogEntry> entries,
int windowMinutes, int minCount) {
List<LogEntry> errors = entries.stream()
.filter(e -> e.level() == LogLevel.ERROR || e.level() == LogLevel.FATAL)
.sorted(Comparator.comparing(LogEntry::timestamp))
.toList();
List<List<LogEntry>> bursts = new ArrayList<>();
List<LogEntry> currentBurst = new ArrayList<>();
for (LogEntry error : errors) {
if (currentBurst.isEmpty()) {
currentBurst.add(error);
} else {
LogEntry last = currentBurst.get(currentBurst.size() - 1);
long minutes = java.time.Duration.between(
last.timestamp(), error.timestamp()).toMinutes();
if (minutes <= windowMinutes) {
currentBurst.add(error);
} else {
if (currentBurst.size() >= minCount) {
bursts.add(new ArrayList<>(currentBurst));
}
currentBurst.clear();
currentBurst.add(error);
}
}
}
if (currentBurst.size() >= minCount) {
bursts.add(currentBurst);
}
return bursts;
}
// Task 6: Search messages by keyword
public List<LogEntry> searchMessages(List<LogEntry> entries, String keyword) {
String lowerKeyword = keyword.toLowerCase();
return entries.stream()
.filter(e -> e.message().toLowerCase().contains(lowerKeyword))
.toList();
}
// Task 7: Generate summary report
public String generateReport(List<LogEntry> entries) {
Map<LogLevel, Long> counts = countByLevel(entries);
var stats = entries.stream()
.collect(Collectors.summarizingLong(e -> 1));
return """
╔══════════════════════════════════════╗
║ LOG ANALYSIS REPORT ║
╠══════════════════════════════════════╣
║ Total Entries: %,10d ║
║ ║
║ By Level: ║
║ DEBUG: %,10d ║
║ INFO: %,10d ║
║ WARN: %,10d ║
║ ERROR: %,10d ║
║ FATAL: %,10d ║
╚══════════════════════════════════════╝
""".formatted(
entries.size(),
counts.getOrDefault(LogLevel.DEBUG, 0L),
counts.getOrDefault(LogLevel.INFO, 0L),
counts.getOrDefault(LogLevel.WARN, 0L),
counts.getOrDefault(LogLevel.ERROR, 0L),
counts.getOrDefault(LogLevel.FATAL, 0L)
);
}
}
Exercises to Complete
Additional Tasks
For each exercise, implement these additional methods:
Exercise 1 (Employees):
- Find departments where average salary > company average
- Create a ranking of employees by salary within each department
Exercise 2 (Words):
- Find anagrams in the text
- Calculate sentence complexity (avg words per sentence)
Exercise 3 (Transactions):
- Find accounts with negative balance
- Calculate running balance over time
Exercise 4 (Logs):
- Find recurring error patterns
- Calculate error rate (errors per hour)
Deliverables
Bonus Challenges
Advanced
Parallel Streams: Optimize the log analyzer for large files using parallel streams
Advanced
Custom Collector: Create a custom collector that calculates median
Expert
Stream Pipeline: Create a real-time log monitoring pipeline using infinite streams