← Back to Lecture
Practical Work 11

Functional Programming in Java

Master lambdas, streams, and functional interfaces for data processing

Duration 3 hours
Difficulty Advanced
Session 11 - Functional Programming

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

OperationTypeDescription
filter()IntermediateKeep elements matching condition
map()IntermediateTransform each element
flatMap()IntermediateTransform and flatten nested structures
sorted()IntermediateSort elements
distinct()IntermediateRemove duplicates
limit()/skip()IntermediatePagination
collect()TerminalGather results into collection
reduce()TerminalCombine elements into single result
forEach()TerminalExecute action for each element
count()TerminalCount elements
anyMatch()/allMatch()TerminalTest 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:

  1. countByDepartment() - Count employees per department
  2. getHighEarners(double threshold) - Get employees earning above threshold
  3. partitionByManagerStatus() - Split into managers and non-managers
  4. findLongestServingEmployee() - 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

Resources