Table of Contents
- 2.1 Introduction and course agenda
- 2.2 Prerequisites and learner profile
- 2.3 Define types of Lambda Expressions
- 2.4 Define Functional Interfaces
- 2.5 The
@FunctionalInterfaceannotation - 2.6 Demo: write a
Consumerto display a list - 2.7 Demo: use a
Predicateto filter a list - 2.8 Demo: write a
Supplierand invoke its method - 2.9 Demo: use a
Functionto map objects - 2.10 The package
java.util.function - 2.11 Are lambdas objects?
- 2.12 Demo: capturing external values in a lambda
- 2.13 Demo: use non-denotable types to create immutable wrappers
- 2.14 Module Summary
- 3.1 Introduction and module agenda
- 3.2 Demo: design the OR operation on the
Predicate - 3.3 Demo: implement the OR operation on the
Predicate - 3.4 Demo: design and implement the AND operation on the
Predicate - 3.5 Demo: protect lambdas against errors
- 3.6 Demo: design the NOT operator on the
Predicate - 3.7 Demo: analyze the
Predicateinterface of the JDK - 3.8 Demo: chain
Functionof the same type - 3.9 Demo: chaining
Functionof different types in the right order - 3.10 Demo: compose
Function - 3.11 Demo: analyze the
Functioninterface of the JDK - 3.12 Demo: fail fast when chaining lambdas
- 3.13 Summary on the
defaultandstaticmethods with lambdas - 3.14 Demo: use
forEachwithIterableandMap - 3.15 Demo: chaining
ConsumerandBiConsumer - 3.16 Module Summary
- 4.1 Introduction and module agenda
- 4.2 Write your first Method Reference
- 4.3 Calling constructors and methods with Method References
- 4.4 The four types of Method References
- 4.5 Demo: writing Method References efficiently with the IDE
- 4.6 Demo: building tables with Method References
- 4.7 Demo: create Method References on its own interfaces
- 4.8 Module Summary
- 5.1 Introduction and module agenda
- 5.2 Comparing objects with
ComparableandComparator - 5.3 Demo: write a simple first
Comparator - 5.4 Demo: create a generic
Comparatorfactory - 5.5 Demo: chaining
Comparatorswithdefaultmethods - 5.6 Demo: refactoring
Comparatorchaining withFunction - 5.7 Demo: create a natural order
Comparator(naturalOrder) - 5.8 Demo: reverse an existing
Comparator(reverse) - 5.9 Demo: protect
Comparatorsagainst null values - 5.10 Demo: analyze the
Comparatorinterface of the JDK - 5.11 Module Summary
- 6.1 Introduction and module agenda
- 6.2 Demo: read the example file line by line
- 6.3 Demo: analyze data structure and map them to
Record - 6.4 Demo: handle corrupt lines and bad formats
- 6.5 Demo: refactoring the analysis of a line
- 6.6 Demo: using
Recordand lambdas to make the code more readable - 6.7 Demo: get the company with the highest income
- 6.8 Demo: get the company with the highest cumulative income
- 6.9 Demo: make code meaningful with
Record - 6.10 Demo: improve readability with factory methods
- 6.11 Module and course summary
- 7.1 Module 2 — Writing Lambda Expressions
- 7.2 Module 3 — Composing and Chaining Lambda Expressions
- 7.3 Module 4 — Method References
- 7.4 Module 5 — Comparators
- 7.5 Module 6 — Text file parser
1. Course Overview
This course, Lambda Expressions in Java, teaches you in less than 3 hours how to write lambda expressions and how to use them in your Java applications. Here are the main steps covered:
- Write lambdas in any situation, thanks to a simple and universal step-by-step method.
- Chain and compose lambdas to create new ones.
- Write lambdas as method references to improve code readability.
- Two concrete use cases:
- The JDK
ComparatorAPI: chaining, composition, factory methods, method references, error handling and performance. - An application for analyzing data from a CSV file: composition, factory methods and error management.
The objective is to allow you to write clean, robust and efficient code using lambda expressions in Java.
2. Writing Lambda Expressions with Functional Interfaces
2.1 Introduction and course agenda
This course starts from zero — writing simple lambdas — and goes all the way to the design of real APIs, complex factory methods, chaining and composition. You will learn:
- What functional interfaces are and why they are essential to lambdas.
- How to chain and compose lambdas.
- How to improve readability using method references (and how to avoid their pitfalls).
- Two use cases: the design of APIs based on lambdas (
ComparatorAPI) and the analysis of a CSV file with the Stream API.
At the end of this course, you will be able to use lambdas effectively, following best practices to improve the quality of your code.
2.2 Prerequisites and learner profile
This course requires:
- Basic knowledge of Java: write a simple application with a
mainmethod and a few classes. - Knowledge of the Collections Framework and the Stream API (sufficient basic level).
- Java 8 minimum (lambdas added in 2014 with JDK 8). The course uses Java 21, but most examples work with older versions.
- Any IDE: IntelliJ (Free Community Edition), NetBeans, Eclipse, VS Code, etc.
2.3 Define types of Lambda Expressions
Java is a statically typed language: each variable has a known type at compile time, and this type always has a name. There are no exceptions to this rule.
Therefore:
- lambda expressions have a type in Java, known at compile time.
- This type has a name — it is always a functional interface.
- A lambda expression is an implementation of a functional interface.
Definition: A lambda expression is an implementation of a functional interface.
2.4 Define Functional Interfaces
A functional interface is an interface with a single abstract method.
Important rules to know:
defaultmethods (instance methods with implementation) do not count as abstract methods.staticmethods cannot be abstracted, so don’t count either.- Abstract methods inherited from class
Object(likeequals) do not count.
Concrete example: The JDK
Comparatorinterface is a functional interface. However, it declares theequalsmethod inherited fromObject, to clarify its semantics in its Javadoc (what does equality between two comparators mean?). This does not make it non-functional.
2.5 The @FunctionalInterface annotation
The @FunctionalInterface annotation can be added on an interface to indicate to the compiler your intention. The compiler will then check that the interface is functional and report an error if it is not.
Key points:
- This annotation is not required. The definition of a functional interface does not depend on its presence.
- It is absent for backwards compatibility reasons: interfaces written before Java 8 can become functional without modification, and be implemented with lambdas.
- If you put it on a class you will always get a compilation error (a functional interface must be an interface).
2.6 Demo: write a Consumer to display a list
Universal method for writing a lambda:
- Identify the type of the expected parameter (eg:
Consumer<Integer>). - Navigate to the interface to find its abstract method (here:
void accept(T t)). - Copy the parameter block, replace
Twith the concrete type (hereInteger), add the->arrow, then write the body.
// Méthode forEach attend un Consumer<Integer>
// Consumer : void accept(T t)
ints.forEach((Integer i) -> System.out.println(i));
// Forme simplifiée (inférence de type, suppression des parenthèses si 1 seul param)
ints.forEach(i -> System.out.println(i));
Tip: in the “return
true” or “empty body” step, you can check that your lambda is well-formed (right parameters, right return type) before writing the real business logic.
2.7 Demo: use a Predicate to filter a list
// removeIf attend un Predicate<Integer>
// Predicate : boolean test(T t)
listOfInts.removeIf((Integer i) -> i % 2 == 1); // supprime les impairs
The step-by-step method works here too:
- Visit
removeIf→ takes aPredicate<E>. - Visit
Predicate→ abstract methodboolean test(T t). - Copy
(Integer i), write->, implement:return i % 2 == 1.
Errors detected by the compiler: if the return type does not match (return an
intinstead of aboolean) or if the number of parameters is incorrect, the compiler immediately reports the error.
2.8 Demo: write a Supplier and invoke its method
A Supplier<T> takes no arguments and returns something like T.
// Supplier : T get()
Supplier<String> supplier = () -> "one";
String s = supplier.get(); // invocation
// Les parenthèses vides sont obligatoires pour les lambdas sans paramètre
System.out.println(supplier.toString()); // on peut appeler les méthodes d'Object
Note: even if a lambda doesn’t look like an object, you can call
toString(),equals(),hashCode()on it.
2.9 Demo: use a Function to map objects
A Function<T, R> takes an argument of type T and returns an object of type R.
// Function : R apply(T t)
Function<String, String> mapper1 = s -> s.toUpperCase();
Function<String, Integer> mapper2 = s -> s.length();
Function<String, String> mapper3 = s -> {
if (s.length() > 4) {
return s.substring(0, 4);
} else {
return s + "-".repeat(4 - s.length());
}
};
strings.stream()
.map(mapper1) // met en majuscules
.forEach(s -> System.out.println(s));
The Stream API is entirely built on the use of lambda expressions.
2.10 The java.util.function package
The four fundamental functional interfaces of the JDK that you absolutely must know:
| Interface | Abstract signature | Description |
|---|---|---|
Beg<T> | T get() | No argument, returns something |
Consumer<T> | void accept(T t) | Takes an argument, returns nothing |
Predicate<T> | boolean test(T t) | Takes an argument, returns a boolean |
Function<T, R> | R apply(T t) | Takes an argument, returns something different |
Important variants:
| Interface | Description |
|---|---|
BiConsumer<T, U> | Consumer with two arguments |
BiPredicate<T, U> | Two-argument predicate |
BiFunction<T, U, R> | Two-argument function |
UnaryOperator<T> | Function that takes and returns the same type |
BinaryOperator<T> | Function which takes two parameters of the same type and returns the same type |
2.11 Are lambdas objects?
The answer is subtle:
- From a code point of view: yes, we can call
toString(),equals(),hashCode()on a lambda. Lambdas are even serializable. - From the point of view of the JVM at runtime: a lambda is implemented as a static method in the class where it is defined. The JVM sees it as a simple piece of code, which it can inline if its size allows.
Practical consequence: keep your lambdas short to obtain the best inlining performance. If you decide not to inline them, the JVM will be able to optimize them — but that’s its decision, not yours.
2.12 Demo: capturing external values in a lambda
A capturing lambda (capturing lambda) references a variable declared outside its own scope.
var strings = new ArrayList<String>();
var ints = List.of(0, 1, 2, 3, 4);
// strings est capturée dans la lambda
ints.forEach(i -> strings.add(Integer.toString(i)));
System.out.println(strings);
Fundamental rule: the captured variable must be final or actually final (effectively final). It cannot be modified after its declaration.
// ILLÉGAL - counter serait mutable (non effectively final)
int counter = 0;
ints.forEach(i -> counter++); // erreur de compilation
Why this restriction? Lambdas can be executed in a thread different from the one that created them. Allowing modification of a captured variable would introduce concurrency issues.
2.13 Demo: using non-denotable types to create immutable wrappers
When you need a mutable counter in a lambda, a trick is to use the var keyword with an anonymous class:
// La référence est effectively final, mais son contenu est mutable
var counter = new Object() { int count = 0; };
ints.forEach(i -> counter.count++);
System.out.println("counter = " + counter.count);
Explanation:
- The
countervariable is indeed final (the reference does not change). - The non-denotable type inferred by the compiler via
varexposes thecountfield of the anonymous class. - If we replace
varwithObject, the compiler no longer sees thecountfield and generates an error.
2.14 Module Summary
What you learned in this module:
- Method to write a lambda in any situation:
- Find the type of lambda.
- Find the only abstract method of this interface.
- Copy settings and provide implementation.
- What a functional interface is and the four fundamentals of the JDK:
Supplier,Consumer,Predicate,Function. - The
@FunctionalInterfaceannotation and when to use it. - The answer to the subtle question: are lambdas objects in Java?
3. Compose and Chain Lambda Expressions
3.1 Module Introduction and Agenda
The notion of function composition is fundamental in functional programming, born in the 1930s with lambda calculus (Alonzo Church) and implemented in functional languages since the 1970s. Java borrowed this idea, as it did for parameterized types (generics, Java 5).
In this module, you will learn to:
- Combine lambdas: consumers, predicates, functions (suppliers do not combine).
- Design your own functional interfaces with
defaultmethods for chaining/composition. - Use
staticmethods as factory methods on functional interfaces.
3.2 Demo: designing the OR operation on Predicate
Predicate tests objects and produces booleans. Combining two predicates with OR/AND/NOT is a natural operation.
Top-down approach: first write the desired calling code, then design the interface to make it compilable.
MyPredicate<String> isEmpty = s -> s.isEmpty();
MyPredicate<String> isCorrupted = s -> s.equals("***");
// Code d'appel désiré :
MyPredicate<String> isEmptyOrIsCorrupted = isEmpty.or(isCorrupted);
or is an instance method call on isEmpty → it must be a default method of MyPredicate (the interface is already functional, we cannot add another abstract method).
3.3 Demo: implement the OR operation on Predicate
@FunctionalInterface
public interface MyPredicate<T> {
boolean test(T t); // seule méthode abstraite
default MyPredicate<T> or(MyPredicate<T> other) {
Objects.requireNonNull(other);
// Implémenter la combinaison OR avec une lambda
return (T t) -> this.test(t) || other.test(t);
}
}
Design steps:
oris an instance method →default.- Return type is
MyPredicate<T>, parameter isMyPredicate<T>. - The implementation returns a new lambda which evaluates
this.test(t) || other.test(t). - Protection against nulls with
Objects.requireNonNull(other).
3.4 Demo: design and implement the AND operation on Predicate
Same principle as for or:
default MyPredicate<T> and(MyPredicate<T> other) {
Objects.requireNonNull(other);
return (T t) -> this.test(t) && other.test(t);
}
Usage:
MyPredicate<String> isNotEmpty = s -> !s.isEmpty();
MyPredicate<String> isNotNull = s -> s != null;
// Ordre important : isNotNull doit être évalué EN PREMIER (court-circuit)
MyPredicate<String> isNotEmptyAndIsNotNull = isNotNull.and(isNotEmpty);
Be careful of the order: if
isNotEmptyis evaluated first andsis null, aNullPointerExceptionwill be thrown. WithisNotNull.and(isNotEmpty), ifisNotNullisfalse,isNotEmptyis never evaluated (short circuit&&).
3.5 Demo: protect lambdas against errors
Why use Objects.requireNonNull(other) during construction?
// Sans protection :
MyPredicate<String> corruptedPredicate = isEmpty.or(null);
// Aucune exception ici — la lambda est créée mais corrompue
// L'exception survient PLUS TARD, lors de l'appel :
boolean b2 = corruptedPredicate.test("one"); // NullPointerException ici
With the requireNonNull protection, the NullPointerException is thrown immediately, when the combined lambda is created. This is the fail-fast principle: it is better to fail early with a clear message rather than late with unpredictable behavior.
3.6 Demo: designing the NOT operator on Predicate
default MyPredicate<T> not() {
// Pas de paramètre, retourne MyPredicate<T>
return (T t) -> !this.test(t);
}
Usage:
MyPredicate<String> isEmpty = s -> s.isEmpty();
MyPredicate<String> isNotEmpty = isEmpty.not(); // plus lisible !
Key point:
defaultmethods allow you to create readable combinations.isEmpty.not()is more explicit thans -> !s.isEmpty()for a reader of the code.
3.7 Demo: analyze the JDK Predicate interface
The JDK Predicate<T> interface is exactly what we built:
| Method | Type | Designed equivalent |
|---|---|---|
boolean test(T t) | abstract | test |
Predicate<T> and(Predicate<? super T> other) | default | and |
Predicate<T> negate() | default | not |
Predicate<T> or(Predicate<? super T> other) | default | or |
static <T> Predicate<T> isEqual(Object targetRef) | static | factory methods |
static <T> Predicate<T> not(Predicate<? super T> target) | static (Java 11) | factory method of negation |
JDK generics use wildcards (
? super T) for greater flexibility, unlike our simplified version.
3.8 Demo: chain Function of the same type
Function can be chained (one after the other) or composed (in the other direction).
MyFunction<String, String> toUpperCase = s -> s.toUpperCase();
MyFunction<String, String> pad = s -> {
if (s.length() > 4) return s.substring(0, 4);
else return s + "-".repeat(4 - s.length());
};
// Chaînage : toUpperCase PUIS pad
MyFunction<String, String> chain = toUpperCase.andThen(pad);
Implementation of andThen:
default <S> MyFunction<T, S> andThen(MyFunction<R, S> other) {
return (T t) -> {
var r = this.apply(t); // applique this en premier
var s = other.apply(r); // applique other sur le résultat
return s;
};
}
3.9 Demo: chain Function of different types in the correct order
The return type of andThen must be generic to allow chaining of functions of different types:
// <S> est déclaré localement à la méthode default
default <S> MyFunction<T, S> andThen(MyFunction<R, S> other) { ... }
Type compatibility:
MyFunction<String, String> toUpperCase // String → String
MyFunction<String, Integer> toLength // String → Integer
MyFunction<String, Integer> chain2 = toUpperCase.andThen(toLength); // OK
// MyFunction<?, ?> chain3 = toLength.andThen(toUpperCase); // ERREUR : Integer ≠ String
Rule: the output type of the first function must match the input type of the second.
3.10 Demo: compose Function
Composition vs. chaining:
- Chaining (
andThen): appliesthisthenother. - Composition (
compose): appliesotherthenthis.
// chain = toUpperCase.andThen(toLength) → applique d'abord toUpperCase
// composed = toLength.compose(toUpperCase) → applique d'abord toUpperCase
MyFunction<String, Integer> composed = toLength.compose(toUpperCase);
// Équivalent à : s -> toLength.apply(toUpperCase.apply(s))
Implementation of compose:
default <S> MyFunction<S, R> compose(MyFunction<S, T> other) {
return (S s) -> {
var t = other.apply(s); // applique other en premier
var r = this.apply(t); // applique this sur le résultat
return r;
};
}
Subtle distinction:
f.andThen(g)= “firstf, theng”.f.compose(g)= “firstg, thenf”.
3.11 Demo: analyze the JDK Function interface
The JDK Function<T, R> interface contains:
| Method | Type | Description |
|---|---|---|
R apply(T t) | abstract | The only abstract method |
<V> Function<T, V> andThen(Function<? super R, ? extends V> after) | default | String with null-check |
<V> Function<V, R> compose(Function<? super V, ? extends T> before) | default | Compose with null-check |
static <T> Function<T, T> identity() | static | Returns the identity function |
The
identity()method is extremely useful: it returns aFunctionwhich returns its argument as is.
3.12 Demo: Fail fast when chaining lambdas
Problem demonstration:
// Sans requireNonNull dans la méthode or :
MyPredicate<String> isEmptyOrIsCorrupted = isEmpty.or(null);
// Aucune exception à la création !
System.out.println("Lambda created"); // s'affiche normalement
// L'exception survient lors de l'évaluation sur certaines valeurs :
boolean b1 = isEmptyOrIsCorrupted.test(""); // peut fonctionner (court-circuit OR)
boolean b2 = isEmptyOrIsCorrupted.test("one"); // NullPointerException ici !
Solution: always call Objects.requireNonNull(other) at the start of each combination method.
3.13 Summary of default and static methods with lambdas
Two essential points:
-
Combination of lambdas via
defaultmethods: use the top-down approach — write the desired calling code, then implement the method. This technique is very effective. -
Staticmethods for creating named lambdas: Static factory methods allow you to create named instances of lambdas, improving code readability.
3.14 Demo: using forEach with Iterable and Map
The Iterable interface defines the forEach(Consumer<? super T> action) method. It is the super-interface of all collections:
// forEach avec une liste d'entiers
List<Integer> ints = List.of(1, 2, 3);
ints.forEach(t -> System.out.println(t));
// forEach avec une Map (BiConsumer)
Map<Integer, String> map = Map.of(1, "one", 2, "two", 3, "three");
map.forEach((key, value) -> System.out.println(key + "::" + value));
3.15 Demo: chaining Consumer and BiConsumer
@FunctionalInterface
public interface MyConsumer<T> {
void accept(T t);
default MyConsumer<T> andThen(MyConsumer<T> other) {
return (T t) -> {
this.accept(t);
other.accept(t);
};
}
}
Usage:
MyConsumer<String> print1 = s -> System.out.println("C1: " + s);
MyConsumer<String> print2 = s -> System.out.println("C2: " + s);
MyConsumer<String> print1AndThenPrint2 = print1.andThen(print2);
print1AndThenPrint2.accept("ONE");
// Affiche :
// C1: ONE
// C2: ONE
Note: a chained
Consumercalls both consumers on the same object — this is different from a chainedFunctionwhere the output of one is the input of the other.
3.16 Module Summary
What you learned in this module:
- Combine lambdas with
defaultmethods (top-down approach). - Factory methods: use
staticmethods to create named lambdas. - The three operations on
Predicate:or,and,not. - Chaining (
andThen) and composition (compose) onFunction. - The combination of
ConsumerwithandThen. - The importance of fail-fast: protect combination methods with
Objects.requireNonNull.
4. Use Method References to write Lambda Expressions
4.1 Module Introduction and Agenda
method references are not required — you can write complete applications without using a single one. But you would be missing out on a powerful tool for code readability.
Arguments to use them:
- Improved readability: main argument.
- Slight performance improvement: very marginal, does not justify their use alone.
In this module:
- What exactly are method references and how to write them.
- The four types of method references defined in the Java language.
- Demonstrations in the IDE with pitfalls to avoid.
4.2 Write your first Method Reference
Classic example:
// Lambda classique
ints.forEach(i -> System.out.println(i));
// Method reference — même chose, plus concis
ints.forEach(System.out::println);
The object::method syntax is a method reference. It points to the println method of the System.out object.
Important: a method reference is not a method pointer in the C/C++ sense. It’s simply another way of writing a lambda expression.
Equivalence:
Consumer<Integer> printer = i -> System.out.println(i);
Consumer<Integer> printerRef = System.out::println; // équivalent
4.3 Calling constructors and methods with Method References
Representative examples:
// Supplier appelant un constructeur
Supplier<List<String>> supplier = () -> new ArrayList<>();
Supplier<List<String>> supplierRef = ArrayList::new; // constructor reference
// Consumer — méthode d'instance sur un objet connu
Consumer<Integer> printer = i -> System.out.println(i);
Consumer<Integer> printerRef = System.out::println; // bound method reference
// Predicate — méthode d'instance sur le paramètre
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> isEmptyRef = String::isEmpty; // unbound method reference
// BiFunction — méthode d'instance avec argument
BiFunction<String, String, Integer> indexOf = (s, w) -> s.indexOf(w);
BiFunction<String, String, Integer> indexOfRef = String::indexOf; // unbound
// UnaryOperator — méthode statique
UnaryOperator<Double> sqrt = d -> Math.sqrt(d);
UnaryOperator<Double> sqrtRef = Math::sqrt; // static method reference
4.4 The four types of Method References
| Type | Syntax | Example | Note |
|---|---|---|---|
| Constructor reference | ClassName::new | ArrayList::new | Can be Supplier, Function, etc. depending on the context |
| Static method reference | ClassName::staticMethod | Math::sqrt | The method can take 0, 1 or more arguments |
| Bound method reference | instance::instanceMethod | System.out::println | The method is always called on the specified object |
| Unbound method reference | ClassName::instanceMethod | String::isEmpty | The object is provided as the first parameter of the lambda |
Difference bound vs unbound:
- Bound: the object on which the method is called is fixed in the method reference (e.g.
System.out). - Unbound: the object on which the method is called is provided at run time (it is the first parameter of the lambda).
Rule for understanding: to know what a method reference does, look at its type (what functional interface does it implement?).
4.5 Demo: writing Method References efficiently with the IDE
Modern IDEs (IntelliJ, Eclipse, VS Code) can automatically detect that a lambda can be replaced by a method reference and propose the conversion:
Supplier<List<String>> supplier = () -> new ArrayList<>();
// IntelliJ suggère : ArrayList::new
Supplier<List<String>> supplierRef = ArrayList::new;
ints.forEach(t -> System.out.println(t));
// IntelliJ suggère : System.out::println
ints.forEach(System.out::println);
Best practice: use the IDE suggestion, but understand the result. You can even Ctrl+click
::newto navigate to the referenced constructor.
Case with removeIf:
listOfStrings.removeIf(s -> s.isEmpty());
// → peut être écrit comme :
listOfStrings.removeIf(String::isEmpty);
Case with map in the Stream API:
strings.stream()
.map(s -> s.toUpperCase())
.forEach(System.out::println);
// → peut être écrit comme :
strings.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
4.6 Demo: building tables with Method References
The toArray(IntFunction<T[]> generator) method of the Stream API takes an IntFunction which, from the desired size, creates the array:
// Lambda classique
String[] array = listOfStrings.stream()
.filter(s -> !s.isEmpty())
.toArray(length -> new String[length]);
// Method reference (array constructor reference)
String[] array = listOfStrings.stream()
.filter(String::isEmpty)
.toArray(String[]::new);
The String[]::new syntax is a constructor reference for arrays, which takes the length as an argument.
4.7 Demo: create Method References on your own interfaces
Example with indexOf having several overloads:
// indexOf(String str) — 2 paramètres au total (l'instance + l'argument)
ToIntBiFunction<String, String> indexOf2 = String::indexOf; // OK
// indexOf(String str, int fromIndex) — 3 paramètres au total
// ToIntBiFunction ne suffit pas → créer sa propre interface
@FunctionalInterface
interface ToIntTriFunction<T, R> {
int apply(T t, R r, int index);
}
ToIntTriFunction<String, String> indexOf3 = String::indexOf; // OK avec 3 paramètres
Conclusion: when the
java.util.functionpackage does not offer a suitable interface (here, no interface for 3 arguments), you can create your own functional interface and use method references with it.
4.8 Module Summary
What you learned in this module:
- How to write method references (your IDE can do this for you).
- The four types: constructor, static, bound, unbound.
- The key to understanding a method reference: look at its type (which functional interface does it implement?).
- When to use them: when they make the code more readable. Do not use them for performance reasons (negligible gain).
5. Create Comparators with Method References
5.1 Module Introduction and Agenda
This module is a case study: understanding how the JDK Comparator API was designed. This is a remarkable example that combines:
- Default methods and factory methods.
- Lambda composition.
- Method references.
- Handling errors and special cases.
- Performance.
You will learn to design your own APIs following the same principles.
5.2 Comparing objects with Comparable and Comparator
Two ways to compare objects in Java:
1. The Comparable<T> interface — natural comparison
// Implémenter dans la classe elle-même
record User(String name, int age) implements Comparable<User> {
@Override
public int compareTo(User other) {
return this.name.compareTo(other.name());
}
}
// Utilisation
Collections.sort(users); // fonctionne car User est Comparable
2. The Comparator<T> interface — external comparison
// Créer un Comparator pour un critère différent (ex: par âge)
Comparator<User> byAge = (u1, u2) -> Integer.compare(u1.age(), u2.age());
users.sort(byAge);
Important rule: never use subtraction to compare integers. Use
Integer.compare(a, b)to avoid integer overflows (integer overflow).
Return conventions of compare / compareTo:
- Return < 0: first object < second.
- Return = 0: the two objects are equal.
- Return > 0: first object > second.
5.3 Demo: writing a first simple Comparator
// MyComparator est une functional interface personnalisée (même principe que Comparator)
MyComparator<String> stringComparator = (s1, s2) -> {
var l1 = s1.length();
var l2 = s2.length();
return Integer.compare(l1, l2); // compare par longueur
};
Problem: this interface is limited to String. To make it generic, extract the logic into a factory method.
5.4 Demo: create a generic Comparator factory
Approach: a static comparing factory method which takes a key extractor (a Function<T, R>):
static <T, R extends Comparable<R>> MyComparator<T>
comparing(Function<T, R> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (s1, s2) -> {
var l1 = keyExtractor.apply(s1);
var l2 = keyExtractor.apply(s2);
return l1.compareTo(l2);
};
}
The R extends Comparable<R> constraint: the extracted type must be comparable (which is the case for Integer, String, Double…).
Use with method references:
MyComparator<String> stringComparator = MyComparator.comparing(String::length);
MyComparator<User> byName = MyComparator.comparing(User::name);
MyComparator<User> byAge = MyComparator.comparing(User::age);
5.5 Demo: chaining Comparators with default methods
To sort by age then by name in case of a tie:
MyComparator<User> userByAgeThenByName =
userComparatorByAge.andThen(userComparatorByName);
Implementation of andThen:
default MyComparator<T> andThen(MyComparator<T> comparator) {
Objects.requireNonNull(comparator);
return (T t1, T t2) -> {
int cmp = this.compare(t1, t2);
if (cmp == 0) {
return comparator.compare(t1, t2); // si égaux, appliquer le second critère
} else {
return cmp;
}
};
}
5.6 Demo: refactor Comparator chaining with Function
Improved version with thenComparing which directly takes a key extractor:
MyComparator<User> userByAgeThenByName2 =
MyComparator.comparing(User::age)
.thenComparing(User::name); // plus lisible !
Implementation of thenComparing:
default <R extends Comparable<R>> MyComparator<T>
thenComparing(Function<T, R> keyExtractor) {
Objects.requireNonNull(keyExtractor);
var other = comparing(keyExtractor); // crée un comparateur à partir du key extractor
return this.andThen(other); // chaîne avec this
}
Important pattern:
thenComparingreuses the factory methodcomparingand the methodandThenalready implemented — no need for additional lambda.
5.7 Demo: create a natural order Comparator (naturalOrder)
For types that implement Comparable:
static <T extends Comparable<T>> MyComparator<T> naturalOrder() {
return Comparable::compareTo; // unbound method reference sur Comparable !
}
Usage:
MyComparator<String> cmp = MyComparator.naturalOrder();
// Utilise l'ordre naturel des String (alphabétique)
Limitation:
naturalOrder()only works for types implementingComparable<T>. SinceUseris notComparable,MyComparator.<User>naturalOrder()is a compilation error.
5.8 Demo: reverse an existing Comparator (reverse)
default MyComparator<T> reverse() {
return (T t1, T t2) -> this.compare(t2, t1); // inverser t1 et t2
}
Usage:
MyComparator<String> cmpReverse = cmp.reverse();
// Chaînage avec reverse
MyComparator<User> userCmp =
MyComparator.comparing(User::age)
.thenComparing(User::name)
.reverse();
Equivalence:
compare(t2, t1)is equivalent to-compare(t1, t2). Both approaches are correct.
5.9 Demo: protect Comparator against null values
Lists may contain null values — a NullPointerException will occur during comparison. Solution: the nullsLast factory method:
static <T> MyComparator<T> nullsLast(MyComparator<T> cmp) {
return (t1, t2) -> {
if (t1 == null && t2 == null) {
return 0;
} else if (t1 == null) {
return 1; // null est "plus grand" → va à la fin
} else if (t2 == null) {
return -1;
} else {
return cmp.compare(t1, t2);
}
};
}
Convention chosen: null values are considered to be the largest → they are found at the end of the sorted list.
MyComparator<User> userCmpNoNull = MyComparator.nullsLast(userCmp);
5.10 Demo: analyze the JDK Comparator interface
The JDK Comparator<T> interface is more complex than our simplified version (wildcards, serialization), but follows the same principles:
Static factory methods:
Comparator.comparing(Function<? super T, ? extends U> keyExtractor)— comparison via key extractor.Comparator.comparingInt/Long/Double(...)— specialized versions for primitive types (avoids boxing).Comparator.naturalOrder()— natural order.Comparator.reverseOrder()— reversed natural order.Comparator.nullsFirst/nullsLast(Comparator<? super T> comparator)— handling nulls.
Default methods:
thenComparing(Comparator<? super T> other)— chaining with another comparator.thenComparing(Function<? super T, ? extends U> keyExtractor)— chaining with a key extractor.thenComparingInt/Long/Double(...)— specialized versions.reversed()— vice versa.
Performance note: versions
comparingInt,comparingLong,comparingDoubleavoid autoboxing — concrete performance benefit for large lists.
5.11 Module Summary
This module is a case study on the JDK Comparator API. Key lessons:
- Default methods + factory methods allow you to build an easy-to-use, very expressive, and robust API.
- The simplicity of the calling code facilitates the handling of errors and special cases.
- method references combined with these patterns produce very readable code.
- These principles apply to designing your own APIs.
6. Design a text file parser with lambdas
6.1 Module Introduction and Agenda
This module is an application case study: analyze a CSV file (fortune500.csv.gz) containing company data (Fortune 500 rank, annual revenue) with lambdas and the Stream API.
Objectives:
- Read the file line by line and convert the contents into Java objects.
- Handle errors (corrupt lines, invalid formats).
- Calculate statistics on the data.
- Improve readability with records, method references and factory methods.
6.2 Demo: read the example file line by line
The file is a gzip compressed CSV (~33,000 lines). Reading structure:
private static List<String> readLines(Path csvFile) {
try (var is = Files.newInputStream(csvFile); // InputStream sur le fichier
var gzis = new GZIPInputStream(is); // décompression gzip
var isr = new InputStreamReader(gzis); // binaire → caractères
var br = new BufferedReader(isr); // pour accéder à .lines()
var stream = br.lines();) { // Stream<String> des lignes
return stream.toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Key points:
- Using try-with-resources (Java 7): all self-closing resources are freed automatically, including the
Stream. - Layered decoration:
InputStream→GZIPInputStream→InputStreamReader→BufferedReader. - The
.lines()method is set toBufferedReaderand returns aStream<String>.
6.3 Demo: analyze the data structure and map it to Record
File format:
# Year,Name,Revenue,Rank
2021,Walmart,559151,1
2021,Amazon,386064,2
2021,AmerisourceBergen,189893.9,8
Creating a Record to represent a row of data:
public record Data(int year, String name, double revenue, int rank) {
public static Data of(String line) {
var elements = line.split(",");
// # Year,Name,Revenue,Rank
int year = Integer.parseInt(elements[0]);
String name = elements[1];
double revenue = Double.parseDouble(elements[2]);
int rank = Integer.parseInt(elements[3]);
return new Data(year, name, revenue, rank);
}
}
Note: the
revenuefield isdouble(notint) because some values are decimal (e.g.189893.9).
Transformation pipeline:
List<Data> datas = lines.stream()
.filter(line -> !line.startsWith("#")) // ignorer les commentaires
.map(Data::of) // convertir chaque ligne en Data
.toList();
6.4 Demo: handle corrupt lines and bad formats
In real conditions, certain lines may be invalid (missing income, incorrect format). Solution: use flatMap to return either 1 valid element, or 0 elements in case of error:
public record Data(int year, String name, double revenue, int rank) {
public static Stream<Data> of(Line line) {
try {
int year = line.year();
String name = line.name();
double revenue = line.revenue();
int rank = line.rank();
return Stream.of(new Data(year, name, revenue, rank));
} catch (NumberFormatException e) {
return Stream.empty(); // ligne ignorée silencieusement
}
}
}
Pipeline with flatMap:
List<Data> datas = lines.stream()
.map(Line::new)
.filter(Line::isNotAComment)
.flatMap(Data::of) // flatMap : 1 ligne → 0 ou 1 Data
.toList();
Important pattern:
flatMapwith a method returningStream.of(element)orStream.empty()is an elegant way to filter and map simultaneously with error handling.
6.5 Demo: refactor the parsing of a line
Problem: the filter code !line.startsWith("#") is difficult to read — no apparent business sense.
Solution: create a Record Line to encapsulate a raw line and add business methods to it:
public record Line(String[] elements) {
// Constructeur canonique avec copie défensive
public Line {
elements = Arrays.copyOf(elements, elements.length);
}
// Constructeur de convenance depuis une String
public Line(String line) {
this(line.split(","));
}
// Getter sécurisé (copie défensive)
public String[] elements() {
return Arrays.copyOf(elements, elements.length);
}
// Méthode métier lisible
public boolean isNotAComment() {
return !elements[0].startsWith("#");
}
// Accesseurs par champ
public int year() { return Integer.parseInt(elements[0]); }
public String name() { return elements[1]; }
public double revenue() { return Double.parseDouble(elements[2]); }
public int rank() { return Integer.parseInt(elements[3]); }
}
Pipeline refactored:
List<Data> datas = lines.stream()
.map(Line::new) // String → Line (method reference : constructeur)
.filter(Line::isNotAComment) // méthode métier lisible
.flatMap(Data::of)
.toList();
Two precautions for
Recordwrapping tables:
- Defensive copy in canonical constructor:
elements = Arrays.copyOf(elements, elements.length).- Defensive copy in getter:
return Arrays.copyOf(elements, elements.length). These precautions guarantee the immutability of the record.
6.6 Demo: using Record and lambdas to make the code more readable
By integrating the analysis directly into the Record Line (a single split instead of several), we obtain cleaner and more efficient code:
// Le split est fait une seule fois dans le constructeur de convenance
public Line(String line) {
this(line.split(",")); // délègue au constructeur canonique
}
Advantage: elements[0], elements[1], etc. are accessible via the year(), name(), revenue(), rank() methods — the calling code never sees the technical details.
6.7 Demo: get the company with the highest income
// Version directe avec Comparator.comparing
var maxRevenueCompany = datas.stream()
.max(Comparator.comparing(Data::revenue))
.orElseThrow();
System.out.println("maxRevenueCompany = " + maxRevenueCompany);
// Version avec factory method sur Data (plus lisible)
var maxRevenueCompany2 = datas.stream()
.max(Data.comparingByRevenue())
.orElseThrow();
Factory method on Data:
public static Comparator<? super Data> comparingByRevenue() {
return Comparator.comparing(Data::revenue);
}
Optional.orElseThrow(): if the list is empty, throws aNoSuchElementException. UseorElse(defaultValue)if a default value makes sense.
6.8 Demo: get the company with the highest cumulative revenue
Two-step calculation:
Step 1: group by company and sum the revenues
Map<Company, TotalRevenue> totalRevenuePerCompany =
datas.stream()
.collect(Collectors.groupingBy(
Data::company, // clé : nom de la compagnie
Data.summingRevenue() // valeur : somme des revenus
));
Custom collector summingRevenue:
private static Collector<Data, Object, TotalRevenue> summingRevenue() {
return Collectors.collectingAndThen(
Collectors.summingDouble(data -> data.revenue().revenue()),
TotalRevenue::new
);
}
Step 2: extract the maximum
CompanyTotalRevenue maxTotalRevenuePerCompany =
totalRevenuePerCompany.entrySet().stream()
.map(CompanyTotalRevenue::new) // Map.Entry → CompanyTotalRevenue
.max(CompanyTotalRevenue.comparingByRevenue())
.orElseThrow();
6.9 Demo: make code meaningful with Record
Problem: Map<String, Double> says nothing about the business domain. Neither does Map.Entry<String, Double>.
Solution: create business Records for each concept:
public record Company(String name) {}
public record Revenue(double revenue) implements Comparable<Revenue> {
@Override
public int compareTo(Revenue other) {
return Double.compare(this.revenue, other.revenue);
}
}
public record TotalRevenue(double revenue) implements Comparable<TotalRevenue> {
@Override
public int compareTo(TotalRevenue other) {
return Double.compare(this.revenue, other.revenue);
}
}
public record CompanyTotalRevenue(Company company, TotalRevenue totalRevenue) {
// Constructeur depuis Map.Entry
public CompanyTotalRevenue(Map.Entry<Company, TotalRevenue> entry) {
this(entry.getKey(), entry.getValue());
}
public static Comparator<? super CompanyTotalRevenue> comparingByRevenue() {
return Comparator.comparing(CompanyTotalRevenue::totalRevenue);
}
}
Benefit: the type Map<Company, TotalRevenue> is self-documenting. The code becomes a business narrative.
6.10 Demo: improve readability with factory methods
Before refactoring (unreadable):
Map<Company, TotalRevenue> totalRevenuePerCompany =
datas.stream()
.collect(Collectors.groupingBy(
data -> data.company(),
Collectors.collectingAndThen(
Collectors.summingDouble(data -> data.revenue().revenue()),
TotalRevenue::new
)
));
After refactoring (readable):
Map<Company, TotalRevenue> totalRevenuePerCompany =
datas.stream()
.collect(Collectors.groupingBy(
Data::company,
Data.summingRevenue() // factory method nommée, intention claire
));
And for the final result:
// Avant : Map.Entry<Company, TotalRevenue> → difficile à lire
// Après : CompanyTotalRevenue → type métier parlant
var maxCompany = totalRevenuePerCompany.entrySet().stream()
.map(CompanyTotalRevenue::new)
.max(CompanyTotalRevenue.comparingByRevenue())
.orElseThrow();
General principle: hide technical code in named methods (factory methods, static methods). Leave only operations with business value in the processing code.
6.11 Module and course summary
Module Summary:
This module covers the use of lambdas with two JDK APIs transformed by Java 8:
- Collections Framework (Java 2, 1998):
forEach,removeIf, sorting withComparator, etc. - Stream API (Java 8, 2014):
filter,map,flatMap,collect,max, etc.
Techniques used:
- Reading a compressed CSV file with
BufferedReader.lines()and try-with-resources. - Mapping and filtering with the Stream API and lambdas/method references.
- Error handling with
flatMapandStream.empty(). - Grouping and aggregation with
Collectors.groupingByandCollectors.collectingAndThen. - Improved readability with
Record, factory methods and method references.
Course Summary:
Lambdas are the second major contribution of functional programming to the Java platform (after generics in Java 5, 2004). They improve the expressivity and readability of the code.
What you learned:
- Write lambdas with the universal method: identify the type → find the abstract method → copy the parameters → implement.
- The fundamental functional interfaces:
Supplier,Consumer,Predicate,Function. - Compose and chain lambdas with
defaultmethods. - Method references: use them when they improve readability, not for performance.
- Two case studies: the
ComparatorAPI and the analysis of a CSV file.
7. Demo source code
7.1 Module 2 — Writing Lambda Expressions
Maven project: 02_Writing-Lambda-Expressions (Java 21)
A_WritingFirstLambdas.java
package org.paumard;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class A_WritingFirstLambdas {
public static void main(String[] args) {
var ints = List.of(0, 1, 2, 3, 5, 6, 7, 8, 9);
var listOfInts = new ArrayList<>(ints);
System.out.println("Ints");
// ints.forEach(i -> System.out.println(i));
listOfInts.removeIf(i -> i % 2 == 1);
System.out.println("Filtered ints");
// listOfInts.forEach(i -> System.out.println(i));
// T get();
Supplier<String> supplier = () -> "one";
var s = supplier.get();
System.out.println("s = " + s);
var toString = supplier.toString();
System.out.println("toString = " + toString);
}
}
Concepts illustrated: writing a lambda for forEach (Consumer), writing a lambda for removeIf (Predicate), creating a Supplier, invoking toString() on a lambda.
B_Function.java
package org.paumard;
import java.util.List;
import java.util.function.Function;
public class B_Function {
public static void main(String[] args) {
var strings = List.of("one", "two", "three", "four", "five");
Function<String, String> mapper1 = s -> s.toUpperCase();
Function<String, Integer> mapper2 = s -> s.length();
Function<String, String> mapper3 = s -> {
if (s.length() > 4) {
return s.substring(0, 4);
} else {
return s + "-".repeat(4 - s.length());
}
};
strings.stream()
.map(mapper3)
.forEach(s -> System.out.println(s));
}
}
Concepts illustrated: three types of Function (String→String, String→Integer, String→String with multiple lines), use in the Stream API with .map().
C_Capturing.java
package org.paumard;
import java.util.ArrayList;
import java.util.List;
public class C_Capturing {
public static void main(String[] args) {
var strings = new ArrayList<String>();
var ints = List.of(0, 1, 2, 3, 4);
}
}
Concepts illustrated: basis for demonstrating capturing lambdas — capturing an external variable strings in a lambda iterating over ints.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.paumard</groupId>
<artifactId>02_Writing-Lambda-Expressions</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
7.2 Module 3 — Composing and Chaining Lambda Expressions
Maven project: 03_Composing-and-Chaining
MyPredicate.java
package org.paumard;
import java.util.Objects;
@FunctionalInterface
public interface MyPredicate<T> {
boolean test(T t);
default MyPredicate<T> or(MyPredicate<T> other) {
Objects.requireNonNull(other);
return (T t) -> this.test(t) || other.test(t);
}
default MyPredicate<T> and(MyPredicate<T> other) {
Objects.requireNonNull(other);
return (T t) -> this.test(t) && other.test(t);
}
default MyPredicate<T> not() {
return (T t) -> !this.test(t);
}
}
Concepts illustrated: complete definition of a custom functional interface with or, and, not combinations. Fail-fast protection with Objects.requireNonNull.
MyFunction.java
package org.paumard;
@FunctionalInterface
public interface MyFunction<T, R> {
R apply(T t);
default <S> MyFunction<T, S> andThen(MyFunction<R, S> other) {
return (T t) -> {
var r = this.apply(t);
var s = other.apply(r);
return s;
};
}
default <S> MyFunction<S, R> compose(MyFunction<S, T> other) {
return (S s) -> {
var t = other.apply(s);
var r = this.apply(t);
return r;
};
}
}
Concepts illustrated: chaining (andThen) and composition (compose) with local generic types <S>. Distinction between the two operations.
MyConsumer.java
package org.paumard;
@FunctionalInterface
public interface MyConsumer<T> {
void accept(T t);
default MyConsumer<T> andThen(MyConsumer<T> other) {
return (T t) -> {
this.accept(t);
other.accept(t);
};
}
}
Concepts illustrated: consumer chaining — both consumers receive the same argument.
A_Or_MyPredicate.java
package org.paumard;
import java.util.ArrayList;
import java.util.List;
public class A_Or_MyPredicate {
public static void main(String[] args) {
var strings =
List.of("one", "", "two", "***", "three",
"", "four", "five", "***");
var listOfStrings = new ArrayList<>(strings);
MyPredicate<String> isEmpty = s -> s.isEmpty();
MyPredicate<String> isCorrupted = s -> s.equals("***");
MyPredicate<String> isEmptyOrIsCorrupted = isEmpty.or(isCorrupted);
strings.forEach(
s -> System.out.println(s + ": " + isEmptyOrIsCorrupted.test(s))
);
}
}
B_And_MyPredicate.java
package org.paumard;
import java.util.ArrayList;
import java.util.List;
public class B_And_MyPredicate {
public static void main(String[] args) {
var strings =
List.of("one", "", "two", "", "three",
"four", "five");
var listOfStrings = new ArrayList<>(strings);
listOfStrings.add(null);
listOfStrings.add(null);
MyPredicate<String> isNotEmpty = s -> !s.isEmpty();
MyPredicate<String> isNotNull = s -> s != null;
MyPredicate<String> isNotEmptyAndIsNotNull =
isNotNull.and(isNotEmpty); // isNotNull EN PREMIER (court-circuit)
listOfStrings.forEach(x -> System.out.println(x + ": " +
isNotEmptyAndIsNotNull.test(x)));
}
}
C_Not_MyPredicate.java
package org.paumard;
import java.util.ArrayList;
import java.util.List;
public class C_Not_MyPredicate {
public static void main(String[] args) {
var strings =
List.of("one", "", "two", "", "three",
"four", "five");
var listOfStrings = new ArrayList<>(strings);
MyPredicate<String> isEmpty = s -> s.isEmpty();
MyPredicate<String> isNotEmpty = isEmpty.not();
listOfStrings.forEach(x -> System.out.println(x + ": " +
isNotEmpty.test(x)));
}
}
D_Chain_MyFunction.java
package org.paumard;
import java.util.List;
public class D_Chain_MyFunction {
public static void main(String[] args) {
var strings = List.of("one", "two", "three", "four", "five");
MyFunction<String, String> toUpperCase = s -> s.toUpperCase();
MyFunction<String, Integer> toLength = s -> s.length();
MyFunction<String, String> pad = s -> {
if (s.length() > 4) return s.substring(0, 4);
else return s + "-".repeat(4 - s.length());
};
MyFunction<String, String> chain1 = toUpperCase.andThen(pad);
MyFunction<String, Integer> chain2 = toUpperCase.andThen(toLength);
MyFunction<String, Integer> composed = toLength.compose(toUpperCase);
strings.forEach(s -> System.out.println(s + ": " + composed.apply(s)));
}
}
E_Failing_NPE.java
package org.paumard;
public class E_Failing_NPE {
public static void main(String[] args) {
MyPredicate<String> isEmpty = s -> s.isEmpty();
MyPredicate<String> isCorrupted = s -> s.equals("***");
MyPredicate<String> isEmptyOrIsCorrupted = isEmpty.or(null);
// Sans requireNonNull : aucune exception ici !
boolean b1 = isEmptyOrIsCorrupted.test(""); // peut fonctionner
System.out.println("b1 = " + b1);
boolean b2 = isEmptyOrIsCorrupted.test("one"); // NullPointerException ici
System.out.println("b2 = " + b2);
System.out.println("Lambda created");
}
}
Concepts illustrated: demonstrating behavior without requireNonNull — NPE occurs late, making debugging difficult.
F_AndThen_MyConsumer.java
package org.paumard;
import java.util.List;
import java.util.Map;
public class F_AndThen_MyConsumer {
public static void main(String[] args) {
var ints = List.of(1, 2, 3);
ints.forEach(t -> System.out.println(t));
var map = Map.of(1, "one", 2, "two", 3, "three");
map.forEach((key, value) ->
System.out.println(key + "::" + value));
MyConsumer<String> print1 = s -> System.out.println("C1: " + s);
MyConsumer<String> print2 = s -> System.out.println("C2: " + s);
print1.accept("One");
print2.accept("Two");
MyConsumer<String> print1AndThenPrint2 = print1.andThen(print2);
print1AndThenPrint2.accept("ONE");
}
}
7.3 Module 4 — Method References
Maven project: 04_Using-Method-References
A_MethodReferences.java
package org.paumard;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class A_MethodReferences {
public static void main(String[] args) {
var ints = List.of(1, 2, 3, 4);
var strings = List.of("", "one", "", "two", "three", "");
var listOfStrings = new ArrayList<>(strings);
// Constructor reference
Supplier<List<String>> supplier = ArrayList::new;
// Bound method reference (System.out est fixé)
ints.forEach(System.out::println);
// Unbound method reference (l'objet est le paramètre)
listOfStrings.removeIf(String::isEmpty);
// Unbound method reference dans map()
listOfStrings.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
// Static method reference
ints.stream()
.map(Math::sqrt)
.forEach(System.out::println);
// Array constructor reference
String[] array = listOfStrings.stream()
.filter(String::isEmpty)
.toArray(String[]::new);
}
}
B_Caveats_MethodReferences.java
package org.paumard;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class B_Caveats_MethodReferences {
public static void main(String[] args) {
var strings = List.of(
"one", "two", "three", "four",
"five", "six", "seven", "eight",
"nine", "ten");
Map<Integer, List<String>> map = new HashMap<>();
strings.forEach(
s -> map.computeIfAbsent(
s.length(),
key -> new ArrayList<>()
).add(s)
);
map.forEach((key, value) -> System.out.println(key + " ::" + value));
}
}
Concepts illustrated: case where a lambda with several nested operations cannot be simplified to method reference — importance of understanding the limits.
C_Custom_MethodReference.java
package org.paumard;
import java.util.function.ToIntBiFunction;
public class C_Custom_MethodReference {
@FunctionalInterface
interface ToIntTriFunction<T, R> {
int apply(T t, R r, int index);
}
public static void main(String[] args) {
// Unbound method reference : indexOf(String) → 2 paramètres effectifs
ToIntBiFunction<String, String> indexOf2 = String::indexOf;
// Unbound method reference : indexOf(String, int) → 3 paramètres effectifs
ToIntTriFunction<String, String> indexOf3 = String::indexOf;
}
}
Concepts illustrated: creating a custom functional interface ToIntTriFunction for 3-parameter method references, not covered by java.util.function.
7.4 Module 5 — Comparators
Maven project: 05_Creating-Comparators
User.java
package org.paumard;
record User(String name, int age) {
}
MyComparator.java
package org.paumard;
import java.util.Objects;
import java.util.function.Function;
@FunctionalInterface
public interface MyComparator<T> {
int compare(T t1, T t2); // seule méthode abstraite
// Default method : inversion de l'ordre
default MyComparator<T> reverse() {
return (T t1, T t2) -> this.compare(t2, t1);
}
// Default method : chaînage avec un key extractor
default <R extends Comparable<R>> MyComparator<T>
thenComparing(Function<T, R> keyExtractor) {
Objects.requireNonNull(keyExtractor);
var other = comparing(keyExtractor);
return this.andThen(other);
}
// Default method : chaînage avec un autre comparateur
default MyComparator<T> andThen(MyComparator<T> comparator) {
Objects.requireNonNull(comparator);
return (T t1, T t2) -> {
int cmp = this.compare(t1, t2);
if (cmp == 0) {
return comparator.compare(t1, t2);
} else {
return cmp;
}
};
}
// Static factory method : gestion des valeurs null (nulls à la fin)
static <T> MyComparator<T> nullsLast(MyComparator<T> cmp) {
return (t1, t2) -> {
if (t1 == null && t2 == null) return 0;
else if (t1 == null) return 1;
else if (t2 == null) return -1;
else return cmp.compare(t1, t2);
};
}
// Static factory method : ordre naturel (pour les Comparable)
static <T extends Comparable<T>> MyComparator<T> naturalOrder() {
return Comparable::compareTo;
}
// Static factory method : comparaison via key extractor
static <T, R extends Comparable<R>> MyComparator<T>
comparing(Function<T, R> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (s1, s2) -> {
var l1 = keyExtractor.apply(s1);
var l2 = keyExtractor.apply(s2);
return l1.compareTo(l2);
};
}
}
A_CreatingComparators.java
package org.paumard;
import java.util.List;
public class A_CreatingComparators {
public static void main(String[] args) {
var strings = List.of("one", "two", "three", "four", "five");
// Comparateur via key extractor (method reference)
MyComparator<String> stringComparator =
MyComparator.comparing(String::length);
MyComparator<User> userComparatorByName =
MyComparator.comparing(User::name);
MyComparator<User> userComparatorByAge =
MyComparator.comparing(User::age);
// Chaînage version 1 : andThen avec deux comparateurs
MyComparator<User> userByAgeThenByName =
userComparatorByAge.andThen(userComparatorByName);
var mary = new User("Mary", 23);
var michael = new User("Michael", 23);
var barbara = new User("Barbara", 22);
int i = userByAgeThenByName.compare(barbara, michael);
System.out.println("i = " + i);
// Chaînage version 2 : thenComparing (plus fluide)
MyComparator<User> userByAgeThenByName2 =
MyComparator.comparing(User::age)
.thenComparing(User::name);
int j = userByAgeThenByName.compare(barbara, michael);
System.out.println("j = " + j);
}
}
B_Comparator_NaturalOrder.java
package org.paumard;
import java.util.List;
public class B_Comparator_NaturalOrder {
public static void main(String[] args) {
var strings = List.of("one", "two", "three", "four", "five");
// naturalOrder() — fonctionne pour String (Comparable)
MyComparator<String> cmp = MyComparator.naturalOrder();
MyComparator<String> cmpReverse = cmp.reverse();
// Chaînage complet avec reverse
MyComparator<User> userCmp =
MyComparator.comparing(User::age)
.thenComparing(User::name)
.reverse();
// Note : MyComparator.<User>naturalOrder() serait une erreur
// car User n'implémente pas Comparable
}
}
C_Comparator_NullValues.java
package org.paumard;
import java.util.List;
public class C_Comparator_NullValues {
public static void main(String[] args) {
var strings = List.of("one", "two", "three", "four", "five");
MyComparator<User> userCmp =
MyComparator.comparing(User::age)
.thenComparing(User::name)
.reverse();
// Protection contre les valeurs null
MyComparator<User> userCmpNoNull =
MyComparator.nullsLast(userCmp);
}
}
7.5 Module 6 — Text File Parser
Maven project: 06_Designing-a-Text-File-Analyzer
A_ReadLinesAndValidate.java
package org.paumard.lambdas;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
public class A_ReadLinesAndValidate {
public record Line(String[] elements) {
// Constructeur canonique avec copie défensive (immutabilité)
public Line {
elements = Arrays.copyOf(elements, elements.length);
}
// Constructeur de convenance : parse la ligne CSV
public Line(String line) {
this(line.split(","));
}
// Getter avec copie défensive
public String[] elements() {
return Arrays.copyOf(elements, elements.length);
}
// Méthode métier : filtre les lignes de commentaire
public boolean isNotAComment() {
return !elements[0].startsWith("#");
}
public int year() { return Integer.parseInt(elements[0]); }
public String name() { return elements[1]; }
public double revenue() { return Double.parseDouble(elements[2]); }
public int rank() { return Integer.parseInt(elements[3]); }
}
public record Data(int year, String name, double revenue, int rank) {
// Factory method avec gestion d'erreur via Stream.empty()
public static Stream<Data> of(Line line) {
try {
return Stream.of(new Data(
line.year(), line.name(), line.revenue(), line.rank()
));
} catch (NumberFormatException e) {
return Stream.empty(); // ignore les lignes corrompues
}
}
}
public static void main(String[] args) {
var csvFile = Path.of("files/fortune500.csv.gz");
var lines = readLines(csvFile);
System.out.println("# lines = " + lines.size());
lines.stream().limit(5).forEach(System.out::println);
// Pipeline complet de transformation
List<Data> datas = lines.stream()
.map(Line::new) // String → Line
.filter(Line::isNotAComment) // filtrer les commentaires
.flatMap(Data::of) // Line → Stream<Data> (0 ou 1 élément)
.toList();
System.out.println("# datas = " + datas.size());
}
private static List<String> readLines(Path csvFile) {
try (var is = Files.newInputStream(csvFile);
var gzis = new GZIPInputStream(is);
var isr = new InputStreamReader(gzis);
var br = new BufferedReader(isr);
var stream = br.lines()) {
return stream.toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
B_Analysis.java
package org.paumard.lambdas;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
import java.util.zip.GZIPInputStream;
public class B_Analysis {
// Records métier pour un code auto-documenté
public record Company(String name) {}
public record Revenue(double revenue) implements Comparable<Revenue> {
@Override public int compareTo(Revenue other) {
return Double.compare(this.revenue, other.revenue);
}
}
public record TotalRevenue(double revenue) implements Comparable<TotalRevenue> {
@Override public int compareTo(TotalRevenue other) {
return Double.compare(this.revenue, other.revenue);
}
}
public record CompanyTotalRevenue(Company company, TotalRevenue totalRevenue) {
public CompanyTotalRevenue(Map.Entry<Company, TotalRevenue> entry) {
this(entry.getKey(), entry.getValue());
}
public static Comparator<? super CompanyTotalRevenue> comparingByRevenue() {
return Comparator.comparing(CompanyTotalRevenue::totalRevenue);
}
}
public record Data(int year, Company company, Revenue revenue, int rank) {
public static Stream<Data> of(Line line) {
try {
return Stream.of(new Data(
line.year(),
new Company(line.name()),
new Revenue(line.revenue()),
line.rank()
));
} catch (NumberFormatException e) {
return Stream.empty();
}
}
public static Comparator<? super Data> comparingByRevenue() {
return Comparator.comparing(Data::revenue);
}
// Collector personnalisé : somme les revenus en TotalRevenue
private static Collector<Data, Object, TotalRevenue> summingRevenue() {
return Collectors.collectingAndThen(
Collectors.summingDouble(data -> data.revenue().revenue()),
TotalRevenue::new
);
}
}
// ... Line record identique à A_ReadLinesAndValidate
public static void main(String[] args) {
var csvFile = Path.of("files/fortune500.csv.gz");
var lines = readLines(csvFile);
List<Data> datas = lines.stream()
.map(Line::new).filter(Line::isNotAComment)
.flatMap(Data::of).toList();
// Compagnie avec le revenu le plus élevé
var maxRevenueCompany = datas.stream()
.max(Data.comparingByRevenue())
.orElseThrow();
System.out.println("maxRevenueCompany = " + maxRevenueCompany);
// Compagnie avec le revenu cumulé le plus élevé
Map<Company, TotalRevenue> totalRevenuePerCompany =
datas.stream()
.collect(Collectors.groupingBy(
Data::company,
Data.summingRevenue()
));
CompanyTotalRevenue maxTotalRevenuePerCompany =
totalRevenuePerCompany.entrySet().stream()
.map(CompanyTotalRevenue::new)
.max(CompanyTotalRevenue.comparingByRevenue())
.orElseThrow();
System.out.println("maxTotalRevenuePerCompany = " + maxTotalRevenuePerCompany);
}
private static List<String> readLines(Path csvFile) {
try (var is = Files.newInputStream(csvFile);
var gzis = new GZIPInputStream(is);
var isr = new InputStreamReader(gzis);
var br = new BufferedReader(isr);
var stream = br.lines()) {
return stream.toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
C_BetterAnalysis.java
Improved version with an enriched object model: all primitive fields are encapsulated in dedicated records (Year, Rank, Revenue, CumulatedRevenue).
package org.paumard.lambdas;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import java.util.zip.GZIPInputStream;
public class C_BetterAnalysis {
// Modèle objet complet — chaque concept métier a son type
public record Rank(int rank) implements Comparable<Rank> {
@Override public int compareTo(Rank other) {
return Integer.compare(this.rank, other.rank);
}
}
public record Year(int year) {}
public record Revenue(double revenue) implements Comparable<Revenue> {
@Override public int compareTo(Revenue other) {
return Double.compare(this.revenue, other.revenue);
}
}
public record CumulatedRevenue(double revenue) implements Comparable<CumulatedRevenue> {
@Override public int compareTo(CumulatedRevenue other) {
return Double.compare(this.revenue, other.revenue);
}
}
public record Company(String name) {}
public record YearRevenue(Year year, Revenue revenue) {
public static YearRevenue of(Data data) {
return new YearRevenue(data.year(), data.revenue());
}
public double revenueAsDouble() { return revenue().revenue(); }
}
public record RevenuePerCompany(Company company, CumulatedRevenue revenue) {
public RevenuePerCompany(Map.Entry<Company, CumulatedRevenue> entry) {
this(entry.getKey(), entry.getValue());
}
public static Comparator<? super RevenuePerCompany> comparingByRevenue() {
return Comparator.comparing(RevenuePerCompany::revenue);
}
}
public record Data(Year year, Company company, Revenue revenue, Rank rank) {
public static Stream<Data> of(Line line) {
try {
return Stream.of(new Data(
new Year(line.year()),
new Company(line.name()),
new Revenue(line.revenue()),
new Rank(line.rank())
));
} catch (NumberFormatException e) {
return Stream.empty();
}
}
public static Comparator<? super Data> comparingByRevenue() {
return Comparator.comparing(Data::revenue);
}
public static Collector<? super Data, Object, CumulatedRevenue> summingRevenue() {
return Collectors.collectingAndThen(
Collectors.summingDouble(data -> data.revenue().revenue()),
CumulatedRevenue::new
);
}
public boolean isCompany(Company company) {
return this.company().equals(company);
}
}
// Line record identique, avec isNotAComment() basé sur equals("#")
public static void main(String[] args) {
var csvFile = Path.of("files/fortune500.csv.gz");
var lines = readLines(csvFile);
var datas = lines.stream()
.map(Line::new).filter(Line::isNotAComment)
.flatMap(Data::of).toList();
// Compagnie avec le revenu le plus élevé
var maxRevenueCompany = datas.stream()
.max(Data.comparingByRevenue()).orElseThrow();
System.out.println("Company with the max revenue = " + maxRevenueCompany);
// Compagnie avec le revenu cumulé le plus élevé
Map<Company, CumulatedRevenue> cumulatedRevenuePerCompany =
datas.stream()
.collect(Collectors.groupingBy(
Data::company,
Data.summingRevenue()
));
var maxCumulatedRevenueCompany =
cumulatedRevenuePerCompany.entrySet().stream()
.map(RevenuePerCompany::new)
.max(RevenuePerCompany.comparingByRevenue())
.orElseThrow();
System.out.println("Company with the max cumulated revenue = " + maxCumulatedRevenueCompany);
}
private static List<String> readLines(Path csvFile) {
try (var is = Files.newInputStream(csvFile);
var gzis = new GZIPInputStream(is);
var isr = new InputStreamReader(gzis);
var br = new BufferedReader(isr);
var stream = br.lines()) {
return stream.toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Training carried out with Java 21 — Course led by Jose Paumard, Java Champion and JavaOne Rock Star.
Search Terms
lambda · expressions · java · backend · architecture · full-stack · web · method · comparator · references · function · lambdas · predicate · agenda · chaining · analyze · methods · types · write · writing · chain · comparators · interface · interfaces