📝 Introduction : au-delà des limites du JDK
Si vous êtes arrivé ici sans avoir lu les précédentes parties de notre série sur la programmation fonctionnelle en Java, je vous recommande de consulter dans un premier temps :
Dans cet article, nous allons explorer Vavr, une librairie Java sans dépendance transitive, qui introduit des concepts avancés de programmation fonctionnelle. Vavr comble les lacunes du JDK en offrant des outils modernes comme des collections immuables, des types fonctionnels avancés et des patterns éprouvés. Vous êtes prêt ? Plongeons dans l'univers de Vavr !
🚀 Vavr : une introduction à la programmation fonctionnelle avancée
Qu'est-ce que Vavr ?
Vavr (anciennement Javaslang) est une librairie qui enrichit Java avec des fonctionnalités inspirées de langages fonctionnels comme Scala ou Kotlin. Elle propose :
- Collections immuables : des structures de données sécurisées et sans effets de bord.
- Types fonctionnels avancés : gestion d'erreurs, validation, composition de fonctions, etc.
- Interopérabilité avec Java : Vavr s'intègre parfaitement dans un projet Java existant.
Pourquoi utiliser Vavr ?
Le JDK offre des outils fonctionnels de base (comme Optional ou les Streams), mais il reste limité pour des cas d'usage avancés. Vavr comble ces lacunes en introduisant des concepts comme :
- Gestion typée des erreurs avec
Either. - Validation accumulative pour des scénarios complexes.
- Memoization pour optimiser les performances.
1. Installation et configuration
Pour intégrer Vavr dans votre projet Maven, ajoutez simplement la dépendance suivante à votre fichier pom.xml :
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.6</version>
</dependency>Configuration pom.xml
Une fois configurée, vous êtes prêt à explorer les fonctionnalités avancées de Vavr.
2. Collections immuables : sécurité et fluidité
L'une des forces de Vavr réside dans ses collections immuables. Contrairement aux collections classiques du JDK, celles de Vavr sont thread-safe et ne peuvent pas être modifiées après leur création.
Exemple : listes immuables
import io.vavr.collection.List;
List<String> list = List.of("Java", "Scala", "Kotlin");
List<String> newList = list.append("Clojure"); // Retourne une nouvelle liste
System.out.println("Original: " + list); // [Java, Scala, Kotlin]
System.out.println("Nouvelle: " + newList); // [Java, Scala, Kotlin, Clojure]Liste immuable
Exemple : Maps immuables
import io.vavr.collection.Map;
Map<String, Integer> prices = HashMap.of(
"Coffee", 3,
"Tea", 2
);
Map<String, Integer> newPrices = prices.put("Water", 1);Map immuable
Ces collections permettent également des opérations fluides comme le filtrage ou le mapping :
import io.vavr.collection.List;
// ✅ Opérations fluides
List<String> result = List.range(1, 6) // [1, 2, 3, 4, 5]
.filter(n -> n % 2 == 0) // [2, 4]
.map(n -> "Number: " + n); // [Number: 2, Number: 4]Like a Stream but is not
3. Types fonctionnels avancés
Vavr introduit des types fonctionnels puissants qui simplifient la gestion des erreurs et des exceptions.
Option : Une alternative enrichie à Optional
Option<String> some = Option.of("Hello");
Option<String> none = Option.none();
String result1 = some.getOrElse("Default"); // "Hello"
String result2 = none.getOrElse("Default"); // "Default"
// Chaînage
String result3 = Option.of(" hello ")
.map(String::trim)
.map(String::toUpperCase)
.filter(s -> s.length() > 3)
.getOrElse("Too short"); // "HELLO"
Option
Either : Gestion typée des erreurs
Either permet de représenter soit une valeur correcte (Right), soit une erreur (Left).
public Either<String, Integer> divide(int a, int b) {
if (b == 0) {
return Either.left("Division par zéro");
}
return Either.right(a / b);
}
public void eitherExample() {
String result1 = divide(10, 2)
.fold(error -> "Erreur: " + error, success -> "Résultat: " + success);
// "Résultat: 5"
String result2 = divide(10, 0)
.fold(error -> "Erreur: " + error, success -> "Résultat: " + success);
// "Erreur: Division par zéro"
}Either
Try : Gestion des exceptions
Avec Try, vous pouvez encapsuler des opérations susceptibles de lever des exceptions.
Try<Integer> result = Try.of(() -> Integer.parseInt("123"));
String message = result
.map(n -> "Nombre: " + n)
.getOrElse("Parsing échoué"); // "Nombre: 123"
// Avec exception
Try<Integer> failed = Try.of(() -> Integer.parseInt("abc"));
String errorMessage = failed
.recover(throwable -> -1)
.map(n -> "Résultat: " + n)
.get(); // "Résultat: -1"
Try
Validation : Accumulation d'erreurs
La validation est un cas d'usage courant dans les applications. Avec Vavr, vous pouvez accumuler plusieurs erreurs au lieu de vous arrêter à la première.
// ✅ Validation - Accumulation d'erreurs
public Validation<List<String>, Person> validatePerson(String name, int age, String email) {
return Validation.combine(
validateName(name),
validateAge(age),
validateEmail(email)
).map(Person::new);
}
private Validation<String, String> validateName(String name) {
return name != null && !name.trim().isEmpty()
? Validation.valid(name)
: Validation.invalid("Nom requis");
}
private Validation<String, Integer> validateAge(int age) {
return age >= 0 && age <= 120
? Validation.valid(age)
: Validation.invalid("Âge invalide");
}
private Validation<String, String> validateEmail(String email) {
return email != null && email.contains("@")
? Validation.valid(email)
: Validation.invalid("Email invalide");
}Validation
4. Composition de Fonctions : Construisez des pipelines puissants
Vavr facilite la composition de fonctions, un concept clé en programmation fonctionnelle.
Exemple : Composition avec andThen
// Function0 - fonction sans paramètre
Function0<String> getCurrentTime = () -> LocalDateTime.now().toString();
Function1<String, String> formatTime = time -> "Heure actuelle: " + time;
// Composition avec andThen
Function0<String> composedFunction = getCurrentTime.andThen(formatTime);
String result = composedFunction.get();
System.out.println(result); // "Heure actuelle: 2025-06-26T10:30:45"Function composition
Exemple pratique : Pipeline de traitement de texte
Function1<String, String> trim = String::trim;
Function1<String, String> toUpper = String::toUpperCase;
Function1<String, Integer> getLength = String::length;
// andThen - composition de gauche à droite
Function1<String, Integer> pipeline1 = trim
.andThen(toUpper)
.andThen(getLength);
int result1 = pipeline1.apply(" hello "); // 5
// compose - composition de droite à gauche
Function1<String, Integer> pipeline2 = getLength
.compose(toUpper)
.compose(trim);
int result2 = pipeline2.apply(" hello "); // 5 (même résultat)Function1
Exemple de currification, permettant d'appliquer partiellement des paramètres
Function2<Integer, Integer, Integer> add = (a, b) -> a + b;
Function2<Integer, Integer, Integer> multiply = (a, b) -> a * b;
// Currying - transformer Function2 en Function1
Function1<Integer, Integer> add5 = add.curried().apply(5);
Function1<Integer, Integer> multiplyBy3 = multiply.curried().apply(3);
// Composition de fonctions curryfiées
Function1<Integer, Integer> addThenMultiply = add5.andThen(multiplyBy3);
int result = addThenMultiply.apply(10); // (10 + 5) * 3 = 45Function2
Memoization : Optimisez vos calculs
La mémorisation permet de mettre en cache les résultats d'une fonction pour éviter des calculs répétitifs.
// Fonction coûteuse
Function1<Integer, Integer> expensiveCalculation = n -> {
System.out.println("Calcul pour: " + n);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
return n * n;
};
// Version mémorisée
Function1<Integer, Integer> memoized = expensiveCalculation.memoized();
System.out.println(memoized.apply(5)); // Calcul effectué
System.out.println(memoized.apply(5)); // Résultat en cache
System.out.println(memoized.apply(3)); // Nouveau calcul
System.out.println(memoized.apply(5)); // Résultat en cacheMemoization
Avantages de Vavr
Collections :
- Immuable par défaut
- API fluide et expressive
Gestion d'erreurs :
Optionplus riche qu'OptionalEitherpour erreurs typéesTrypour exceptionsValidationpour accumulation d'erreurs
Composition :
- Fonctions composables
- Memoization intégrée
- Currying automatique
Migration :
- Compatible avec Java existant
- Introduction progressive possible
- Interopérabilité parfaite
- Portage partiel de fonctionnalités Scala et Kotlin
Vavr transforme Java en langage fonctionnel moderne tout en gardant la familiarité Java.
✍️ Conclusion
Avec Vavr, Java franchit un cap décisif vers la programmation fonctionnelle moderne. Cette librairie ne se contente pas de combler les lacunes du JDK : elle transforme véritablement votre façon d'écrire du code Java.
🔑 Les apports essentiels de Vavr
Sécurité et robustesse : Les collections immuables et la gestion typée des erreurs avec Either, Try et Validation éliminent une grande partie des bugs courants en Java traditionnel.
Expressivité : le code devient plus lisible et déclaratif grâce aux compositions de fonctions et aux opérations fluides sur les collections.
Performance : la mémorisation automatique et les structures de données optimisées améliorent les performances sans complexité supplémentaire.
Adoption progressive : l'interopérabilité parfaite avec l'écosystème Java existant permet une migration en douceur, sans révolution brutale de votre codebase.
Vers une nouvelle ère du développement Java
Vavr démontre que Java peut rivaliser avec les langages fonctionnels modernes comme Scala ou Kotlin, tout en conservant sa stabilité et sa maturité. En adoptant Vavr, vous investissez dans un code plus maintenable, plus sûr et plus expressif.
La programmation fonctionnelle n'est plus l'apanage des langages académiques : avec Vavr, elle devient accessible et pratique pour tous les développeurs Java. Il est temps de franchir le pas et d'explorer ces nouveaux horizons !
Cet article conclut notre série sur la programmation fonctionnelle en Java. Nous espérons que ce voyage vous aura donné les clés pour écrire du code Java plus moderne et plus robuste. N'hésitez pas à expérimenter avec Vavr dans vos projets !