Skip to main content

10 - Functional Programming and Stream API

Functional programming in Java: lambda expressions, functional interfaces, method references, Stream API, Collectors and Optional.

What do you think about this article?

At a Glance

Functional programming in Java: lambda expressions, functional interfaces, method references, Stream API, Collectors and Optional.

Functional Programming in Java

Since Java 8, the language supports functional programming through lambda expressions, method references, the Stream API and Optional. These tools enable more concise, readable and declarative code.

What You Will Learn

  • Lambda expressions and syntax
  • Functional interfaces
  • Method references
  • Stream API: intermediate and terminal operations
  • Collectors and grouping
  • Optional for null handling

Lambda Expressions

A lambda is an anonymous function that can be passed as a parameter or assigned to a variable.

Lambda Syntax
// Before Java 8 - anonymous class
Comparator<String> oldComparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
};

// With Lambda - Java 8+
Comparator<String> lambdaComparator = (s1, s2) -> s1.length() - s2.length();

// Syntax variants
// Single parameter: parentheses optional
Consumer<String> print = s -> System.out.println(s);

// No parameters: empty parentheses required
Runnable action = () -> System.out.println("Executed!");

// Body with multiple statements: use block {}
Comparator<String> withBlock = (s1, s2) -> {
    int diff = s1.length() - s2.length();
    return diff != 0 ? diff : s1.compareTo(s2);
};

Functional Interfaces

Main Functional Interfaces

Interface Method Use
Predicate<T> boolean test(T) Conditions, filters
Function<T,R> R apply(T) Transformations
Consumer<T> void accept(T) Actions on elements
Supplier<T> T get() Value generation
BiFunction<T,U,R> R apply(T,U) Two inputs, one output
UnaryOperator<T> T apply(T) Same type in/out
Functional Interface Examples
import java.util.function.*;

public class FunctionalExamples {
    public static void main(String[] args) {
        // Predicate: condition
        Predicate<Integer> isPassing = grade -> grade >= 60;
        System.out.println(isPassing.test(78));  // true
        System.out.println(isPassing.test(55));  // false

        // Function: transformation
        Function<String, Integer> length = s -> s.length();
        System.out.println(length.apply("Java"));  // 4

        // Consumer: action
        Consumer<String> greet = name -> System.out.println("Hello " + name);
        greet.accept("John");  // Hello John

        // Supplier: generation
        Supplier<Double> randomGrade = () -> 60 + Math.random() * 40;
        System.out.println(randomGrade.get());  // random grade 60-100

        // Composition
        Predicate<Integer> isExcellent = grade -> grade >= 90;
        Predicate<Integer> isGood = isPassing.and(isExcellent.negate());

        Function<String, String> uppercase = String::toUpperCase;
        Function<String, String> withPrefix = s -> "Prof. " + s;
        Function<String, String> title = uppercase.andThen(withPrefix);
        System.out.println(title.apply("smith"));  // Prof. SMITH
    }
}

Method References

Method Reference Types
import java.util.*;

public class MethodReferences {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Laura", "Joseph");

        // 1. Reference to static method
        // Lambda:  s -> System.out.println(s)
        names.forEach(System.out::println);

        // 2. Reference to instance method (specific object)
        String prefix = "Student: ";
        // Lambda: s -> prefix.concat(s)
        names.stream()
            .map(prefix::concat)
            .forEach(System.out::println);

        // 3. Reference to instance method (type)
        // Lambda: s -> s.toUpperCase()
        names.stream()
            .map(String::toUpperCase)
            .forEach(System.out::println);

        // 4. Reference to constructor
        // Lambda: s -> new StringBuilder(s)
        names.stream()
            .map(StringBuilder::new)
            .forEach(sb -> System.out.println(sb.reverse()));
    }
}

Stream API

Streams allow processing sequences of elements declaratively, with support for parallel operations.

Creating Streams
import java.util.*;
import java.util.stream.*;

public class StreamCreation {
    public static void main(String[] args) {
        // From Collection
        List<String> list = Arrays.asList("A", "B", "C");
        Stream<String> listStream = list.stream();

        // From array
        String[] array = {"A", "B", "C"};
        Stream<String> arrayStream = Arrays.stream(array);

        // Stream.of()
        Stream<String> ofStream = Stream.of("A", "B", "C");

        // Stream.generate() - infinite
        Stream<Double> randoms = Stream.generate(Math::random).limit(5);

        // Stream.iterate() - sequence
        Stream<Integer> evens = Stream.iterate(0, n -> n + 2).limit(10);

        // IntStream, LongStream, DoubleStream - primitives
        IntStream integers = IntStream.range(1, 11);  // 1-10
        IntStream integersClosed = IntStream.rangeClosed(1, 10);  // 1-10

        // From file (lines)
        // Stream<String> lines = Files.lines(Path.of("file.txt"));
    }
}

Intermediate Operations

Filter, Map, FlatMap
import java.util.*;
import java.util.stream.*;

public class IntermediateOperations {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("John", 85, "Computer Science"),
            new Student("Laura", 92, "Mathematics"),
            new Student("Joseph", 78, "Computer Science"),
            new Student("Anna", 88, "Physics")
        );

        // FILTER: select elements
        List<Student> passing = students.stream()
            .filter(s -> s.getGrade() >= 60)
            .collect(Collectors.toList());

        // MAP: transform elements
        List<String> names = students.stream()
            .map(Student::getName)
            .collect(Collectors.toList());

        // MAP with transformation
        List<String> upperNames = students.stream()
            .map(s -> s.getName().toUpperCase())
            .collect(Collectors.toList());

        // FLATMAP: flattens stream of streams
        List<List<String>> coursesPerStudent = Arrays.asList(
            Arrays.asList("Java", "Python"),
            Arrays.asList("SQL", "MongoDB"),
            Arrays.asList("Java", "JavaScript")
        );

        List<String> allCourses = coursesPerStudent.stream()
            .flatMap(List::stream)
            .distinct()
            .collect(Collectors.toList());
        // [Java, Python, SQL, MongoDB, JavaScript]

        // SORTED: sort
        List<Student> byGrade = students.stream()
            .sorted(Comparator.comparing(Student::getGrade).reversed())
            .collect(Collectors.toList());

        // DISTINCT: remove duplicates
        List<String> uniqueCourses = students.stream()
            .map(Student::getCourse)
            .distinct()
            .collect(Collectors.toList());

        // PEEK: debug (doesn't modify stream)
        students.stream()
            .peek(s -> System.out.println("Processing: " + s.getName()))
            .filter(s -> s.getGrade() >= 90)
            .forEach(System.out::println);

        // LIMIT and SKIP
        List<Student> first2 = students.stream()
            .limit(2)
            .collect(Collectors.toList());

        List<Student> afterFirst2 = students.stream()
            .skip(2)
            .collect(Collectors.toList());
    }
}

class Student {
    private String name;
    private int grade;
    private String course;

    public Student(String name, int grade, String course) {
        this.name = name;
        this.grade = grade;
        this.course = course;
    }

    public String getName() { return name; }
    public int getGrade() { return grade; }
    public String getCourse() { return course; }
}

Terminal Operations

Collect, Reduce, Match, Find
import java.util.*;
import java.util.stream.*;

public class TerminalOperations {
    public static void main(String[] args) {
        List<Integer> grades = Arrays.asList(85, 92, 78, 88, 92, 75);

        // FOREACH: action on each element
        grades.stream().forEach(System.out::println);

        // COUNT: count elements
        long howMany = grades.stream()
            .filter(g -> g >= 90)
            .count();  // 2

        // SUM, AVERAGE, MIN, MAX (IntStream)
        int sum = grades.stream()
            .mapToInt(Integer::intValue)
            .sum();

        OptionalDouble average = grades.stream()
            .mapToInt(Integer::intValue)
            .average();

        OptionalInt maximum = grades.stream()
            .mapToInt(Integer::intValue)
            .max();

        // REDUCE: reduce to single value
        int sumReduce = grades.stream()
            .reduce(0, (a, b) -> a + b);

        Optional<Integer> maxReduce = grades.stream()
            .reduce(Integer::max);

        // MATCH: verify conditions
        boolean allPassing = grades.stream()
            .allMatch(g -> g >= 60);  // true

        boolean atLeastOne100 = grades.stream()
            .anyMatch(g -> g == 100);  // false

        boolean noneFailing = grades.stream()
            .noneMatch(g -> g < 60);  // true

        // FIND: find elements
        Optional<Integer> first = grades.stream()
            .filter(g -> g >= 90)
            .findFirst();

        Optional<Integer> any = grades.stream()
            .filter(g -> g >= 90)
            .findAny();  // Useful with parallelStream

        // TOARRAY
        Integer[] gradeArray = grades.stream()
            .filter(g -> g >= 80)
            .toArray(Integer[]::new);
    }
}

Advanced Collectors

Grouping and Partitioning
import java.util.*;
import java.util.stream.*;

public class AdvancedCollectors {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("John", 85, "Computer Science"),
            new Student("Laura", 92, "Mathematics"),
            new Student("Joseph", 78, "Computer Science"),
            new Student("Anna", 88, "Physics"),
            new Student("Mark", 92, "Computer Science")
        );

        // GROUPING BY: group by key
        Map<String, List<Student>> byCourse = students.stream()
            .collect(Collectors.groupingBy(Student::getCourse));
        // {Computer Science=[John, Joseph, Mark], Mathematics=[Laura], Physics=[Anna]}

        // GroupingBy with downstream collector
        Map<String, Long> countByCourse = students.stream()
            .collect(Collectors.groupingBy(
                Student::getCourse,
                Collectors.counting()
            ));
        // {Computer Science=3, Mathematics=1, Physics=1}

        Map<String, Double> avgByCourse = students.stream()
            .collect(Collectors.groupingBy(
                Student::getCourse,
                Collectors.averagingInt(Student::getGrade)
            ));

        Map<String, Optional<Student>> bestPerCourse = students.stream()
            .collect(Collectors.groupingBy(
                Student::getCourse,
                Collectors.maxBy(Comparator.comparing(Student::getGrade))
            ));

        // PARTITIONING BY: divide into true/false
        Map<Boolean, List<Student>> passingVsNot = students.stream()
            .collect(Collectors.partitioningBy(s -> s.getGrade() >= 60));
        // {true=[all], false=[]}

        Map<Boolean, Long> countPassing = students.stream()
            .collect(Collectors.partitioningBy(
                s -> s.getGrade() >= 90,
                Collectors.counting()
            ));

        // JOINING: concatenate strings
        String concatenatedNames = students.stream()
            .map(Student::getName)
            .collect(Collectors.joining(", "));
        // "John, Laura, Joseph, Anna, Mark"

        String withPrefixSuffix = students.stream()
            .map(Student::getName)
            .collect(Collectors.joining(", ", "Students: [", "]"));
        // "Students: [John, Laura, Joseph, Anna, Mark]"

        // TO MAP
        Map<String, Integer> nameGrade = students.stream()
            .collect(Collectors.toMap(
                Student::getName,
                Student::getGrade
            ));

        // SUMMARIZING
        IntSummaryStatistics stats = students.stream()
            .collect(Collectors.summarizingInt(Student::getGrade));
        System.out.println("Min: " + stats.getMin());
        System.out.println("Max: " + stats.getMax());
        System.out.println("Average: " + stats.getAverage());
        System.out.println("Sum: " + stats.getSum());
        System.out.println("Count: " + stats.getCount());
    }
}

Optional

Optional is a container that may or may not contain a value, avoiding NullPointerException.

Using Optional
import java.util.*;

public class OptionalExamples {

    public static Optional<Student> findStudent(List<Student> students,
                                                  String name) {
        return students.stream()
            .filter(s -> s.getName().equals(name))
            .findFirst();
    }

    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("John", 85, "Computer Science"),
            new Student("Laura", 92, "Mathematics")
        );

        // Creating Optional
        Optional<String> full = Optional.of("Value");
        Optional<String> empty = Optional.empty();
        Optional<String> nullable = Optional.ofNullable(null);

        // isPresent and isEmpty
        Optional<Student> john = findStudent(students, "John");
        if (john.isPresent()) {
            System.out.println("Found: " + john.get().getGrade());
        }

        // ifPresent: execute action if present
        john.ifPresent(s -> System.out.println("Grade: " + s.getGrade()));

        // ifPresentOrElse (Java 9+)
        Optional<Student> paul = findStudent(students, "Paul");
        paul.ifPresentOrElse(
            s -> System.out.println("Found: " + s.getName()),
            () -> System.out.println("Student not found")
        );

        // orElse: default value
        Student result = paul.orElse(new Student("Default", 0, "N/A"));

        // orElseGet: supplier for default (lazy)
        Student resultLazy = paul.orElseGet(
            () -> new Student("Default", 0, "N/A")
        );

        // orElseThrow: exception if empty
        Student required = john.orElseThrow(
            () -> new NoSuchElementException("Student required!")
        );

        // map: transform if present
        Optional<String> studentName = john.map(Student::getName);

        // flatMap: for nested Optional
        Optional<String> courseOpt = john.flatMap(s ->
            Optional.ofNullable(s.getCourse())
        );

        // filter: filter value
        Optional<Student> excellent = john
            .filter(s -> s.getGrade() >= 90);

        // Complete chaining
        String finalResult = findStudent(students, "John")
            .filter(s -> s.getGrade() >= 80)
            .map(Student::getName)
            .map(String::toUpperCase)
            .orElse("NOT FOUND");
        System.out.println(finalResult);  // JOHN
    }
}

Complete Example: Student Registry Analysis

RegistryAnalysis.java
import java.util.*;
import java.util.stream.*;

public class RegistryAnalysis {

    public static void main(String[] args) {
        List<Exam> exams = Arrays.asList(
            new Exam("John", "Programming", 85, 2024),
            new Exam("John", "Database", 92, 2024),
            new Exam("John", "Networks", 78, 2024),
            new Exam("Laura", "Programming", 92, 2024),
            new Exam("Laura", "Database", 88, 2024),
            new Exam("Joseph", "Programming", 75, 2024),
            new Exam("Joseph", "Database", 80, 2023),
            new Exam("Anna", "Programming", 92, 2024),
            new Exam("Anna", "Networks", 85, 2024)
        );

        // 1. Average grade per student
        System.out.println("=== Average per student ===");
        Map<String, Double> avgPerStudent = exams.stream()
            .collect(Collectors.groupingBy(
                Exam::getStudent,
                Collectors.averagingInt(Exam::getGrade)
            ));
        avgPerStudent.forEach((name, avg) ->
            System.out.printf("%s: %.2f%n", name, avg)
        );

        // 2. Student with highest average
        System.out.println("\n=== Best student ===");
        avgPerStudent.entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .ifPresent(e ->
                System.out.printf("%s with average %.2f%n", e.getKey(), e.getValue())
            );

        // 3. Grade distribution
        System.out.println("\n=== Grade distribution ===");
        Map<String, Long> distribution = exams.stream()
            .collect(Collectors.groupingBy(
                e -> {
                    int g = e.getGrade();
                    if (g >= 90) return "Excellent (90-100)";
                    if (g >= 80) return "Good (80-89)";
                    return "Passing (60-79)";
                },
                Collectors.counting()
            ));
        distribution.forEach((tier, count) ->
            System.out.println(tier + ": " + count)
        );

        // 4. Exams per subject with student list
        System.out.println("\n=== Students per subject ===");
        Map<String, String> studentsPerSubject = exams.stream()
            .collect(Collectors.groupingBy(
                Exam::getSubject,
                Collectors.mapping(
                    Exam::getStudent,
                    Collectors.joining(", ")
                )
            ));
        studentsPerSubject.forEach((subject, studentList) ->
            System.out.println(subject + ": " + studentList)
        );

        // 5. Top 3 grades
        System.out.println("\n=== Top 3 grades ===");
        exams.stream()
            .sorted(Comparator.comparing(Exam::getGrade).reversed())
            .limit(3)
            .forEach(e -> System.out.printf("%s - %s: %d%n",
                e.getStudent(), e.getSubject(), e.getGrade()));

        // 6. Students with all exams >= 90
        System.out.println("\n=== Excellent students ===");
        Map<String, Boolean> allExcellent = exams.stream()
            .collect(Collectors.groupingBy(
                Exam::getStudent,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    list -> list.stream().allMatch(e -> e.getGrade() >= 90)
                )
            ));
        allExcellent.entrySet().stream()
            .filter(Map.Entry::getValue)
            .map(Map.Entry::getKey)
            .forEach(System.out::println);

        // 7. Complete statistics
        System.out.println("\n=== Statistics ===");
        IntSummaryStatistics stats = exams.stream()
            .mapToInt(Exam::getGrade)
            .summaryStatistics();
        System.out.println("Total exams: " + stats.getCount());
        System.out.println("Average: " + String.format("%.2f", stats.getAverage()));
        System.out.println("Min: " + stats.getMin());
        System.out.println("Max: " + stats.getMax());
    }
}

class Exam {
    private String student;
    private String subject;
    private int grade;
    private int year;

    public Exam(String student, String subject, int grade, int year) {
        this.student = student;
        this.subject = subject;
        this.grade = grade;
        this.year = year;
    }

    public String getStudent() { return student; }
    public String getSubject() { return subject; }
    public int getGrade() { return grade; }
    public int getYear() { return year; }
}

Conclusion

Functional programming in Java makes code more concise and readable. Lambda, Stream and Optional are essential tools for modern development.

Key Points to Remember

  • Lambda: Anonymous functions for concise code
  • Method Reference: Abbreviated syntax (Class::method)
  • Stream: Declarative pipelines for data processing
  • Collectors: Grouping, partitioning, statistics
  • Optional: Safe handling of nullable values
  • Lazy evaluation: Streams don't process until needed

In the next article we'll cover concurrency and multithreading: Thread, ExecutorService and synchronization.

Frequently asked questions

What is a lambda expression in Java?

A lambda is an anonymous function that can be passed as a parameter or assigned to a variable.

What is the purpose of the Stream API in Java?

The Stream API allows processing sequences of elements declaratively, with support for parallel operations.

What is Optional in Java used for?

Optional is a container that may or may not contain a value, avoiding null pointer exceptions.

How does functional programming make code more readable and concise in Java?

Functional programming makes code more readable and concise through the use of lambda expressions, the Stream API, and other tools.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects