Tutoriel pour apprendre les Stream Java grâce au SQL

Ce tutoriel s'adresse aux développeurs qui souhaitent découvrir (ou redécouvrir) l'API stream introduite dans Java 8 grâce au langage SQL.

2 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Prérequis

Vous pouvez utiliser l'IDE que vous souhaitez pour suivre ce tutoriel. Le code source des exemples présentés dans ce tutoriel est disponible dans ce repository Git, et plus particulièrement dans cette classe.

Les outils dont vous aurez besoin pour suivre ce tutoriel sont : JDK 8 d'Oracle, Maven 3.

N'hésitez pas à lire le tutoriel Maven 3 si vous souhaitez en apprendre plus sur ce dernier.

Avoir un SGBDR installé sur votre machine est un plus pour tester les exemples SQL. Si vous souhaitez tester les requêtes SQL qui vont suivre, vous pouvez exécuter le script ci-dessous dans une base de test :

 
CacherSélectionnez
CREATE TABLE employees (
    gender    VARCHAR(100),
    age        INTEGER,
    salary    INTEGER,
    name    VARCHAR(100)
);


INSERT INTO employees (gender, age, salary, name)
VALUES
    ('M', 40, 9800, 'Bruce WAYNE'),
    ('M', 33, 1500, 'Clark KENT'),
    ('M', 23, 4500, 'Barry ALLEN'),
    ('M', 19, 2400, 'Wally WEST'),
    ('M', 28, 8000, 'Hal JORDAN'),
    ('M', 35, 9500, 'Oliver QUEEN'),
    ('M', 42, 4700, 'Ray PALMER'),
    ('M', 22, 3600, 'Victor Stone'),
    ('M', 27, 1500, 'John CONSTANTINE'),
    ('M', 65, 2600, 'J''onn J''ONZZ'),
    ('M', 28, 1400, 'Arthur CURRY'),
    ('M', 25, 2500, 'Dick GRAYSON'),
    ('F', 65, 6000, 'Diana PRINCE'),
    ('F', 24, 2500, 'Barbara GORDON'),
    ('F', 36, 1800, 'Selina KYLE'),
    ('F', 30, 2400, 'Pamela ISLEY'),
    ('F', 28, 2400, 'Harleen QUINZEL'),
    ('F', 29, 2000, 'Zatanna ZATARA');

Voici un aperçu des données Java :

 
CacherSélectionnez
public static List<Employee> employees() {
    return new ArrayList<Employee>(){{
        add(Employee.builder().gender("M").age(40).salary(9800).name("Bruce WAYNE").build());
        add(Employee.builder().gender("M").age(33).salary(1500).name("Clark KENT").build());
        add(Employee.builder().gender("M").age(23).salary(4500).name("Barry ALLEN").build());
        add(Employee.builder().gender("M").age(19).salary(2400).name("Wally WEST").build());
        add(Employee.builder().gender("M").age(28).salary(8000).name("Hal JORDAN").build());
        add(Employee.builder().gender("M").age(35).salary(9500).name("Oliver QUEEN").build());
        add(Employee.builder().gender("M").age(42).salary(4700).name("Ray PALMER").build());
        add(Employee.builder().gender("M").age(22).salary(3600).name("Victor Stone").build());
        add(Employee.builder().gender("M").age(27).salary(1500).name("John CONSTANTINE").build());
        add(Employee.builder().gender("M").age(65).salary(2600).name("J'onn J'ONZZ").build());
        add(Employee.builder().gender("M").age(28).salary(1400).name("Arthur CURRY").build());
        add(Employee.builder().gender("M").age(25).salary(2500).name("Dick GRAYSON").build());
        add(Employee.builder().gender("F").age(65).salary(6000).name("Diana PRINCE").build());
        add(Employee.builder().gender("F").age(24).salary(2500).name("Barbara GORDON").build());
        add(Employee.builder().gender("F").age(36).salary(1800).name("Selina KYLE").build());
        add(Employee.builder().gender("F").age(30).salary(2400).name("Pamela ISLEY").build());
        add(Employee.builder().gender("F").age(28).salary(2400).name("Harleen QUINZEL").build());
        add(Employee.builder().gender("F").age(29).salary(2000).name("Zatanna ZATARA").build());
    }};
}

II. Les bases

L'API Stream est inspirée des langages fonctionnels, elle permet de réaliser diverses opérations sur des ensembles en utilisant le fluent pattern (ou « chaînage de méthodes » en français). Pour notre cas, les collections (ou « listes » si vous préférez) représenteront ces ensembles.

L'API Stream ne permet pas de réaliser des choses qu'il n'était pas possible de faire avant, par contre elle améliore grandement la lisibilité du code (à moins d'en abuser bien évidemment). Voici deux algorithmes qui permettent de récupérer les trois employés (homme) les plus riches de l'entreprise :

Sans l'API Stream :

 
Sélectionnez
List<Employee> temp = new ArrayList<>();
for (Employee employee : employees()) {
    if ("M".equals(employee.getGender())) { // WHERE gender = 'M'
        temp.add(employee);
    }
}

Collections.sort(temp, (e1, e2) -> e2.getSalary() - e1.getSalary()); // ORDER BY salary DESC
int limit = (temp.size() > 0 && temp.size() <= 3) ? temp.size() : 3;
List<Employee> result = (
    temp.isEmpty()) ? Collections.emptyList() : temp.subList(0, limit); // LIMIT X

// Print
System.out.println("Salary | Name");
result.forEach(employee ->
    System.out.println(
        String.format("%s   | %s", employee.getSalary(), employee.getName())
    )
);

Avec l'API Stream :

 
Sélectionnez
List<Employee> result = employees().stream()
    .filter(employee -> "M".equals(employee.getGender())) // WHERE gender = 'M'
    .sorted((e1, e2) -> e2.getSalary() - e1.getSalary())  // ORDER BY salary DESC
    .limit(3) // LIMIT X
    .collect(Collectors.toList());

// Print
System.out.println("Salary | Name");
result.forEach(employee ->
    System.out.println(
        String.format("%s   | %s", employee.getSalary(), employee.getName())
    )
);

Pour les exemples qui vont suivre, nous allons d'un côté réaliser des opérations sur une table en SQL, et de l'autre sur une liste d'objets en Java. Reprenons l'exemple précédent :

en SQL :

 
Sélectionnez
SELECT *
FROM employees
WHERE gender = 'M'
ORDER BY salary DESC
LIMIT 3;

avec l'API Stream en Java :

 
Sélectionnez
List<Employee> result = employees().stream()
    .filter(employee -> "M".equals(employee.getGender())) // WHERE gender = 'M'
    .sorted((e1, e2) -> e2.getSalary() - e1.getSalary())  // ORDER BY salary DESC
    .limit(3)                                             // LIMIT X
    .collect(Collectors.toList());

Grâce à cet exemple de base, nous avons vu comment filtrer, ordonner et limiter le résultat.

L'exemple ci-dessous permet d'ajouter plusieurs critères pour ordonner le résultat :

en SQL :

 
Sélectionnez
SELECT *
FROM employees
ORDER BY
    gender ASC,
    salary ASC,
    name ASC;

avec l'API Stream en Java :

 
Sélectionnez
List<Employee> result = employees().stream()
    .sorted(
        Comparator.comparing(Employee::getGender) // ORDER BY gender ASC
            .thenComparing(Employee::getSalary)   // , salary ASC
            .thenComparing(Employee::getName)     // , name ASC
    )
    .collect(Collectors.toList());

III. Les fonctions d'agrégat

Les fonctions d'agrégat permettent, comme leur nom indique, d'agréger un ensemble. Voici quelques opérations d'agrégat sur notre liste d'employés.

III-A. MIN : Plus jeune âge

En SQL :

 
Sélectionnez
SELECT
    MIN(age) AS youngestAge
FROM employees;

Avec l'API Stream en Java :

 
Sélectionnez
Optional<Integer> youngestAge = employees().stream()
    .map(Employee::getAge)
    .min(Integer::compare); // MIN(age)

III-B. MAX : Plus haut salaire

En SQL :

 
Sélectionnez
SELECT
    MAX(salary) AS highestSalary
FROM employees;

Avec l'API Stream en Java :

 
Sélectionnez
Optional<Integer> highestSalary =  employees().stream()
    .map(Employee::getSalary)
    .max(Integer::compare); // MAX(salary)

III-C. AVG : Moyenne d'âge des employés

En SQL :

 
Sélectionnez
SELECT
    AVG(age) AS averageAge
FROM employees;

Avec l'API Stream en Java :

 
Sélectionnez
OptionalDouble averageAge = employees().stream()
    .mapToInt(Employee::getAge)
    .average(); // AVG(age)

III-D. COUNT : Nombre de femmes

En SQL :

 
Sélectionnez
SELECT
    COUNT(*) AS womenCount
FROM employees WHERE gender = 'F';

Avec l'API Stream en Java :

 
Sélectionnez
long womenCount = employees().stream()
    .filter(employee -> "F".equals(employee.getGender())) // WHERE gender = 'F'
    .count(); // COUNT(*)

III-E. SUM : Somme des salaires avec 21,7 % de taxe

En SQL :

 
Sélectionnez
SELECT
    SUM(salary) * 1.217 AS salarySumWithTaxes
FROM employees;

Avec l'API Stream en Java :

 
Sélectionnez
double salarySumWithTaxes = employees().stream()
    .mapToDouble(Employee::getSalary)
    .sum() * 1.217; // SUM(salary) * 1.217

IV. La syntaxe Reduce

Il est possible de réaliser l'ensemble des opérations effectuées dans la partie III manuellement grâce à la méthode de base reduce().

La méthode reduce() possède plusieurs signatures de méthode. Nous en étudierons deux :

La méthode reduce() prend en paramètre un lambda avec deux paramètres, le premier paramètre du lambda représente le résultat des calculs effectués précédemment et le second paramètre le nouvel élément du stream, l'implémentation de votre lambda contiendra donc l'opération que vous comptez effectuer entre le résultat de vos précédentes opérations et le nouvel élément du stream.

À savoir

  • La première méthode retourne un optional. Si vous utilisez la première méthode et que votre stream ne contient aucun élément, vous obtiendrez un optional vide.
  • La deuxième méthode retourne directement votre résultat. Le premier paramètre de la deuxième méthode représente la valeur initiale de votre stream, et ce sera la valeur retournée si votre stream ne contient aucun élément.
  • Les deux méthodes ont un intérêt différent, par exemple, si vous souhaitez relever le plus haut salaire mensuel à partir de 10 000 €, mais que votre stream ne contient aucun salaire aussi élevé, il serait peut-être préférable d'utiliser la première méthode et de retourner un optional vide plutôt que 0. Par contre, si vous souhaitez retourner le nombre de femmes et qu'il n'y a pas de femme dans l'entreprise, il serait peut-être préférable d'utiliser la deuxième et de retourner 0 plutôt qu'un optional vide.
  • Si votre stream ne contient qu'un seul élément, le résultat de votre reduce sera cet élément.
  • Si vous utilisez la première méthode, lors de la première opération, le premier paramètre de votre lambda vaut le premier élément du stream et le second paramètre vaut le second élément du stream. Si vous utilisez la deuxième méthode, lors de la première opération, le premier paramètre de votre lambda vaut l'opération réalisée entre la valeur initiale de votre stream et le premier élément du stream. Lors des autres passages, le premier paramètre de votre lambda vaudra le résultat de vos précédentes opérations, et le second paramètre le nouvel élément du stream.

IV-A. MIN : Plus jeune âge

 
Sélectionnez
Optional<Integer> youngestAge = employees().stream()
    .map(Employee::getAge)
    .reduce((a, b) -> (a < b) ? a : b); // MIN(age)

IV-B. MAX : Plus haut salaire

 
Sélectionnez
Optional<Integer> highestSalary =  employees().stream()
    .map(Employee::getSalary)
    .reduce((a, b) -> (a > b) ? a : b); // MAX(age)

IV-C. AVG : Moyenne d'âge des employés

 
Sélectionnez
// AVG => SUM() / COUNT(*)
AtomicInteger count = new AtomicInteger(1);
OptionalDouble sumAge = employees().stream()
    .mapToDouble(Employee::getAge)
    .reduce((a, b) -> {
        count.incrementAndGet();
        return a + b;
    });
OptionalDouble averageAge = sumAge.isPresent() ?
    OptionalDouble.of(sumAge.getAsDouble() / count.get()) : OptionalDouble.empty();

IV-D. COUNT : Nombre de femmes

 
Sélectionnez
long womenCount = employees().stream()
    .filter(employee -> "F".equals(employee.getGender())) // WHERE gender = 'F'
    .map(e -> 1L)
    .reduce(0L, (a, b) -> a + b); // COUNT(*)

IV-E. SUM : Somme des salaires avec 21,7 % de taxe

 
Sélectionnez
double salarySumWithTaxes = employees().stream()
    .map(Employee::getSalary)
    .reduce(0, (a, b) -> (a + b)) * 1.217; // SUM(salary) * 1.217

V. Le regroupement

Les streams permettent de réaliser des regroupements.

V-A. La syntaxe GROUP BY

En SQL :

 
Sélectionnez
SELECT salary
FROM employees
GROUP BY salary;

Avec l'API Stream en Java :

 
Sélectionnez
Map<Integer, List<Employee>> genders2 = employees().stream()
    .collect(Collectors.groupingBy(Employee::getSalary));        // GROUP BY

// Print
System.out.println(String.format("Genders: %s",     genders2.entrySet().stream()
        .map(Map.Entry::getKey)
        .map(Object::toString)
        .collect(Collectors.joining(", "))
));

V-B. La syntaxe DISTINCT

En SQL :

 
Sélectionnez
SELECT DISTINCT salary
FROM employees;

Avec l'API Stream en Java :

 
Sélectionnez
List<Integer> genders1 = employees().stream()
    .map(Employee::getSalary)
    .distinct()        // DISTINCT
    .collect(Collectors.toList());

// Print
System.out.println(String.format("Genders: %s",
    genders1.stream()
        .map(Object::toString)
        .collect(Collectors.joining(", "))
));

V-C. Exemple avec MAX

En SQL :

 
Sélectionnez
SELECT gender, MAX(salary) AS salary
FROM employees
GROUP BY gender;

Avec l'API Stream en Java :

 
Sélectionnez
Map<String, Optional<Integer>> result = employees().stream()
    .collect(Collectors.groupingBy(
        Employee::getGender, // GROUP BY gender
        Collectors.mapping(Employee::getSalary, Collectors.maxBy(Integer::compare)) // MAX(salary)
    ));

// Print
result.entrySet().forEach(entry ->
    System.out.println(String.format("Gender: %s | Max Salary: %s", entry.getKey(), entry.getValue()))
);

V-D. Exemple avec AVG

En SQL :

 
Sélectionnez
SELECT gender, AVG(salary) AS salary
FROM employees
GROUP BY gender;

Avec l'API Stream en Java :

 
Sélectionnez
Map<String, Double> result = employees().stream()
    .collect(Collectors.groupingBy(
        Employee::getGender, // GROUP BY gender
        Collectors.averagingDouble(Employee::getSalary) // AVG(salary)
    ));

// Print
result.entrySet().forEach(entry ->
    System.out.println(String.format("Gender: %s | Average Salary: %s", entry.getKey(), entry.getValue()))
);

V-E. Exemple avec COUNT

En SQL :

 
Sélectionnez
SELECT gender, COUNT(*) AS count
FROM employees
GROUP BY gender;

Avec l'API Stream en Java :

 
Sélectionnez
Map<String, Long> result = employees().stream()
    .collect(Collectors.groupingBy(
        Employee::getGender,  // GROUP BY gender
        Collectors.counting() // COUNT(*)
    ));

// Print
result.entrySet().forEach(entry ->
    System.out.println(String.format("Gender: %s | Count: %s", entry.getKey(), entry.getValue()))
);

VI. La jointure cartésienne

Les streams permettent de réaliser des jointures cartésiennes.

En SQL :

 
Sélectionnez
SELECT t1.e AS x, t2.e AS y, t3.e AS z
FROM t1
CROSS JOIN t2
CROSS JOIN t3;

Avec l'API Stream en Java :

 
Sélectionnez
String[] t1 = {"A", "B", "C"};
String[] t2 = {"B", "C", "D"};
String[] t3 = {"C", "F", "E"};

// CROSS JOIN
List<Container> list = Stream.of(t1)
    .flatMap(x -> Arrays.stream(t2)            // t1 CROSS JOIN t2
        .flatMap(y -> Arrays.stream(t3)        // t2 CROSS JOIN t3
            .map(z -> new Container(x, y, z))
        )
    )
    .collect(Collectors.toList());

// Print
list.forEach(System.out::println);

VII. La jointure interne

Les streams permettent de réaliser des jointures internes.

En SQL :

 
Sélectionnez
SELECT t1.e AS x, t2.e AS y, t3.e AS z
FROM t1
INNER JOIN t2
ON t1.e = t2.e
INNER JOIN t3
ON t2.e = t3.e;

Avec l'API Stream en Java :

 
Sélectionnez
String[] t1 = {"A", "B", "C"};
String[] t2 = {"B", "C", "D"};
String[] t3 = {"C", "F", "E"};

// INNER JOIN
List<Container> list = Stream.of(t1)
    .flatMap(x -> Arrays.stream(t2)            // t1 INNER JOIN t2
        .filter(y -> Objects.equals(x, y))        // ON t1.e = t2.e
        .flatMap(y -> Arrays.stream(t3)        // t2 INNER JOIN t3
            .filter(z -> Objects.equals(y, z))    // ON t2.e = t3.e
            .map(z -> new Container(x, y, z))
        )
    ).collect(Collectors.toList());

// Print
list.forEach(System.out::println);

VIII. Les limites de l'API Stream

L'API stream permet de réaliser avec une grande aisance des opérations sur une collection, mais son utilisation possède aussi des limites lorsqu'il s'agit d'effectuer des jointures entre plusieurs collections. En effet, la syntaxe peut porter à confusion au fur et à mesure qu'une liste est ajoutée dans la chaîne d'opérations, de plus, les streams ne sont pas adaptés pour réaliser des jointures externes (cf. : LEFT JOIN / RIGHT JOIN / FULL JOIN).

Quoi de mieux que du SQL pour faire du SQL ? Et si nous essayions de manipuler des collections d'objets Java directement en SQL ? C'est ce que permet par exemple de faire l'API InMemorySQL du projet dbms-replication. Voici la dépendance Maven pour l'importer dans votre projet :

 
Sélectionnez
<dependency> 
    <groupId>com.github.gokan-ekinci</groupId> 
    <artifactId>dbms-replication</artifactId> 
    <version>1.0</version> 
</dependency>

Si nous reprenons l'exemple de la jointure cartésienne, cela donne :

 
Sélectionnez
List<Tuple> t1 = tupleInit("A", "B", "C");
List<Tuple> t2 = tupleInit("B", "C", "D");
List<Tuple> t3 = tupleInit("C", "D", "E");

String request = "SELECT t1.e AS x, t2.e AS y, t3.e AS z FROM t1 CROSS JOIN t2 CROSS JOIN t3";
new InMemorySQL()
    .add(Tuple.class, t1)
    .add(Tuple.class, t2)
    .add(Tuple.class, t3)
    .executeQuery(Container.class, request)
    .forEach(System.out::println);

Pour la jointure interne, cela donne :

 
Sélectionnez
List<Tuple> t1 = tupleInit("A", "B", "C");
List<Tuple> t2 = tupleInit("B", "C", "D");
List<Tuple> t3 = tupleInit("C", "D", "E");

String request = "SELECT t1.e AS x, t2.e AS y, t3.e AS z " +
        "FROM t1 " +
        "INNER JOIN t2 " +
        "ON t1.e = t2.e " +
        "INNER JOIN t3 " +
        "ON t2.e = t3.e";
new InMemorySQL()
    .add(Tuple.class, t1)
    .add(Tuple.class, t2)
    .add(Tuple.class, t3)
    .executeQuery(Container.class, request)
    .forEach(System.out::println);

IX. Remerciements

Je tiens à remercier Mickael Baron pour la relecture technique et ClaudeLELOUP pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2017 Gokan EKINCI. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.