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 :
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 :
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 :
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 :
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 :
SELECT *
FROM employees
WHERE gender = 'M'
ORDER BY salary DESC
LIMIT 3;avec l'API Stream en Java :
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 :
SELECT *
FROM employees
ORDER BY
gender ASC,
salary ASC,
name ASC;avec l'API Stream en Java :
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 :
SELECT
MIN(age) AS youngestAge
FROM employees;Avec l'API Stream en Java :
Optional<Integer> youngestAge = employees().stream()
.map(Employee::getAge)
.min(Integer::compare); // MIN(age)III-B. MAX : Plus haut salaire▲
En SQL :
SELECT
MAX(salary) AS highestSalary
FROM employees;Avec l'API Stream en Java :
Optional<Integer> highestSalary = employees().stream()
.map(Employee::getSalary)
.max(Integer::compare); // MAX(salary)III-C. AVG : Moyenne d'âge des employés▲
En SQL :
SELECT
AVG(age) AS averageAge
FROM employees;Avec l'API Stream en Java :
OptionalDouble averageAge = employees().stream()
.mapToInt(Employee::getAge)
.average(); // AVG(age)III-D. COUNT : Nombre de femmes▲
En SQL :
SELECT
COUNT(*) AS womenCount
FROM employees WHERE gender = 'F';Avec l'API Stream en Java :
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 :
SELECT
SUM(salary) * 1.217 AS salarySumWithTaxes
FROM employees;Avec l'API Stream en Java :
double salarySumWithTaxes = employees().stream()
.mapToDouble(Employee::getSalary)
.sum() * 1.217; // SUM(salary) * 1.217IV. 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 :
- Optional<T> reduce(BinaryOperator<T> accumulator) ;
- T reduce(T valeurInitiale, BinaryOperator<T> accumulator).
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▲
Optional<Integer> youngestAge = employees().stream()
.map(Employee::getAge)
.reduce((a, b) -> (a < b) ? a : b); // MIN(age)IV-B. MAX : Plus haut salaire▲
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▲
// 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▲
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▲
double salarySumWithTaxes = employees().stream()
.map(Employee::getSalary)
.reduce(0, (a, b) -> (a + b)) * 1.217; // SUM(salary) * 1.217V. Le regroupement▲
Les streams permettent de réaliser des regroupements.
V-A. La syntaxe GROUP BY▲
En SQL :
SELECT salary
FROM employees
GROUP BY salary;Avec l'API Stream en Java :
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 :
SELECT DISTINCT salary
FROM employees;Avec l'API Stream en Java :
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 :
SELECT gender, MAX(salary) AS salary
FROM employees
GROUP BY gender;Avec l'API Stream en Java :
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 :
SELECT gender, AVG(salary) AS salary
FROM employees
GROUP BY gender;Avec l'API Stream en Java :
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 :
SELECT gender, COUNT(*) AS count
FROM employees
GROUP BY gender;Avec l'API Stream en Java :
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 :
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 :
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 :
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 :
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 :
<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 :
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 :
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.





