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.
// 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 |
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
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.
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
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
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
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.
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
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.







