Index

Functional Programming in Collections and APIs

Java Functional Programming

13.1 Using Streams for Collection Manipulation

Java's Streams API, introduced in Java 8, revolutionized how developers interact with collections by enabling declarative, functional-style data processing. Instead of writing verbose for-loops or mutating data imperatively, you can now express operations like filtering, transforming, sorting, and aggregating in a clean, composable way.

Why Streams?

Traditional loops operate eagerly, mix logic and iteration, and often mutate intermediate state. In contrast, the Streams API:

Core Stream Operations

Let’s explore the most common operations with examples:

Filtering and Mapping

import java.util.List;
import java.util.stream.Collectors;

public class StreamBasics {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie", "David");

        List<String> filtered = names.stream()
            .filter(name -> name.length() > 3)
            .map(String::toUpperCase)
            .collect(Collectors.toList());

        System.out.println(filtered); // [ALICE, CHARLIE, DAVID]
    }
}

Sorting and Distinct

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

List<Integer> sorted = numbers.stream()
    .distinct()
    .sorted()
    .collect(Collectors.toList());

System.out.println(sorted); // [1, 2, 3, 4, 5]

Grouping and Partitioning

import java.util.Map;
import java.util.stream.Collectors;

List<String> animals = List.of("cat", "cow", "dog", "dolphin");

Map<Character, List<String>> grouped = animals.stream()
    .collect(Collectors.groupingBy(name -> name.charAt(0)));

System.out.println(grouped); // {c=[cat, cow], d=[dog, dolphin]}
Map<Boolean, List<String>> partitioned = animals.stream()
    .collect(Collectors.partitioningBy(name -> name.length() > 3));

System.out.println(partitioned); // {false=[cat, cow, dog], true=[dolphin]}

Benefits of Functional Collection Processing

Summary

The Streams API enables expressive, functional-style manipulation of Java collections. By chaining operations like map, filter, and collect, developers can write cleaner, safer, and more maintainable code. Whether filtering lists, sorting elements, or grouping by criteria, streams provide a declarative approach that fits naturally with modern functional programming techniques in Java.

Index

13.2 Functional Interfaces in Java APIs (e.g., Comparator, Runnable)

While Java 8 introduced the java.util.function package with core functional interfaces like Function, Predicate, and Consumer, several long-standing interfaces in Java’s standard library are also functional by design. These interfaces typically define a single abstract method, making them ideal targets for lambda expressions and method references.

Let’s explore some widely used functional interfaces already present in common Java APIs.

Comparator<T> Functional Sorting

The Comparator<T> interface is used to define custom ordering logic for objects. Its single abstract method is:

int compare(T o1, T o2);

Example: Sorting a list of strings by length

import java.util.*;

public class ComparatorExample {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie");

        names.stream()
            .sorted(Comparator.comparingInt(String::length))
            .forEach(System.out::println); // Bob, Alice, Charlie
    }
}

The use of Comparator.comparingInt with a method reference makes the sort logic declarative and functional.

Runnable Deferred Execution

Runnable represents a task to run, typically in a separate thread. Its abstract method:

void run();

Example: Executing a task in a new thread

public class RunnableExample {
    public static void main(String[] args) {
        Runnable task = () -> System.out.println("Running task...");
        new Thread(task).start();
    }
}

Using a lambda for Runnable avoids boilerplate like anonymous classes, improving readability.

Callable<V> Asynchronous Computation with Return

Callable<V> is similar to Runnable but returns a result and may throw an exception:

V call() throws Exception;

Example: Executing a computation with ExecutorService

import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        Callable<String> task = () -> "Result from callable";

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(task);

        System.out.println(future.get()); // Result from callable
        executor.shutdown();
    }
}

Supplier<T> Deferred Value Generation

Supplier<T> is used to generate or provide a value on demand:

T get();

Example: Lazy initialization

import java.util.function.Supplier;

public class SupplierExample {
    public static void main(String[] args) {
        Supplier<Double> randomSupplier = () -> Math.random();
        System.out.println(randomSupplier.get()); // e.g., 0.5834...
    }
}

Summary

Java’s built-in functional interfaces like Comparator, Runnable, Callable, and Supplier support functional programming idioms across many APIs. Thanks to lambda expressions, these interfaces can be used more succinctly than ever, enabling cleaner, more expressive, and less error-prone code—whether you’re sorting data, executing tasks, or providing values on demand.

Index

13.3 Example: Sorting and Grouping Complex Data

Sorting and grouping complex objects like employees by attributes such as department or salary range is a common task in real-world applications. Using Java Streams and functional programming techniques, we can express this logic declaratively, resulting in cleaner and more maintainable code.

Below is a runnable example that demonstrates:

Code Example

import java.util.*;
import java.util.stream.*;
  
public class EmployeeProcessing {
    static class Employee {
        String name;
        String department;
        double salary;

        Employee(String name, String department, double salary) {
            this.name = name;
            this.department = department;
            this.salary = salary;
        }

        @Override
        public String toString() {
            return name + " (" + department + ", $" + salary + ")";
        }
    }

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Alice", "HR", 48000),
            new Employee("Bob", "Engineering", 75000),
            new Employee("Charlie", "Engineering", 82000),
            new Employee("Diana", "HR", 52000),
            new Employee("Evan", "Marketing", 60000),
            new Employee("Fiona", "Engineering", 69000)
        );

        // 1. Sort by department, then by salary descending
        List<Employee> sorted = employees.stream()
            .sorted(Comparator.comparing((Employee e) -> e.department)
                    .thenComparing(Comparator.comparingDouble((Employee e) -> e.salary).reversed()))
            .collect(Collectors.toList());

        System.out.println("Sorted Employees:");
        sorted.forEach(System.out::println);

        // 2. Group by department
        Map<String, List<Employee>> groupedByDept = employees.stream()
            .collect(Collectors.groupingBy(e -> e.department));

        System.out.println("\nGrouped by Department:");
        groupedByDept.forEach((dept, list) -> {
            System.out.println(dept + ": " + list);
        });

        // 3. Partition by salary > 70000
        Map<Boolean, List<Employee>> highEarners = employees.stream()
            .collect(Collectors.partitioningBy(e -> e.salary > 70000));

        System.out.println("\nPartitioned by Salary > 70000:");
        highEarners.forEach((isHigh, list) -> {
            System.out.println((isHigh ? "High Earners" : "Others") + ": " + list);
        });
    }
}

Explanation

Benefits of Functional Style

This approach demonstrates how Java's functional features simplify complex data manipulation tasks.

Index