Java: Programmation fonctionnelle
Bassirou Ndiaye
Les expressions lambda ont radicalement changé la façon de programmer des développeurs java.
Avec les interfaces fonctionnelles standard fournies par Java et l’API Stream, effectuaient des opérations en vrac, en parallèle ou en séquentiel devient un jeu d’enfant.
Ce qui rend le code beaucoup plus élégant et moderne comme le langage JavaScript par exemple.
Dans cet article, nous allons vous montrer avec du code la nette différence entre la programmation fonctionnelle et la programmation impérative (traditionnelle).
Puis nous vous ferons découvrir l’univers des interfaces fonctionnelles de Java.
Programmation impérative vs fonctionnelle
Imaginez vous implémenter la fonctionnalité, trouver le nombre de produits qui ont un prix supérieur a un montant défini pour un e-commerce.
Considérons la classe Product défini comme suit:
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public double getPrice() {
return price;
}
}
Une simple classe qui décrit un produit avec son nom et son prix, rien de compliqué jusque là.
Soit notre programme principal:
public class Main {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Iphone 8", 290000),
new Product("Iphone X", 320000),
new Product("MacBook Air", 370000),
new Product("Google pixel 3", 270000),
new Product("Huawei P20", 300000)
);
}
}
Maintenant, faisons simple et implémentons la fonctionnalité dans la fonction main de manière:
- Impérative:
public class Main {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Iphone 8", 290_000),
new Product("Iphone X", 320_000),
new Product("MacBook Air", 370_000),
new Product("Google pixel 3", 270_000),
new Product("Huawei P20", 300_000)
);
final double definedPrice = 300_000;
Long count = 0;
for (Product product: products) {
if(product.getPrice() > definedPrice)
count++;
}
System.out.println(count);
}
}
Tout marche parfaitement comme à l'ancienne, on utilise une boucle for et un if et le tour est jouer.
Que peut bien améliorer la programmation fonctionnelle dans ça ?
- Fonctionnelle ou déclarative
public class Main {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Iphone 8", 290_000),
new Product("Iphone X", 320_000),
new Product("MacBook Air", 370_000),
new Product("Google pixel 3", 270_000),
new Product("Huawei P20", 300_000)
);
final double definedPrice = 300_000;
Long count = products.stream()
.filter(product->product.getPrice() > definedPrice)
.count();
System.out.println(count);
}
}
Pas de if pas de for que des instructions :
- filtre tous les produits qui ont un prix supérieur 300,000
- compte le nombre de produits trouvés
Résultat : on a un code beaucoup plus parlant et concis.
La méthode stream() est disponible pour tout ce qui est collection Java (pas seulement).
Cette fonction retourne une Stream qui contrairement aux collections ne conserve pas de données et expose des méthodes plus qu'utiles pour effectuaient des opérations sur des collections.
Mais d'où vient toute cette magie, c'est ce que nous allons découvrir dans les prochaines sections de notre article.
Les interfaces fonctionnelles standard de Java
Java met à notre disposition plusieurs interfaces fonctionnelles prédéfinies pour nous faciliter certaine taches. Vous pouvez les retrouver dans le package java.util.function.
Malgré leur nombre important il en existe quatre catégories:
- Predicate : Représente une opération qui prend un argument en paramètre et vérifie s'il satisfait un critère.
Exemple:
Long count = products.stream()
.filter(product->product.getPrice() > definedPrice)
.count();
La méthode filter prend en paramètre un Predicate , si on extrait son argument on obtient:
Predicate<Product> productPredicate = product -> product.getPrice() > definedPrice;
Long count = products
.stream()
.filter(productPredicate)
.count();
- Supplier : Représente une opération qui prend aucun argument en paramètre et retourne une valeur.
Exemple: Pour construire une fonction qui renvoie une valeur aléatoire on pourrait faire:
Supplier<Double> getRandomNumber = () -> Math.random(); System.out.println(getRandomNumber.get());
- Consumer : Représente une opération qui prend un argument en paramètre et retourne rien.
Exemple: Si on voulait afficher chaque produit de la liste.
products.forEach(product -> System.out.println(product));
Si on extrait l'argument du foreach on obtient:
Consumer<Product> productConsumer = product -> System.out.println(product); products.forEach(productConsumer);
- Function : Représente une opération qui prend un argument en paramètre et le transforme.
Exemple: Pour construire une fonction qui additionne par un la valeur qu'on lui fournit en argument on pourrait faire:
Function<Integer, Integer> increase = n -> n + 1; int n = increase.apply(3); System.out.println(n);
Conclusion
Nous avons vu la différence entre la programmation fonctionnelle et la programmation impérative.
Malgré un apport clair a la clarté du code, n'abusez pas de la programmation fonctionnelle.
Pour une meilleure utilisation de ce paradigme en Java, la compréhension des Stream est un must.
Ce qui sera l'objet de notre prochain article.