Articles

Filtrage d’une Collection Java par une Liste

Aperçu

Le filtrage d’une Collection par une Liste est un scénario de logique métier courant. Il existe de nombreuses façons d’y parvenir. Cependant, certaines peuvent conduire à des solutions sous-performantes si elles ne sont pas faites correctement.

Dans ce tutoriel, nous allons comparer certaines implémentations de filtrage et discuter de leurs avantages et inconvénients.

En utilisant une boucle For-Each

Nous commencerons par la syntaxe la plus classique, une boucle for-each.

Pour cet exemple et tous les autres de cet article, nous utiliserons la classe suivante:

public class Employee { private Integer employeeNumber; private String name; private Integer departmentId; //Standard constructor, getters and setters.}

Nous utiliserons également les méthodes suivantes pour tous les exemples, par souci de simplicité:

private List<Employee> buildEmployeeList() { return Arrays.asList( new Employee(1, "Mike", 1), new Employee(2, "John", 1), new Employee(3, "Mary", 1), new Employee(4, "Joe", 2), new Employee(5, "Nicole", 2), new Employee(6, "Alice", 2), new Employee(7, "Bob", 3), new Employee(8, "Scarlett", 3));}private List<String> employeeNameFilter() { return Arrays.asList("Alice", "Mike", "Bob");}

Pour notre exemple, nous filtrerons la première liste d’employés en fonction de la deuxième liste avec des noms d’employés pour trouver uniquement les employés avec ces noms spécifiques .

Maintenant, voyons l’approche traditionnelle – parcourir les deux listes à la recherche de correspondances:

@Testpublic void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingForEachLoop() { List<Employee> filteredList = new ArrayList<>(); List<Employee> originalList = buildEmployeeList(); List<String> nameFilter = employeeNameFilter(); for (Employee employee : originalList) { for (String name : nameFilter) { if (employee.getName().equals(name)) { filteredList.add(employee); // break; } } } assertThat(filteredList.size(), is(nameFilter.size()));}

C’est une syntaxe simple, mais assez verbeuse et en fait assez inefficace. En termes simples, il parcourt le produit cartésien des deux ensembles afin d’obtenir notre réponse.

Même en ajoutant une pause pour sortir plus tôt, on itérera toujours dans le même ordre qu’un produit cartésien dans le cas moyen.

Si nous appelons la taille de la liste des employés n, alors le filtre de nom sera tout aussi grand dans l’ordre, ce qui nous donne une classification O(n2).

En utilisant Streams et List #contains

Nous allons maintenant refactoriser la méthode précédente en utilisant lambdas pour simplifier la syntaxe et améliorer la lisibilité. Utilisons également la méthode List#contains comme filtre lambda:

@Testpublic void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambda() { List<Employee> filteredList; List<Employee> originalList = buildEmployeeList(); List<String> nameFilter = employeeNameFilter(); filteredList = originalList.stream() .filter(employee -> nameFilter.contains(employee.getName())) .collect(Collectors.toList()); assertThat(filteredList.size(), is(nameFilter.size()));}

En utilisant l’API Stream, la lisibilité a été grandement améliorée, mais notre code reste aussi inefficace que notre méthode précédente car il itère toujours dans le produit cartésien en interne. Ainsi, nous avons la même classification O(n2).

Utilisation de flux avec HashSet

Pour améliorer les performances, nous devons utiliser la méthode HashSet #contains. Cette méthode diffère de List#contains car elle effectue une recherche de code de hachage, ce qui nous donne un nombre constant d’opérations:

@Testpublic void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambdaAndHashSet() { List<Employee> filteredList; List<Employee> originalList = buildEmployeeList(); Set<String> nameFilterSet = employeeNameFilter().stream().collect(Collectors.toSet()); filteredList = originalList.stream() .filter(employee -> nameFilterSet.contains(employee.getName())) .collect(Collectors.toList()); assertThat(filteredList.size(), is(nameFilterSet.size()));}

En utilisant HashSet, notre efficacité de code s’est considérablement améliorée sans affecter la lisibilité. Puisque HashSet # contient des exécutions en temps constant, nous avons amélioré notre classification en O(n).

Conclusion

Dans ce tutoriel rapide, nous avons appris à filtrer une Collection par une liste de valeurs et les inconvénients de l’utilisation de ce qui peut sembler la méthode la plus simple.

Nous devons toujours considérer l’efficacité car notre code peut finir par s’exécuter dans d’énormes ensembles de données, et les problèmes de performances peuvent avoir des conséquences catastrophiques dans de tels environnements.

Tout le code présenté dans cet article est disponible sur GitHub.

Commencez avec Spring 5 et Spring Boot 2, via le cours Learn Spring:

>>CONSULTEZ LE COURS